diff --git a/Telerik.Sitefinity.Frontend/Designers/Scripts/page-editor-services.js b/Telerik.Sitefinity.Frontend/Designers/Scripts/page-editor-services.js index b04ea8a63..8ad508594 100644 --- a/Telerik.Sitefinity.Frontend/Designers/Scripts/page-editor-services.js +++ b/Telerik.Sitefinity.Frontend/Designers/Scripts/page-editor-services.js @@ -122,16 +122,15 @@ var deferred = $q.defer(); $http.get(getUrl(), requestOptions()) - .success(function (data) { - properties = data; + .then(function (response) { + properties = response.data; //clone the result in initialData - initialData = $.extend(true, {}, data); + initialData = $.extend(true, {}, response.data); loadingPromise = null; - deferred.resolve(data); - }) - .error(function (data) { + deferred.resolve(response.data); + }, function (response) { loadingPromise = null; - deferred.reject(data); + deferred.reject(response.data); }); loadingPromise = deferred.promise; @@ -191,12 +190,11 @@ var deferred = $q.defer(); $http.put(updateUrl(saveMode), modifiedData, requestOptions()) - .success(function (data) { + .then(function (response) { initialData = $.extend(true, {}, properties); - deferred.resolve(data); - }) - .error(function (data) { - deferred.reject(data); + deferred.resolve(response.data); + }, function (response) { + deferred.reject(response.data); }); return deferred.promise; diff --git a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.js b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.js index 335859f71..feb633824 100644 --- a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.js +++ b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.js @@ -1,9 +1,9 @@ /** - * @license AngularJS v1.4.8 - * (c) 2010-2015 Google, Inc. http://angularjs.org + * @license AngularJS v1.8.2 + * (c) 2010-2020 Google LLC. http://angularjs.org * License: MIT */ -(function(window, angular, undefined) {'use strict'; +(function(window, angular) {'use strict'; var $resourceMinErr = angular.$$minErr('$resource'); @@ -53,21 +53,33 @@ function shallowClearAndCopy(src, dst) { * @name ngResource * @description * - * # ngResource - * * The `ngResource` module provides interaction support with RESTful services * via the $resource service. * + * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage. + */ + +/** + * @ngdoc provider + * @name $resourceProvider + * + * @description + * + * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource} + * service. * - *
+ * ## Dependencies + * Requires the {@link ngResource } module to be installed. * - * See {@link ngResource.$resource `$resource`} for usage. */ /** * @ngdoc service * @name $resource * @requires $http + * @requires ng.$log + * @requires $q + * @requires ng.$timeout * * @description * A factory which creates a resource object that lets you interact with @@ -102,27 +114,36 @@ function shallowClearAndCopy(src, dst) { * can escape it with `/\.`. * * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - * `actions` methods. 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). + * `actions` methods. If a parameter value is a function, it will be called every time + * a param value needs to be obtained for a request (unless the param was overridden). The + * function will be passed the current data value as an argument. * * Each key value in the parameter object is first bound to url template if present and then any * excess keys are appended to the url search query after the `?`. * - * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * 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 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`. + * 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 actions + * with a request body). + * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of + * `someParam` will be `data.someProp`. + * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action + * method that does not accept a request body). + * + * @param {Object.=} actions Hash with declaration of custom actions that will be available + * in addition to the default set of resource actions (see below). If a custom action has the same + * key as a default action (e.g. `save`), then the default action will be *overwritten*, and not + * extended. * - * @param {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 $http.config}: + * The declaration should be created in the format of {@link ng.$http#usage $http.config}: * - * {action1: {method:?, params:?, isArray:?, headers:?, ...}, - * action2: {method:?, params:?, isArray:?, headers:?, ...}, - * ...} + * { + * action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ... + * } * * Where: * @@ -131,74 +152,96 @@ function shallowClearAndCopy(src, dst) { * - **`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). - * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * the parameter value is a function, it will be called every time when a param value needs to + * be obtained for a request (unless the param was overridden). The function will be passed the + * current data value as an argument. + * - **`url`** – {string} – Action specific `url` override. The url templating is supported just * like for the resource-level urls. * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, * see `returns` section. * - **`transformRequest`** – * `{function(data, headersGetter)|Array.}` – - * transform function or an array of such functions. The transform function takes the http + * 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 + * an object and serializes it using `angular.toJson`. To prevent this behavior, set * `transformRequest` to an empty array: `transformRequest: []` * - **`transformResponse`** – - * `{function(data, headersGetter)|Array.}` – - * 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 - * caching. - * - **`timeout`** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} that - * should abort the request when resolved. - * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * `{function(data, headersGetter, status)|Array.}` – + * Transform function or an array of such functions. The transform function takes the HTTP + * response body, headers and status 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}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. + * - **`timeout`** – `{number}` – Timeout in milliseconds.
+ * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are + * **not** supported in `$resource`, because the same value would be used for multiple requests. + * If you are looking for a way to cancel requests, you should use the `cancellable` option. + * - **`cancellable`** – `{boolean}` – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return + * value. Calling `$cancelRequest()` for a non-cancellable or an already completed/cancelled + * request will have no effect. + * - **`withCredentials`** – `{boolean}` – Whether to set the `withCredentials` flag on the * XHR object. See - * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * [XMLHttpRequest.withCredentials](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials) * for more information. - * - **`responseType`** - `{string}` - see - * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). - * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - - * `response` and `responseError`. Both `response` and `responseError` interceptors get called - * with `http response` object. See {@link ng.$http $http interceptors}. - * + * - **`responseType`** – `{string}` – See + * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType). + * - **`interceptor`** – `{Object=}` – The interceptor object has four optional methods - + * `request`, `requestError`, `response`, and `responseError`. See + * {@link ng.$http#interceptors $http interceptors} for details. Note that + * `request`/`requestError` interceptors are applied before calling `$http`, thus before any + * global `$http` interceptors. Also, rejecting or throwing an error inside the `request` + * interceptor will result in calling the `responseError` interceptor. + * The resource instance or collection is available on the `resource` property of the + * `http response` object passed to `response`/`responseError` interceptors. + * Keep in mind that the associated promise will be resolved with the value returned by the + * response interceptors. Make sure you return an appropriate value and not the `response` + * object passed as input. For reference, the default `response` interceptor (which gets applied + * if you don't specify a custom one) returns `response.resource`.
+ * See {@link ngResource.$resource#using-interceptors below} for an example of using + * interceptors in `$resource`. + * - **`hasBody`** – `{boolean}` – If true, then the request will have a body. + * If not specified, then only POST, PUT and PATCH requests will have a body. * * @param {Object} options Hash with custom settings that should extend the - * default `$resourceProvider` behavior. The only supported option is - * - * Where: + * default `$resourceProvider` behavior. The supported options are: * * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing * slashes from any calculated URL will be stripped. (Defaults to true.) + * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. + * This can be overwritten per action. (Defaults to false.) * * @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 - * { 'get': {method:'GET'}, - * 'save': {method:'POST'}, - * 'query': {method:'GET', isArray:true}, - * 'remove': {method:'DELETE'}, - * 'delete': {method:'DELETE'} }; + * { + * 'get': {method: 'GET'}, + * 'save': {method: 'POST'}, + * 'query': {method: 'GET', isArray: true}, + * 'remove': {method: 'DELETE'}, + * 'delete': {method: 'DELETE'} + * } * ``` * - * Calling these methods invoke an {@link ng.$http} with the specified http method, - * destination and parameters. When the data is returned from the server then the object is an - * instance of the resource class. The actions `save`, `remove` and `delete` are available on it - * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, - * read, update, delete) on server-side data like this: + * Calling these methods invoke {@link ng.$http} with the specified http method, destination and + * parameters. When the data is returned from the server then the object is an instance of the + * resource class. The actions `save`, `remove` and `delete` are available on it as methods with + * the `$` prefix. This allows you to easily perform CRUD operations (create, read, update, + * delete) on server-side data like this: * ```js - * var User = $resource('/user/:userId', {userId:'@id'}); - * var user = User.get({userId:123}, function() { + * var User = $resource('/user/:userId', {userId: '@id'}); + * User.get({userId: 123}).$promise.then(function(user) { * user.abc = true; * user.$save(); * }); * ``` * - * It is important to realize that invoking a $resource object method immediately returns an + * It is important to realize that invoking a `$resource` object method immediately returns an * empty reference (object or array depending on `isArray`). Once the data is returned from the * server the existing reference is populated with the actual data. This is a useful trick since * usually the resource is assigned to a model which is then rendered by the view. Having an empty @@ -209,157 +252,329 @@ function shallowClearAndCopy(src, dst) { * The action methods on the class object or instance object can be invoked with the following * parameters: * - * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` - * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` - * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * - "class" actions without a body: `Resource.action([parameters], [success], [error])` + * - "class" actions with a body: `Resource.action([parameters], postData, [success], [error])` + * - instance actions: `instance.$action([parameters], [success], [error])` * * - * Success callback is called with (value, responseHeaders) arguments, where the value is - * the populated resource instance or collection object. The error callback is called - * with (httpResponse) argument. + * When calling instance methods, the instance itself is used as the request body (if the action + * should have a body). By default, only actions using `POST`, `PUT` or `PATCH` have request + * bodies, but you can use the `hasBody` configuration option to specify whether an action + * should have a body or not (regardless of its HTTP method). * - * Class actions return empty instance (with additional properties below). - * Instance actions return promise of the action. * - * The Resource instances and collection have these additional properties: + * Success callback is called with (value (Object|Array), responseHeaders (Function), + * status (number), statusText (string)) arguments, where `value` is the populated resource + * instance or collection object. The error callback is called with (httpResponse) argument. * - * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * Class actions return an empty instance (with the additional properties listed below). + * Instance actions return a promise for the operation. + * + * The Resource instances and collections have these additional properties: + * + * - `$promise`: The {@link ng.$q promise} of the original server interaction that created this * instance or collection. * * On success, the promise is resolved with the same resource instance or collection object, - * updated with data from server. This makes it easy to use in - * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * updated with data from server. This makes it easy to use in the + * {@link ngRoute.$routeProvider `resolve` section of `$routeProvider.when()`} to defer view * rendering until the resource(s) are loaded. * - * On failure, the promise is resolved with the {@link ng.$http http response} object, without - * the `resource` property. + * On failure, the promise is rejected with the {@link ng.$http http response} object. * * If an interceptor object was provided, the promise will instead be resolved with the value - * returned by the interceptor. + * returned by the response interceptor (on success) or responceError interceptor (on failure). * * - `$resolved`: `true` after first server interaction is completed (either with success or * rejection), `false` before that. Knowing if the Resource has been resolved is useful in - * data-binding. + * data-binding. If there is a response/responseError interceptor and it returns a promise, + * `$resolved` will wait for that too. + * + * The Resource instances and collections have these additional methods: + * + * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or + * collection, calling this method will abort the request. + * + * The Resource instances have these additional methods: + * + * - `toJSON`: It returns a simple object without any of the extra properties added as part of + * the Resource API. This object can be serialized through {@link angular.toJson} safely + * without attaching AngularJS-specific fields. Notice that `JSON.stringify` (and + * `angular.toJson`) automatically use this method when serializing a Resource instance + * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON%28%29_behavior)). * * @example * - * # Credit card resource + * ### Basic usage * - * ```js - // Define CreditCard class - var CreditCard = $resource('/user/:userId/card/:cardId', - {userId:123, cardId:'@id'}, { - charge: {method:'POST', params:{charge:true}} - }); + ```js + // Define a CreditCard class + var CreditCard = $resource('/users/:userId/cards/:cardId', + {userId: 123, cardId: '@id'}, { + charge: {method: 'POST', params: {charge: true}} + }); // We can retrieve a collection from the server - var cards = CreditCard.query(function() { - // GET: /user/123/card - // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + var cards = CreditCard.query(); + // GET: /users/123/cards + // server returns: [{id: 456, number: '1234', name: 'Smith'}] + // Wait for the request to complete + cards.$promise.then(function() { var card = cards[0]; - // each item is an instance of CreditCard + + // Each item is an instance of CreditCard expect(card instanceof CreditCard).toEqual(true); - card.name = "J. Smith"; - // non GET methods are mapped onto the instances + + // Non-GET methods are mapped onto the instances + card.name = 'J. Smith'; card.$save(); - // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} - // server returns: {id:456, number:'1234', name: 'J. Smith'}; + // POST: /users/123/cards/456 {id: 456, number: '1234', name: 'J. Smith'} + // server returns: {id: 456, number: '1234', name: 'J. Smith'} - // our custom method is mapped as well. - card.$charge({amount:9.99}); - // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + // Our custom method is mapped as well (since it uses POST) + card.$charge({amount: 9.99}); + // POST: /users/123/cards/456?amount=9.99&charge=true {id: 456, number: '1234', name: 'J. Smith'} }); - // we can create an instance as well - var newCard = new CreditCard({number:'0123'}); - newCard.name = "Mike Smith"; - newCard.$save(); - // POST: /user/123/card {number:'0123', name:'Mike Smith'} - // server returns: {id:789, number:'0123', name: 'Mike Smith'}; - expect(newCard.id).toEqual(789); - * ``` + // We can create an instance as well + var newCard = new CreditCard({number: '0123'}); + newCard.name = 'Mike Smith'; + + var savePromise = newCard.$save(); + // POST: /users/123/cards {number: '0123', name: 'Mike Smith'} + // server returns: {id: 789, number: '0123', name: 'Mike Smith'} + + savePromise.then(function() { + // Once the promise is resolved, the created instance + // is populated with the data returned by the server + expect(newCard.id).toEqual(789); + }); + ``` + * + * The object returned from a call to `$resource` is a resource "class" which has one "static" + * method for each action in the definition. + * + * Calling these methods invokes `$http` on the `url` template with the given HTTP `method`, + * `params` and `headers`. * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. + * @example + * + * ### Accessing the response * - * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and - * `headers`. * When the data is returned from the server then the object is an instance of the resource type and * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD * operations (create, read, update, delete) on server-side data. - + * ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(user) { + var User = $resource('/users/:userId', {userId: '@id'}); + User.get({userId: 123}).$promise.then(function(user) { user.abc = true; user.$save(); }); ``` * - * It's worth noting that the success callback for `get`, `query` and other methods gets passed - * in the response that came from the server as well as $http header getter function, so one - * could rewrite the above example and get access to http headers as: + * It's worth noting that the success callback for `get`, `query` and other methods gets called with + * the resource instance (populated with the data that came from the server) as well as an `$http` + * header getter function, the HTTP status code and the response status text. So one could rewrite + * the above example and get access to HTTP headers as follows: * ```js - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}, function(u, getResponseHeaders){ - u.abc = true; - u.$save(function(u, putResponseHeaders) { - //u => saved user object - //putResponseHeaders => $http header getter + var User = $resource('/users/:userId', {userId: '@id'}); + User.get({userId: 123}, function(user, getResponseHeaders) { + user.abc = true; + user.$save(function(user, putResponseHeaders) { + // `user` => saved `User` object + // `putResponseHeaders` => `$http` header getter }); }); ``` * - * You can also access the raw `$http` promise via the `$promise` property on the object returned + * @example * + * ### Creating custom actions + * + * In this example we create a custom method on our resource to make a PUT request: + * + ```js + var app = angular.module('app', ['ngResource']); + + // 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', {id: '@id'}, { + update: {method: 'PUT'} + }); + }]); + + // In our controller we get the ID from the URL using `$location` + app.controller('NotesCtrl', ['$location', 'Notes', function($location, Notes) { + // First, retrieve the corresponding `Note` object from the server + // (Assuming a URL of the form `.../notes?id=XYZ`) + var noteId = $location.search().id; + var note = Notes.get({id: noteId}); + + note.$promise.then(function() { + note.content = 'Hello, world!'; + + // Now call `update` to save the changes on the server + Notes.update(note); + // This will PUT /notes/ID with the note object as the request payload + + // Since `update` is a non-GET method, it will also be available on the instance + // (prefixed with `$`), so we could replace the `Note.update()` call with: + //note.$update(); + }); + }]); ``` - var User = $resource('/user/:userId', {userId:'@id'}); - User.get({userId:123}) - .$promise.then(function(user) { - $scope.user = user; - }); + * + * @example + * + * ### Cancelling requests + * + * If an action's configuration specifies that it is cancellable, you can cancel the request related + * to an instance or collection (as long as it is a result of a "non-instance" call): + * + ```js + // ...defining the `Hotel` resource... + var Hotel = $resource('/api/hotels/:id', {id: '@id'}, { + // Let's make the `query()` method cancellable + query: {method: 'get', isArray: true, cancellable: true} + }); + + // ...somewhere in the PlanVacationController... + ... + this.onDestinationChanged = function onDestinationChanged(destination) { + // We don't care about any pending request for hotels + // in a different destination any more + if (this.availableHotels) { + this.availableHotels.$cancelRequest(); + } + + // Let's query for hotels in `destination` + // (calls: /api/hotels?location=) + this.availableHotels = Hotel.query({location: destination}); + }; ``` + * + * @example + * + * ### Using interceptors + * + * You can use interceptors to transform the request or response, perform additional operations, and + * modify the returned instance/collection. The following example, uses `request` and `response` + * interceptors to augment the returned instance with additional info: + * + ```js + var Thing = $resource('/api/things/:id', {id: '@id'}, { + save: { + method: 'POST', + interceptor: { + request: function(config) { + // Before the request is sent out, store a timestamp on the request config + config.requestTimestamp = Date.now(); + return config; + }, + response: function(response) { + // Get the instance from the response object + var instance = response.resource; + + // Augment the instance with a custom `saveLatency` property, computed as the time + // between sending the request and receiving the response. + instance.saveLatency = Date.now() - response.config.requestTimestamp; + + // Return the instance + return instance; + } + } + } + }); - * # 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']); - * - * // 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', - function($scope, $routeParams, Notes) { - * // First get a note object from the factory - * var note = Notes.get({ id:$routeParams.id }); - * $id = note.id; - * - * // Now call update passing in the ID first then the object you are updating - * Notes.update({ id:$id }, note); - * - * // This will PUT /notes/ID with the note object in the request payload - * }]); - * ``` + Thing.save({foo: 'bar'}).$promise.then(function(thing) { + console.log('That thing was saved in ' + thing.saveLatency + 'ms.'); + }); + ``` + * */ angular.module('ngResource', ['ng']). - provider('$resource', function() { - var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/; + info({ angularVersion: '1.8.2' }). + provider('$resource', function ResourceProvider() { + var PROTOCOL_AND_IPV6_REGEX = /^https?:\/\/\[[^\]]*][^/]*/; + var provider = this; + /** + * @ngdoc property + * @name $resourceProvider#defaults + * @description + * Object containing default options used when creating `$resource` instances. + * + * The default values satisfy a wide range of usecases, but you may choose to overwrite any of + * them to further customize your instances. The available properties are: + * + * - **stripTrailingSlashes** – `{boolean}` – If true, then the trailing slashes from any + * calculated URL will be stripped.
+ * (Defaults to true.) + * - **cancellable** – `{boolean}` – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return + * value. For more details, see {@link ngResource.$resource}. This can be overwritten per + * resource class or action.
+ * (Defaults to false.) + * - **actions** - `{Object.}` - A hash with default actions declarations. Actions are + * high-level methods corresponding to RESTful actions/methods on resources. An action may + * specify what HTTP method to use, what URL to hit, if the return value will be a single + * object or a collection (array) of objects etc. For more details, see + * {@link ngResource.$resource}. The actions can also be enhanced or overwritten per resource + * class.
+ * The default actions are: + * ```js + * { + * get: {method: 'GET'}, + * save: {method: 'POST'}, + * query: {method: 'GET', isArray: true}, + * remove: {method: 'DELETE'}, + * delete: {method: 'DELETE'} + * } + * ``` + * + * #### Example + * + * For example, you can specify a new `update` action that uses the `PUT` HTTP verb: + * + * ```js + * angular. + * module('myApp'). + * config(['$resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions.update = { + * method: 'PUT' + * }; + * }]); + * ``` + * + * Or you can even overwrite the whole `actions` list and specify your own: + * + * ```js + * angular. + * module('myApp'). + * config(['$resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions = { + * create: {method: 'POST'}, + * get: {method: 'GET'}, + * getAll: {method: 'GET', isArray:true}, + * update: {method: 'PUT'}, + * delete: {method: 'DELETE'} + * }; + * }); + * ``` + * + */ this.defaults = { // Strip slashes by default stripTrailingSlashes: true, + // Make non-instance requests cancellable (via `$cancelRequest()`) + cancellable: false, + // Default actions configuration actions: { 'get': {method: 'GET'}, @@ -370,52 +585,18 @@ angular.module('ngResource', ['ng']). } }; - this.$get = ['$http', '$q', function($http, $q) { + this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { 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, '+'); - } - - - /** - * 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' : '+')); - } + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isArray = angular.isArray, + isDefined = angular.isDefined, + isFunction = angular.isFunction, + isNumber = angular.isNumber, + encodeUriQuery = angular.$$encodeUriQuery, + encodeUriSegment = angular.$$encodeUriSegment; function Route(template, defaults) { this.template = template; @@ -429,36 +610,42 @@ angular.module('ngResource', ['ng']). url = actionUrl || self.template, val, encodedVal, - protocolAndDomain = ''; + protocolAndIpv6 = ''; - var urlParams = self.urlParams = {}; + var urlParams = self.urlParams = Object.create(null); forEach(url.split(/\W/), function(param) { if (param === 'hasOwnProperty') { - throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + 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; + if (!(new RegExp('^\\d+$').test(param)) && param && + (new RegExp('(^|[^\\\\]):' + param + '(\\W|$)').test(url))) { + urlParams[param] = { + isQueryParamValue: (new RegExp('\\?.*=:' + param + '(?:\\W|$)')).test(url) + }; } }); url = url.replace(/\\:/g, ':'); - url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { - protocolAndDomain = match; + url = url.replace(PROTOCOL_AND_IPV6_REGEX, function(match) { + protocolAndIpv6 = match; return ''; }); params = params || {}; - forEach(self.urlParams, function(_, urlParam) { + forEach(self.urlParams, function(paramInfo, 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) { + if (isDefined(val) && val !== null) { + if (paramInfo.isQueryParamValue) { + encodedVal = encodeUriQuery(val, true); + } else { + 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, + url = url.replace(new RegExp('(/?):' + urlParam + '(\\W|$)', 'g'), function(match, leadingSlashes, tail) { - if (tail.charAt(0) == '/') { + if (tail.charAt(0) === '/') { return tail; } else { return leadingSlashes + tail; @@ -472,11 +659,12 @@ angular.module('ngResource', ['ng']). 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` + // 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 = protocolAndDomain + url.replace(/\/\\\./, '/.'); + // Replace escaped `/\.` with `/.`. + // (If `\.` comes from a param value, it will be encoded as `%5C.`.) + config.url = protocolAndIpv6 + url.replace(/\/(\\|%5C)\./, '/.'); // set params - delegate param encoding to $http @@ -499,8 +687,8 @@ angular.module('ngResource', ['ng']). var ids = {}; actionParams = extend({}, paramDefaults, actionParams); forEach(actionParams, function(value, key) { - if (isFunction(value)) { value = value(); } - ids[key] = value && value.charAt && value.charAt(0) == '@' ? + if (isFunction(value)) { value = value(data); } + ids[key] = value && value.charAt && value.charAt(0) === '@' ? lookupDottedPath(data, value.substr(1)) : value; }); return ids; @@ -518,59 +706,82 @@ angular.module('ngResource', ['ng']). var data = extend({}, this); delete data.$promise; delete data.$resolved; + delete data.$cancelRequest; return data; }; forEach(actions, function(action, name) { - var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + var hasBody = action.hasBody === true || (action.hasBody !== false && /^(POST|PUT|PATCH)$/i.test(action.method)); + var numericTimeout = action.timeout; + var cancellable = isDefined(action.cancellable) ? + action.cancellable : route.defaults.cancellable; + + if (numericTimeout && !isNumber(numericTimeout)) { + $log.debug('ngResource:\n' + + ' Only numeric values are allowed as `timeout`.\n' + + ' Promises are not supported in $resource, because the same value would ' + + 'be used for multiple requests. If you are looking for a way to cancel ' + + 'requests, you should use the `cancellable` option.'); + delete action.timeout; + numericTimeout = null; + } Resource[name] = function(a1, a2, a3, a4) { - var params = {}, data, success, error; + var params = {}, data, onSuccess, onError; - /* jshint -W086 */ /* (purposefully fall through case statements) */ switch (arguments.length) { case 4: - error = a4; - success = a3; - //fallthrough + onError = a4; + onSuccess = a3; + // falls through case 3: case 2: if (isFunction(a2)) { if (isFunction(a1)) { - success = a1; - error = a2; + onSuccess = a1; + onError = a2; break; } - success = a2; - error = a3; - //fallthrough + onSuccess = a2; + onError = a3; + // falls through } else { params = a1; data = a2; - success = a3; + onSuccess = a3; break; } + // falls through case 1: - if (isFunction(a1)) success = a1; + if (isFunction(a1)) onSuccess = 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", + '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 requestInterceptor = action.interceptor && action.interceptor.request || undefined; + var requestErrorInterceptor = action.interceptor && action.interceptor.requestError || + undefined; var responseInterceptor = action.interceptor && action.interceptor.response || defaultResponseInterceptor; var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || - undefined; + $q.reject; + var successCallback = onSuccess ? function(val) { + onSuccess(val, response.headers, response.status, response.statusText); + } : undefined; + var errorCallback = onError || undefined; + var timeoutDeferred; + var numericTimeoutPromise; + var response; forEach(action, function(value, key) { switch (key) { @@ -580,36 +791,47 @@ angular.module('ngResource', ['ng']). case 'params': case 'isArray': case 'interceptor': - break; - case 'timeout': - httpConfig[key] = value; + case 'cancellable': break; } }); + if (!isInstanceCall && cancellable) { + timeoutDeferred = $q.defer(); + httpConfig.timeout = timeoutDeferred.promise; + + if (numericTimeout) { + numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); + } + } + 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, - promise = value.$promise; + // Start the promise chain + var promise = $q. + resolve(httpConfig). + then(requestInterceptor). + catch(requestErrorInterceptor). + then($http); + + promise = promise.then(function(resp) { + var data = resp.data; if (data) { // Need to convert action.isArray to boolean in case it is undefined - // jshint -W018 - if (angular.isArray(data) !== (!!action.isArray)) { + if (isArray(data) !== (!!action.isArray)) { throw $resourceMinErr('badcfg', 'Error in resource configuration for action `{0}`. Expected response to ' + 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', - angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); + isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); } - // jshint +W018 if (action.isArray) { value.length = 0; forEach(data, function(item) { - if (typeof item === "object") { + if (typeof item === 'object') { value.push(new Resource(item)); } else { // Valid JSON values may be string literals, and these should not be converted @@ -619,31 +841,32 @@ angular.module('ngResource', ['ng']). } }); } else { + var promise = value.$promise; // Save the promise shallowClearAndCopy(data, value); - value.$promise = promise; + value.$promise = promise; // Restore the promise } } - value.$resolved = true; - - response.resource = value; + resp.resource = value; + response = resp; + return responseInterceptor(resp); + }, function(rejectionOrResponse) { + rejectionOrResponse.resource = value; + response = rejectionOrResponse; + return responseErrorInterceptor(rejectionOrResponse); + }); - return response; - }, function(response) { + promise = promise['finally'](function() { value.$resolved = true; - - (error || noop)(response); - - return $q.reject(response); + if (!isInstanceCall && cancellable) { + value.$cancelRequest = noop; + $timeout.cancel(numericTimeoutPromise); + timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; + } }); - promise = promise.then( - function(response) { - var value = responseInterceptor(response); - (success || noop)(value, response.headers); - return value; - }, - responseErrorInterceptor); + // Run the `success`/`error` callbacks, but do not let them affect the returned promise. + promise.then(successCallback, errorCallback); if (!isInstanceCall) { // we are creating instance / collection @@ -651,12 +874,20 @@ angular.module('ngResource', ['ng']). // - return the instance / collection value.$promise = promise; value.$resolved = false; + if (cancellable) value.$cancelRequest = cancelRequest; return value; } // instance call return promise; + + function cancelRequest(value) { + promise.catch(noop); + if (timeoutDeferred !== null) { + timeoutDeferred.resolve(value); + } + } }; @@ -669,10 +900,6 @@ angular.module('ngResource', ['ng']). }; }); - Resource.bind = function(additionalParamDefaults) { - return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); - }; - return Resource; } diff --git a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.min.js b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.min.js index c3fb7aab9..6bd87c051 100644 --- a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.min.js +++ b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-resource.min.js @@ -1,14 +1,15 @@ /* - AngularJS v1.4.8 - (c) 2010-2015 Google, Inc. http://angularjs.org + AngularJS v1.8.2 + (c) 2010-2020 Google LLC. http://angularjs.org License: MIT */ -(function(I,f,C){'use strict';function D(t,e){e=e||{};f.forEach(e,function(f,k){delete e[k]});for(var k in t)!t.hasOwnProperty(k)||"$"===k.charAt(0)&&"$"===k.charAt(1)||(e[k]=t[k]);return e}var y=f.$$minErr("$resource"),B=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;f.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,e=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}}; -this.$get=["$http","$q",function(k,F){function w(f,g){this.template=f;this.defaults=r({},e.defaults,g);this.urlParams={}}function z(l,g,s,h){function c(a,q){var c={};q=r({},g,q);u(q,function(b,q){x(b)&&(b=b());var m;if(b&&b.charAt&&"@"==b.charAt(0)){m=a;var d=b.substr(1);if(null==d||""===d||"hasOwnProperty"===d||!B.test("."+d))throw y("badmember",d);for(var d=d.split("."),n=0,g=d.length;n */ - /* global -ngRouteModule */ -var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), - $routeMinErr = angular.$$minErr('ngRoute'); +/* global -ngRouteModule */ +var ngRouteModule = angular. + module('ngRoute', []). + info({ angularVersion: '1.8.2' }). + provider('$route', $RouteProvider). + // Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess` + // event (unless explicitly disabled). This is necessary in case `ngView` is included in an + // asynchronously loaded template. + run(instantiateRoute); +var $routeMinErr = angular.$$minErr('ngRoute'); +var isEagerInstantiationEnabled; + /** * @ngdoc provider * @name $routeProvider + * @this * * @description * * Used for configuring routes. * * ## Example - * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. + * See {@link ngRoute.$route#examples $route} for an example of configuring and using `ngRoute`. * * ## Dependencies * Requires the {@link ngRoute `ngRoute`} module to be installed. */ function $RouteProvider() { + isArray = angular.isArray; + isObject = angular.isObject; + isDefined = angular.isDefined; + noop = angular.noop; + function inherit(parent, extra) { return angular.extend(Object.create(parent), extra); } @@ -75,12 +167,12 @@ function $RouteProvider() { * * Object properties: * - * - `controller` – `{(string|function()=}` – Controller fn that should be associated with + * - `controller` – `{(string|Function)=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. * If present, the controller will be published to scope under the `controllerAs` name. - * - `template` – `{string=|function()=}` – html template as a string or a function that + * - `template` – `{(string|Function)=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. * This property takes precedence over `templateUrl`. @@ -90,7 +182,9 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * One of `template` or `templateUrl` is required. + * + * - `templateUrl` – `{(string|Function)=}` – path or function that returns a path to an html * template that should be used by {@link ngRoute.directive:ngView ngView}. * * If `templateUrl` is a function, it will be called with the following parameters: @@ -98,25 +192,39 @@ function $RouteProvider() { * - `{Array.}` - route parameters extracted from the current * `$location.path()` by applying the current route * - * - `resolve` - `{Object.=}` - An optional map of dependencies which should + * One of `templateUrl` or `template` is required. + * + * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, the router * will wait for them all to be resolved or one to be rejected before the controller is * instantiated. * If all the promises are resolved successfully, the values of the resolved promises are * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is * fired. If any of the promises are rejected the - * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object - * is: + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. + * For easier access to the resolved dependencies from the template, the `resolve` map will + * be available on the scope of the route, under `$resolve` (by default) or a custom name + * specified by the `resolveAs` property (see below). This can be particularly useful, when + * working with {@link angular.Module#component components} as route templates.
+ *
+ * **Note:** If your scope already contains a property with this name, it will be hidden + * or overwritten. Make sure, you specify an appropriate name for this property, that + * does not collide with other properties on the scope. + *
+ * The map object is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. - * - `factory` - `{string|function}`: If `string` then it is an alias for a service. + * - `factory` - `{string|Function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is {@link auto.$injector#invoke injected} * and the return value is treated as the dependency. If the result is a promise, it is * resolved before its value is injected into the controller. Be aware that * `ngRoute.$routeParams` will still refer to the previous route within these resolve * functions. Use `$route.current.params` to access the new route parameters, instead. * - * - `redirectTo` – {(string|function())=} – value to update + * - `resolveAs` - `{string=}` - The name under which the `resolve` map will be available on + * the scope of the route. If omitted, defaults to `$resolve`. + * + * - `redirectTo` – `{(string|Function)=}` – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: @@ -127,15 +235,50 @@ function $RouteProvider() { * - `{Object}` - current `$location.search()` * * The custom `redirectTo` function is expected to return a string which will be used - * to update `$location.path()` and `$location.search()`. + * to update `$location.url()`. If the function throws an error, no further processing will + * take place and the {@link ngRoute.$route#$routeChangeError $routeChangeError} event will + * be fired. + * + * Routes that specify `redirectTo` will not have their controllers, template functions + * or resolves called, the `$location` will be changed to the redirect url and route + * processing will stop. The exception to this is if the `redirectTo` is a function that + * returns `undefined`. In this case the route transition occurs as though there was no + * redirection. + * + * - `resolveRedirectTo` – `{Function=}` – a function that will (eventually) return the value + * to update {@link ng.$location $location} URL with and trigger route redirection. In + * contrast to `redirectTo`, dependencies can be injected into `resolveRedirectTo` and the + * return value can be either a string or a promise that will be resolved to a string. * - * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` + * Similar to `redirectTo`, if the return value is `undefined` (or a promise that gets + * resolved to `undefined`), no redirection takes place and the route transition occurs as + * though there was no redirection. + * + * If the function throws an error or the returned promise gets rejected, no further + * processing will take place and the + * {@link ngRoute.$route#$routeChangeError $routeChangeError} event will be fired. + * + * `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same + * route definition, will cause the latter to be ignored. + * + * - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes + * (including the path) even if the new URL maps to the same route. + * + * If the option is set to `false` and the URL in the browser changes, but the new URL maps + * to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without + * reloading the route). + * + * - `[reloadOnSearch=true]` - `{boolean=}` - reload route when only `$location.search()` * or `$location.hash()` changes. * - * If the option is set to `false` and url in the browser changes, then - * `$routeUpdate` event is broadcasted on the root scope. + * If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate` + * event is broadcasted on the root scope (without reloading the route). + * + *
+ * **Note:** This option has no effect if `reloadOnUrl` is set to `false`. + *
* - * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive + * - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive * * If the option is set to `true`, then the particular route can be matched without being * case sensitive @@ -147,7 +290,10 @@ function $RouteProvider() { */ this.when = function(path, route) { //copy original route object to preserve params inherited from proto chain - var routeCopy = angular.copy(route); + var routeCopy = shallowCopy(route); + if (angular.isUndefined(routeCopy.reloadOnUrl)) { + routeCopy.reloadOnUrl = true; + } if (angular.isUndefined(routeCopy.reloadOnSearch)) { routeCopy.reloadOnSearch = true; } @@ -156,18 +302,19 @@ function $RouteProvider() { } routes[path] = angular.extend( routeCopy, - path && pathRegExp(path, routeCopy) + {originalPath: path}, + path && routeToRegExp(path, routeCopy) ); // create redirection for trailing slashes if (path) { - var redirectPath = (path[path.length - 1] == '/') + var redirectPath = (path[path.length - 1] === '/') ? path.substr(0, path.length - 1) : path + '/'; routes[redirectPath] = angular.extend( - {redirectTo: path}, - pathRegExp(redirectPath, routeCopy) + {originalPath: path, redirectTo: path}, + routeToRegExp(redirectPath, routeCopy) ); } @@ -185,47 +332,6 @@ function $RouteProvider() { */ this.caseInsensitiveMatch = false; - /** - * @param path {string} path - * @param opts {Object} options - * @return {?Object} - * - * @description - * Normalizes the given path, returning a regular expression - * and the original path. - * - * Inspired by pathRexp in visionmedia/express/lib/utils.js. - */ - function pathRegExp(path, opts) { - var insensitive = opts.caseInsensitiveMatch, - ret = { - originalPath: path, - regexp: path - }, - keys = ret.keys = []; - - path = path - .replace(/([().])/g, '\\$1') - .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { - var optional = option === '?' ? option : null; - var star = option === '*' ? option : null; - keys.push({ name: key, optional: !!optional }); - slash = slash || ''; - return '' - + (optional ? '' : slash) - + '(?:' - + (optional ? slash : '') - + (star && '(.+?)' || '([^/]+)') - + (optional || '') - + ')' - + (optional || ''); - }) - .replace(/([\/$\*])/g, '\\$1'); - - ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); - return ret; - } - /** * @ngdoc method * @name $routeProvider#otherwise @@ -246,6 +352,47 @@ function $RouteProvider() { return this; }; + /** + * @ngdoc method + * @name $routeProvider#eagerInstantiationEnabled + * @kind function + * + * @description + * Call this method as a setter to enable/disable eager instantiation of the + * {@link ngRoute.$route $route} service upon application bootstrap. You can also call it as a + * getter (i.e. without any arguments) to get the current value of the + * `eagerInstantiationEnabled` flag. + * + * Instantiating `$route` early is necessary for capturing the initial + * {@link ng.$location#$locationChangeStart $locationChangeStart} event and navigating to the + * appropriate route. Usually, `$route` is instantiated in time by the + * {@link ngRoute.ngView ngView} directive. Yet, in cases where `ngView` is included in an + * asynchronously loaded template (e.g. in another directive's template), the directive factory + * might not be called soon enough for `$route` to be instantiated _before_ the initial + * `$locationChangeSuccess` event is fired. Eager instantiation ensures that `$route` is always + * instantiated in time, regardless of when `ngView` will be loaded. + * + * The default value is true. + * + * **Note**:
+ * You may want to disable the default behavior when unit-testing modules that depend on + * `ngRoute`, in order to avoid an unexpected request for the default route's template. + * + * @param {boolean=} enabled - If provided, update the internal `eagerInstantiationEnabled` flag. + * + * @returns {*} The current value of the `eagerInstantiationEnabled` flag if used as a getter or + * itself (for chaining) if used as a setter. + */ + isEagerInstantiationEnabled = true; + this.eagerInstantiationEnabled = function eagerInstantiationEnabled(enabled) { + if (isDefined(enabled)) { + isEagerInstantiationEnabled = enabled; + return this; + } + + return isEagerInstantiationEnabled; + }; + this.$get = ['$rootScope', '$location', @@ -254,7 +401,8 @@ function $RouteProvider() { '$injector', '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { + '$browser', + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce, $browser) { /** * @ngdoc service @@ -265,7 +413,7 @@ function $RouteProvider() { * @property {Object} current Reference to the current route definition. * The route definition contains: * - * - `controller`: The controller constructor as define in route definition. + * - `controller`: The controller constructor as defined in the route definition. * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for * controller instantiation. The `locals` contain * the resolved values of the `resolve` map. Additionally the `locals` also contain: @@ -273,6 +421,10 @@ function $RouteProvider() { * - `$scope` - The current route scope. * - `$template` - The current route template HTML. * + * The `locals` will be assigned to the route scope's `$resolve` property. You can override + * the property name, using `resolveAs` in the route definition. See + * {@link ngRoute.$routeProvider $routeProvider} for more info. + * * @property {Object} routes Object with all route configuration Objects as its properties. * * @description @@ -335,12 +487,12 @@ function $RouteProvider() { * }) * * .controller('BookController', function($scope, $routeParams) { - * $scope.name = "BookController"; + * $scope.name = 'BookController'; * $scope.params = $routeParams; * }) * * .controller('ChapterController', function($scope, $routeParams) { - * $scope.name = "ChapterController"; + * $scope.name = 'ChapterController'; * $scope.params = $routeParams; * }) * @@ -373,15 +525,15 @@ function $RouteProvider() { * it('should load and compile correct template', function() { * element(by.linkText('Moby: Ch1')).click(); * var content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: ChapterController/); - * expect(content).toMatch(/Book Id\: Moby/); - * expect(content).toMatch(/Chapter Id\: 1/); + * expect(content).toMatch(/controller: ChapterController/); + * expect(content).toMatch(/Book Id: Moby/); + * expect(content).toMatch(/Chapter Id: 1/); * * element(by.partialLinkText('Scarlet')).click(); * * content = element(by.css('[ng-view]')).getText(); - * expect(content).toMatch(/controller\: BookController/); - * expect(content).toMatch(/Book Id\: Scarlet/); + * expect(content).toMatch(/controller: BookController/); + * expect(content).toMatch(/Book Id: Scarlet/); * }); * * @@ -429,12 +581,14 @@ function $RouteProvider() { * @name $route#$routeChangeError * @eventType broadcast on root scope * @description - * Broadcasted if any of the resolve promises are rejected. + * Broadcasted if a redirection function fails or any redirection or resolve promises are + * rejected. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current route information. * @param {Route} previous Previous route information. - * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. + * @param {Route} rejection The thrown error or the rejection reason of the promise. Usually + * the rejection reason is the error that caused the promise to get rejected. */ /** @@ -442,8 +596,9 @@ function $RouteProvider() { * @name $route#$routeUpdate * @eventType broadcast on root scope * @description - * The `reloadOnSearch` property has been set to false, and we are reusing the same - * instance of the Controller. + * Broadcasted if the same instance of a route (including template, controller instance, + * resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or + * `reloadOnUrl` has been set to `false`. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current/previous route information. @@ -468,10 +623,18 @@ function $RouteProvider() { */ reload: function() { forceReload = true; + + var fakeLocationEvent = { + defaultPrevented: false, + preventDefault: function fakePreventDefault() { + this.defaultPrevented = true; + forceReload = false; + } + }; + $rootScope.$evalAsync(function() { - // Don't support cancellation of a reload for now... - prepareRoute(); - commitRoute(); + prepareRoute(fakeLocationEvent); + if (!fakeLocationEvent.defaultPrevented) commitRoute(); }); }, @@ -495,7 +658,7 @@ function $RouteProvider() { // interpolate modifies newParams, only query params are left $location.search(newParams); } else { - throw $routeMinErr('norout', 'Tried updating route when with no current route'); + throw $routeMinErr('norout', 'Tried updating route with no current route'); } } }; @@ -543,9 +706,7 @@ function $RouteProvider() { var lastRoute = $route.current; preparedRoute = parseRoute(); - preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route - && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) - && !preparedRoute.reloadOnSearch && !forceReload; + preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute); if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { @@ -567,65 +728,145 @@ function $RouteProvider() { } else if (nextRoute || lastRoute) { forceReload = false; $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(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) - .replace(); + + var nextRoutePromise = $q.resolve(nextRoute); + + $browser.$$incOutstandingRequestCount('$route'); + + nextRoutePromise. + then(getRedirectionData). + then(handlePossibleRedirection). + then(function(keepProcessingRoute) { + return keepProcessingRoute && nextRoutePromise. + then(resolveLocals). + then(function(locals) { + // after route change + if (nextRoute === $route.current) { + if (nextRoute) { + nextRoute.locals = locals; + angular.copy(nextRoute.params, $routeParams); + } + $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); + } + }); + }).catch(function(error) { + if (nextRoute === $route.current) { + $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); } - } - } + }).finally(function() { + // Because `commitRoute()` is called from a `$rootScope.$evalAsync` block (see + // `$locationWatch`), this `$$completeOutstandingRequest()` call will not cause + // `outstandingRequestCount` to hit zero. This is important in case we are redirecting + // to a new route which also requires some asynchronous work. - $q.when(nextRoute). - then(function() { - if (nextRoute) { - var locals = angular.extend({}, nextRoute.resolve), - template, templateUrl; + $browser.$$completeOutstandingRequest(noop, '$route'); + }); + } + } - angular.forEach(locals, function(value, key) { - locals[key] = angular.isString(value) ? - $injector.get(value) : $injector.invoke(value, null, null, key); - }); + function getRedirectionData(route) { + var data = { + route: route, + hasRedirection: false + }; + + if (route) { + if (route.redirectTo) { + if (angular.isString(route.redirectTo)) { + data.path = interpolate(route.redirectTo, route.params); + data.search = route.params; + data.hasRedirection = true; + } else { + var oldPath = $location.path(); + var oldSearch = $location.search(); + var newUrl = route.redirectTo(route.pathParams, oldPath, oldSearch); - if (angular.isDefined(template = nextRoute.template)) { - if (angular.isFunction(template)) { - template = template(nextRoute.params); - } - } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { - if (angular.isFunction(templateUrl)) { - templateUrl = templateUrl(nextRoute.params); - } - if (angular.isDefined(templateUrl)) { - nextRoute.loadedTemplateUrl = $sce.valueOf(templateUrl); - template = $templateRequest(templateUrl); - } - } - if (angular.isDefined(template)) { - locals['$template'] = template; - } - return $q.all(locals); + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; } - }). - then(function(locals) { - // after route change - if (nextRoute == $route.current) { - if (nextRoute) { - nextRoute.locals = locals; - angular.copy(nextRoute.params, $routeParams); + } + } else if (route.resolveRedirectTo) { + return $q. + resolve($injector.invoke(route.resolveRedirectTo)). + then(function(newUrl) { + if (angular.isDefined(newUrl)) { + data.url = newUrl; + data.hasRedirection = true; } - $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); - } - }, function(error) { - if (nextRoute == $route.current) { - $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); - } - }); + + return data; + }); + } } + + return data; } + function handlePossibleRedirection(data) { + var keepProcessingRoute = true; + + if (data.route !== $route.current) { + keepProcessingRoute = false; + } else if (data.hasRedirection) { + var oldUrl = $location.url(); + var newUrl = data.url; + + if (newUrl) { + $location. + url(newUrl). + replace(); + } else { + newUrl = $location. + path(data.path). + search(data.search). + replace(). + url(); + } + + if (newUrl !== oldUrl) { + // Exit out and don't process current next value, + // wait for next location change from redirect + keepProcessingRoute = false; + } + } + + return keepProcessingRoute; + } + + function resolveLocals(route) { + if (route) { + var locals = angular.extend({}, route.resolve); + angular.forEach(locals, function(value, key) { + locals[key] = angular.isString(value) ? + $injector.get(value) : + $injector.invoke(value, null, null, key); + }); + var template = getTemplateFor(route); + if (angular.isDefined(template)) { + locals['$template'] = template; + } + return $q.all(locals); + } + } + + function getTemplateFor(route) { + var template, templateUrl; + if (angular.isDefined(template = route.template)) { + if (angular.isFunction(template)) { + template = template(route.params); + } + } else if (angular.isDefined(templateUrl = route.templateUrl)) { + if (angular.isFunction(templateUrl)) { + templateUrl = templateUrl(route.params); + } + if (angular.isDefined(templateUrl)) { + route.loadedTemplateUrl = $sce.valueOf(templateUrl); + template = $templateRequest(templateUrl); + } + } + return template; + } /** * @returns {Object} the current active route, by matching it against the URL @@ -645,6 +886,29 @@ function $RouteProvider() { return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); } + /** + * @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`). + * @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`). + * @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same + * route and it can be reused (based on the config and the type of change). + */ + function isNavigationUpdateOnly(newRoute, oldRoute) { + // IF this is not a forced reload + return !forceReload + // AND both `newRoute`/`oldRoute` are defined + && newRoute && oldRoute + // AND they map to the same Route Definition Object + && (newRoute.$$route === oldRoute.$$route) + // AND `reloadOnUrl` is disabled + && (!newRoute.reloadOnUrl + // OR `reloadOnSearch` is disabled + || (!newRoute.reloadOnSearch + // AND both routes have the same path params + && angular.equals(newRoute.pathParams, oldRoute.pathParams) + ) + ); + } + /** * @returns {string} interpolation of the redirect path with the parameters */ @@ -666,6 +930,14 @@ function $RouteProvider() { }]; } +instantiateRoute.$inject = ['$injector']; +function instantiateRoute($injector) { + if (isEagerInstantiationEnabled) { + // Instantiate `$route` + $injector.get('$route'); + } +} + ngRouteModule.provider('$routeParams', $RouteParamsProvider); @@ -673,6 +945,7 @@ ngRouteModule.provider('$routeParams', $RouteParamsProvider); * @ngdoc service * @name $routeParams * @requires $route + * @this * * @description * The `$routeParams` service allows you to retrieve the current set of route parameters. @@ -716,7 +989,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * @restrict ECA * * @description - * # Overview * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by * including the rendered template of the current route into the main layout (`index.html`) file. * Every time the current route changes, the included view changes with it according to the @@ -725,8 +997,10 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); * Requires the {@link ngRoute `ngRoute`} module to be installed. * * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the new element is inserted to the DOM | + * | {@link ng.$animate#leave leave} | when the old element is removed from to the DOM | * * The enter and leave animation occur concurrently. * @@ -840,17 +1114,17 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); $locationProvider.html5Mode(true); }]) .controller('MainCtrl', ['$route', '$routeParams', '$location', - function($route, $routeParams, $location) { + function MainCtrl($route, $routeParams, $location) { this.$route = $route; this.$location = $location; this.$routeParams = $routeParams; }]) - .controller('BookCtrl', ['$routeParams', function($routeParams) { - this.name = "BookCtrl"; + .controller('BookCtrl', ['$routeParams', function BookCtrl($routeParams) { + this.name = 'BookCtrl'; this.params = $routeParams; }]) - .controller('ChapterCtrl', ['$routeParams', function($routeParams) { - this.name = "ChapterCtrl"; + .controller('ChapterCtrl', ['$routeParams', function ChapterCtrl($routeParams) { + this.name = 'ChapterCtrl'; this.params = $routeParams; }]); @@ -860,15 +1134,15 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory); it('should load and compile correct template', function() { element(by.linkText('Moby: Ch1')).click(); var content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: ChapterCtrl/); - expect(content).toMatch(/Book Id\: Moby/); - expect(content).toMatch(/Chapter Id\: 1/); + expect(content).toMatch(/controller: ChapterCtrl/); + expect(content).toMatch(/Book Id: Moby/); + expect(content).toMatch(/Chapter Id: 1/); element(by.partialLinkText('Scarlet')).click(); content = element(by.css('[ng-view]')).getText(); - expect(content).toMatch(/controller\: BookCtrl/); - expect(content).toMatch(/Book Id\: Scarlet/); + expect(content).toMatch(/controller: BookCtrl/); + expect(content).toMatch(/Book Id: Scarlet/); }); @@ -911,8 +1185,8 @@ function ngViewFactory($route, $anchorScroll, $animate) { } if (currentElement) { previousLeaveAnimation = $animate.leave(currentElement); - previousLeaveAnimation.then(function() { - previousLeaveAnimation = null; + previousLeaveAnimation.done(function(response) { + if (response !== false) previousLeaveAnimation = null; }); currentElement = null; } @@ -933,8 +1207,8 @@ 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).then(function onNgViewEnter() { - if (angular.isDefined(autoScrollExp) + $animate.enter(clone, null, currentElement || $element).done(function onNgViewEnter(response) { + if (response !== false && angular.isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { $anchorScroll(); } @@ -981,6 +1255,7 @@ function ngViewFillContentFactory($compile, $controller, $route) { $element.data('$ngControllerController', controller); $element.children().data('$ngControllerController', controller); } + scope[current.resolveAs || '$resolve'] = locals; link(scope); } diff --git a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-route.min.js b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-route.min.js index 3c5fd343d..ad7a18fb0 100644 --- a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-route.min.js +++ b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-route.min.js @@ -1,15 +1,17 @@ /* - AngularJS v1.4.8 - (c) 2010-2015 Google, Inc. http://angularjs.org + AngularJS v1.8.2 + (c) 2010-2020 Google LLC. http://angularjs.org License: MIT */ -(function(p,c,C){'use strict';function v(r,h,g){return{restrict:"ECA",terminal:!0,priority:400,transclude:"element",link:function(a,f,b,d,y){function z(){k&&(g.cancel(k),k=null);l&&(l.$destroy(),l=null);m&&(k=g.leave(m),k.then(function(){k=null}),m=null)}function x(){var b=r.current&&r.current.locals;if(c.isDefined(b&&b.$template)){var b=a.$new(),d=r.current;m=y(b,function(b){g.enter(b,null,m||f).then(function(){!c.isDefined(t)||t&&!a.$eval(t)||h()});z()});l=d.scope=b;l.$emit("$viewContentLoaded"); -l.$eval(w)}else z()}var l,m,k,t=b.autoscroll,w=b.onload||"";a.$on("$routeChangeSuccess",x);x()}}}function A(c,h,g){return{restrict:"ECA",priority:-400,link:function(a,f){var b=g.current,d=b.locals;f.html(d.$template);var y=c(f.contents());b.controller&&(d.$scope=a,d=h(b.controller,d),b.controllerAs&&(a[b.controllerAs]=d),f.data("$ngControllerController",d),f.children().data("$ngControllerController",d));y(a)}}}p=c.module("ngRoute",["ng"]).provider("$route",function(){function r(a,f){return c.extend(Object.create(a), -f)}function h(a,c){var b=c.caseInsensitiveMatch,d={originalPath:a,regexp:a},g=d.keys=[];a=a.replace(/([().])/g,"\\$1").replace(/(\/)?:(\w+)([\?\*])?/g,function(a,c,b,d){a="?"===d?d:null;d="*"===d?d:null;g.push({name:b,optional:!!a});c=c||"";return""+(a?"":c)+"(?:"+(a?c:"")+(d&&"(.+?)"||"([^/]+)")+(a||"")+")"+(a||"")}).replace(/([\/$\*])/g,"\\$1");d.regexp=new RegExp("^"+a+"$",b?"i":"");return d}var g={};this.when=function(a,f){var b=c.copy(f);c.isUndefined(b.reloadOnSearch)&&(b.reloadOnSearch=!0); -c.isUndefined(b.caseInsensitiveMatch)&&(b.caseInsensitiveMatch=this.caseInsensitiveMatch);g[a]=c.extend(b,a&&h(a,b));if(a){var d="/"==a[a.length-1]?a.substr(0,a.length-1):a+"/";g[d]=c.extend({redirectTo:a},h(d,b))}return this};this.caseInsensitiveMatch=!1;this.otherwise=function(a){"string"===typeof a&&(a={redirectTo:a});this.when(null,a);return this};this.$get=["$rootScope","$location","$routeParams","$q","$injector","$templateRequest","$sce",function(a,f,b,d,h,p,x){function l(b){var e=s.current; -(v=(n=k())&&e&&n.$$route===e.$$route&&c.equals(n.pathParams,e.pathParams)&&!n.reloadOnSearch&&!w)||!e&&!n||a.$broadcast("$routeChangeStart",n,e).defaultPrevented&&b&&b.preventDefault()}function m(){var u=s.current,e=n;if(v)u.params=e.params,c.copy(u.params,b),a.$broadcast("$routeUpdate",u);else if(e||u)w=!1,(s.current=e)&&e.redirectTo&&(c.isString(e.redirectTo)?f.path(t(e.redirectTo,e.params)).search(e.params).replace():f.url(e.redirectTo(e.pathParams,f.path(),f.search())).replace()),d.when(e).then(function(){if(e){var a= -c.extend({},e.resolve),b,f;c.forEach(a,function(b,e){a[e]=c.isString(b)?h.get(b):h.invoke(b,null,null,e)});c.isDefined(b=e.template)?c.isFunction(b)&&(b=b(e.params)):c.isDefined(f=e.templateUrl)&&(c.isFunction(f)&&(f=f(e.params)),c.isDefined(f)&&(e.loadedTemplateUrl=x.valueOf(f),b=p(f)));c.isDefined(b)&&(a.$template=b);return d.all(a)}}).then(function(f){e==s.current&&(e&&(e.locals=f,c.copy(e.params,b)),a.$broadcast("$routeChangeSuccess",e,u))},function(b){e==s.current&&a.$broadcast("$routeChangeError", -e,u,b)})}function k(){var a,b;c.forEach(g,function(d,g){var q;if(q=!b){var h=f.path();q=d.keys;var l={};if(d.regexp)if(h=d.regexp.exec(h)){for(var k=1,m=h.length;k=c;d--)f.end&&f.end(e[d]);e.length=c}}"string"!==typeof a&&(a=null===a||"undefined"===typeof a?"":""+a);var b,k,e=[],m=a,l;for(e.last=function(){return e[e.length-1]};a;){l="";k=!0;if(e.last()&&w[e.last()])a=a.replace(new RegExp("([\\W\\w]*)<\\s*\\/\\s*"+e.last()+"[^>]*>","i"),function(a,b){b=b.replace(H,"$1").replace(I,"$1");f.chars&&f.chars(q(b));return""}),c("",e.last());else{if(0===a.indexOf("\x3c!--"))b=a.indexOf("--",4),0<=b&&a.lastIndexOf("--\x3e", -b)===b&&(f.comment&&f.comment(a.substring(4,b)),a=a.substring(b+3),k=!1);else if(x.test(a)){if(b=a.match(x))a=a.replace(b[0],""),k=!1}else if(J.test(a)){if(b=a.match(y))a=a.substring(b[0].length),b[0].replace(y,c),k=!1}else K.test(a)&&((b=a.match(z))?(b[4]&&(a=a.substring(b[0].length),b[0].replace(z,d)),k=!1):(l+="<",a=a.substring(1)));k&&(b=a.indexOf("<"),l+=0>b?a:a.substring(0,b),a=0>b?"":a.substring(b),f.chars&&f.chars(q(l)))}if(a==m)throw L("badparse",a);m=a}c()}function q(a){if(!a)return"";A.innerHTML= -a.replace(//g,">")}function r(a,f){var d=!1,c=h.bind(a,a.push);return{start:function(a,k,e){a=h.lowercase(a);!d&&w[a]&&(d=a);d||!0!==C[a]||(c("<"),c(a),h.forEach(k,function(d,e){var k=h.lowercase(e),g="img"===a&&"src"===k|| -"background"===k;!0!==O[k]||!0===D[k]&&!f(d,g)||(c(" "),c(e),c('="'),c(B(d)),c('"'))}),c(e?"/>":">"))},end:function(a){a=h.lowercase(a);d||!0!==C[a]||(c(""));a==d&&(d=!1)},chars:function(a){d||c(B(a))}}}var L=h.$$minErr("$sanitize"),z=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,y=/^<\/\s*([\w:-]+)[^>]*>/,G=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,K=/^]*?)>/i, -I=/"\u201d\u2019]/i,d=/^mailto:/i;return function(c,b){function k(a){a&&g.push(E(a))}function e(a, -c){g.push("');k(c);g.push("")}if(!c)return c;for(var m,l=c,g=[],n,p;m=l.match(f);)n=m[0],m[2]||m[4]||(n=(m[3]?"http://":"mailto:")+n),p=m.index,k(l.substr(0,p)),e(n,m[0].replace(d,"")),l=l.substring(p+m[0].length);k(l);return a(g.join(""))}}])})(window,window.angular); -//# sourceMappingURL=angular-sanitize.min.js.map \ No newline at end of file +(function(s,e){'use strict';function O(e){var g=[];B(g,D).chars(e);return g.join("")}var C=e.$$minErr("$sanitize"),E,g,F,G,H,q,D,I,J,B;e.module("ngSanitize",[]).provider("$sanitize",function(){function h(a,d){return A(a.split(","),d)}function A(a,d){var c={},b;for(b=0;b/g,">")}function z(a){for(;a;){if(a.nodeType===s.Node.ELEMENT_NODE)for(var d=a.attributes,c=0,b=d.length;c"))},end:function(a){a=q(a);c||!0!==m[a]||!0===r[a]||(b(""));a==c&&(c=!1)},chars:function(a){c|| +b(K(a))}}};I=s.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)};var Q=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,u=/([^#-~ |!])/g,r=h("area,br,col,hr,img,wbr"),x=h("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),p=h("rp,rt"),n=g({},p,x),x=g({},x,h("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),p=g({},p,h("a,abbr,acronym,b,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")), +l=h("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),w=h("script,style"),m=g({},r,x,p,n),N=h("background,cite,href,longdesc,src,xlink:href,xml:base"),n=h("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,tabindex,target,title,type,valign,value,vspace,width"), +p=h("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,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", +!0),L=g({},N,p,n),M=function(a,d){function c(b){b=""+b;try{var c=(new a.DOMParser).parseFromString(b,"text/html").body;c.firstChild.remove();return c}catch(d){}}var b;try{b=!!c("")}catch(f){b=!1}if(b)return c;if(!d||!d.implementation)throw C("noinert");b=d.implementation.createHTMLDocument("inert");var e=(b.documentElement||b.getDocumentElement()).querySelector("body");return function(a){e.innerHTML=a;d.documentMode&&z(e);return e}}(s,s.document)}).info({angularVersion:"1.8.2"}); +e.module("ngSanitize").filter("linky",["$sanitize",function(h){var g=/((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i,t=/^mailto:/i,q=e.$$minErr("linky"),s=e.isDefined,z=e.isFunction,v=e.isObject,y=e.isString;return function(f,e,u){function r(e){e&&l.push(O(e))}function x(f,h){var g,a=p(f);l.push("');r(h);l.push("")}if(null== +f||""===f)return f;if(!y(f))throw q("notstring",f);for(var p=z(u)?u:v(u)?function(){return u}:function(){return{}},n=f,l=[],w,m;f=n.match(g);)w=f[0],f[2]||f[4]||(w=(f[3]?"http://":"mailto:")+w),m=f.index,r(n.substr(0,m)),x(w,f[0].replace(t,"")),n=n.substring(m+f[0].length);r(n);return h(l.join(""))}}])})(window,window.angular); +//# sourceMappingURL=angular-sanitize.min.js.map diff --git a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-sanitize.min.js.map b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-sanitize.min.js.map index 2f9652d55..031cbd6f8 100644 --- a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-sanitize.min.js.map +++ b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular-sanitize.min.js.map @@ -1,8 +1,8 @@ { "version":3, "file":"angular-sanitize.min.js", -"lineCount":15, -"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkBC,CAAlB,CAA6B,CA6JtCC,QAASA,EAAY,CAACC,CAAD,CAAQ,CAC3B,IAAIC,EAAM,EACGC,EAAAC,CAAmBF,CAAnBE,CAAwBN,CAAAO,KAAxBD,CACbH,MAAA,CAAaA,CAAb,CACA,OAAOC,EAAAI,KAAA,CAAS,EAAT,CAJoB,CAmG7BC,QAASA,EAAO,CAACC,CAAD,CAAMC,CAAN,CAAqB,CAAA,IAC/BC,EAAM,EADyB,CACrBC,EAAQH,CAAAI,MAAA,CAAU,GAAV,CADa,CACGC,CACtC,KAAKA,CAAL,CAAS,CAAT,CAAYA,CAAZ,CAAgBF,CAAAG,OAAhB,CAA8BD,CAAA,EAA9B,CACEH,CAAA,CAAID,CAAA,CAAgBX,CAAAiB,UAAA,CAAkBJ,CAAA,CAAME,CAAN,CAAlB,CAAhB,CAA8CF,CAAA,CAAME,CAAN,CAAlD,CAAA,CAA8D,CAAA,CAEhE,OAAOH,EAL4B,CAqBrCM,QAASA,EAAU,CAACC,CAAD,CAAOC,CAAP,CAAgB,CAiGjCC,QAASA,EAAa,CAACC,CAAD,CAAMC,CAAN,CAAeC,CAAf,CAAqBC,CAArB,CAA4B,CAChDF,CAAA,CAAUvB,CAAAiB,UAAA,CAAkBM,CAAlB,CACV,IAAIG,CAAA,CAAcH,CAAd,CAAJ,CACE,IAAA,CAAOI,CAAAC,KAAA,EAAP,EAAuBC,CAAA,CAAeF,CAAAC,KAAA,EAAf,CAAvB,CAAA,CACEE,CAAA,CAAY,EAAZ,CAAgBH,CAAAC,KAAA,EAAhB,CAIAG,EAAA,CAAuBR,CAAvB,CAAJ,EAAuCI,CAAAC,KAAA,EAAvC,EAAuDL,CAAvD,EACEO,CAAA,CAAY,EAAZ,CAAgBP,CAAhB,CAKF,EAFAE,CAEA,CAFQO,CAAA,CAAaT,CAAb,CAER,EAFiC,CAAEE,CAAAA,CAEnC,GACEE,CAAAM,KAAA,CAAWV,CAAX,CAGF,KAAIW,EAAQ,EAEZV,EAAAW,QAAA,CAAaC,CAAb,CACE,QAAQ,CAACC,CAAD,CAAQC,CAAR,CAAcC,CAAd,CAAiCC,CAAjC,CAAoDC,CAApD,CAAmE,CAMzEP,CAAA,CAAMI,CAAN,CAAA,CAAcI,CAAA,CALFH,CAKE,EAJTC,CAIS,EAHTC,CAGS,EAFT,EAES,CAN2D,CAD7E,CASIrB,EAAAuB,MAAJ,EAAmBvB,CAAAuB,MAAA,CAAcpB,CAAd,CAAuBW,CAAvB,CAA8BT,CAA9B,CA7B6B,CAgClDK,QAASA,EAAW,CAACR,CAAD,CAAMC,CAAN,CAAe,CAAA,IAC7BqB,EAAM,CADuB,CACpB7B,CAEb,IADAQ,CACA,CADUvB,CAAAiB,UAAA,CAAkBM,CAAlB,CACV,CAEE,IAAKqB,CAAL,CAAWjB,CAAAX,OAAX;AAA0B,CAA1B,CAAoC,CAApC,EAA6B4B,CAA7B,EACMjB,CAAA,CAAMiB,CAAN,CADN,EACoBrB,CADpB,CAAuCqB,CAAA,EAAvC,EAKF,GAAW,CAAX,EAAIA,CAAJ,CAAc,CAEZ,IAAK7B,CAAL,CAASY,CAAAX,OAAT,CAAwB,CAAxB,CAA2BD,CAA3B,EAAgC6B,CAAhC,CAAqC7B,CAAA,EAArC,CACMK,CAAAyB,IAAJ,EAAiBzB,CAAAyB,IAAA,CAAYlB,CAAA,CAAMZ,CAAN,CAAZ,CAGnBY,EAAAX,OAAA,CAAe4B,CANH,CAVmB,CAhIf,QAApB,GAAI,MAAOzB,EAAX,GAEIA,CAFJ,CACe,IAAb,GAAIA,CAAJ,EAAqC,WAArC,GAAqB,MAAOA,EAA5B,CACS,EADT,CAGS,EAHT,CAGcA,CAJhB,CADiC,KAQ7B2B,CAR6B,CAQtB3C,CARsB,CAQRwB,EAAQ,EARA,CAQIC,EAAOT,CARX,CAQiB4B,CAGlD,KAFApB,CAAAC,KAEA,CAFaoB,QAAQ,EAAG,CAAE,MAAOrB,EAAA,CAAMA,CAAAX,OAAN,CAAqB,CAArB,CAAT,CAExB,CAAOG,CAAP,CAAA,CAAa,CACX4B,CAAA,CAAO,EACP5C,EAAA,CAAQ,CAAA,CAGR,IAAKwB,CAAAC,KAAA,EAAL,EAAsBqB,CAAA,CAAgBtB,CAAAC,KAAA,EAAhB,CAAtB,CA2DET,CASA,CATOA,CAAAgB,QAAA,CAAa,IAAIe,MAAJ,CAAW,yBAAX,CAAuCvB,CAAAC,KAAA,EAAvC,CAAsD,QAAtD,CAAgE,GAAhE,CAAb,CACL,QAAQ,CAACuB,CAAD,CAAMJ,CAAN,CAAY,CAClBA,CAAA,CAAOA,CAAAZ,QAAA,CAAaiB,CAAb,CAA6B,IAA7B,CAAAjB,QAAA,CAA2CkB,CAA3C,CAAyD,IAAzD,CAEHjC,EAAAjB,MAAJ,EAAmBiB,CAAAjB,MAAA,CAAcuC,CAAA,CAAeK,CAAf,CAAd,CAEnB,OAAO,EALW,CADf,CASP,CAAAjB,CAAA,CAAY,EAAZ,CAAgBH,CAAAC,KAAA,EAAhB,CApEF,KAAqD,CAGnD,GAA6B,CAA7B,GAAIT,CAAAmC,QAAA,CAAa,SAAb,CAAJ,CAEER,CAEA,CAFQ3B,CAAAmC,QAAA,CAAa,IAAb,CAAmB,CAAnB,CAER,CAAa,CAAb,EAAIR,CAAJ,EAAkB3B,CAAAoC,YAAA,CAAiB,QAAjB;AAAwBT,CAAxB,CAAlB,GAAqDA,CAArD,GACM1B,CAAAoC,QAEJ,EAFqBpC,CAAAoC,QAAA,CAAgBrC,CAAAsC,UAAA,CAAe,CAAf,CAAkBX,CAAlB,CAAhB,CAErB,CADA3B,CACA,CADOA,CAAAsC,UAAA,CAAeX,CAAf,CAAuB,CAAvB,CACP,CAAA3C,CAAA,CAAQ,CAAA,CAHV,CAJF,KAUO,IAAIuD,CAAAC,KAAA,CAAoBxC,CAApB,CAAJ,CAGL,IAFAkB,CAEA,CAFQlB,CAAAkB,MAAA,CAAWqB,CAAX,CAER,CACEvC,CACA,CADOA,CAAAgB,QAAA,CAAaE,CAAA,CAAM,CAAN,CAAb,CAAuB,EAAvB,CACP,CAAAlC,CAAA,CAAQ,CAAA,CAFV,CAHK,IAQA,IAAIyD,CAAAD,KAAA,CAA4BxC,CAA5B,CAAJ,CAGL,IAFAkB,CAEA,CAFQlB,CAAAkB,MAAA,CAAWwB,CAAX,CAER,CACE1C,CAEA,CAFOA,CAAAsC,UAAA,CAAepB,CAAA,CAAM,CAAN,CAAArB,OAAf,CAEP,CADAqB,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAiB0B,CAAjB,CAAiC/B,CAAjC,CACA,CAAA3B,CAAA,CAAQ,CAAA,CAHV,CAHK,IAUI2D,EAAAH,KAAA,CAAsBxC,CAAtB,CAAJ,GAGL,CAFAkB,CAEA,CAFQlB,CAAAkB,MAAA,CAAW0B,CAAX,CAER,GAEM1B,CAAA,CAAM,CAAN,CAIJ,GAHElB,CACA,CADOA,CAAAsC,UAAA,CAAepB,CAAA,CAAM,CAAN,CAAArB,OAAf,CACP,CAAAqB,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAiB4B,CAAjB,CAAmC1C,CAAnC,CAEF,EAAAlB,CAAA,CAAQ,CAAA,CANV,GASE4C,CACA,EADQ,GACR,CAAA5B,CAAA,CAAOA,CAAAsC,UAAA,CAAe,CAAf,CAVT,CAHK,CAiBHtD,EAAJ,GACE2C,CAKA,CALQ3B,CAAAmC,QAAA,CAAa,GAAb,CAKR,CAHAP,CAGA,EAHgB,CAAR,CAAAD,CAAA,CAAY3B,CAAZ,CAAmBA,CAAAsC,UAAA,CAAe,CAAf,CAAkBX,CAAlB,CAG3B,CAFA3B,CAEA,CAFe,CAAR,CAAA2B,CAAA,CAAY,EAAZ,CAAiB3B,CAAAsC,UAAA,CAAeX,CAAf,CAExB,CAAI1B,CAAAjB,MAAJ,EAAmBiB,CAAAjB,MAAA,CAAcuC,CAAA,CAAeK,CAAf,CAAd,CANrB,CAhDmD,CAuErD,GAAI5B,CAAJ,EAAYS,CAAZ,CACE,KAAMoC,EAAA,CAAgB,UAAhB,CAC4C7C,CAD5C,CAAN,CAGFS,CAAA,CAAOT,CAhFI,CAoFbW,CAAA,EA/FiC,CA4JnCY,QAASA,EAAc,CAACuB,CAAD,CAAQ,CAC7B,GAAKA,CAAAA,CAAL,CAAc,MAAO,EAErBC,EAAAC,UAAA;AAAsBF,CAAA9B,QAAA,CAAc,IAAd,CAAmB,MAAnB,CAGtB,OAAO+B,EAAAE,YANsB,CAgB/BC,QAASA,EAAc,CAACJ,CAAD,CAAQ,CAC7B,MAAOA,EAAA9B,QAAA,CACG,IADH,CACS,OADT,CAAAA,QAAA,CAEGmC,CAFH,CAE0B,QAAQ,CAACL,CAAD,CAAQ,CAC7C,IAAIM,EAAKN,CAAAO,WAAA,CAAiB,CAAjB,CACLC,EAAAA,CAAMR,CAAAO,WAAA,CAAiB,CAAjB,CACV,OAAO,IAAP,EAAgC,IAAhC,EAAiBD,CAAjB,CAAsB,KAAtB,GAA0CE,CAA1C,CAAgD,KAAhD,EAA0D,KAA1D,EAAqE,GAHxB,CAF1C,CAAAtC,QAAA,CAOGuC,CAPH,CAO4B,QAAQ,CAACT,CAAD,CAAQ,CAC/C,MAAO,IAAP,CAAcA,CAAAO,WAAA,CAAiB,CAAjB,CAAd,CAAoC,GADW,CAP5C,CAAArC,QAAA,CAUG,IAVH,CAUS,MAVT,CAAAA,QAAA,CAWG,IAXH,CAWS,MAXT,CADsB,CAyB/B9B,QAASA,EAAkB,CAACD,CAAD,CAAMuE,CAAN,CAAoB,CAC7C,IAAIC,EAAS,CAAA,CAAb,CACIC,EAAM7E,CAAA8E,KAAA,CAAa1E,CAAb,CAAkBA,CAAA6B,KAAlB,CACV,OAAO,CACLU,MAAOA,QAAQ,CAACrB,CAAD,CAAMY,CAAN,CAAaT,CAAb,CAAoB,CACjCH,CAAA,CAAMtB,CAAAiB,UAAA,CAAkBK,CAAlB,CACDsD,EAAAA,CAAL,EAAe3B,CAAA,CAAgB3B,CAAhB,CAAf,GACEsD,CADF,CACWtD,CADX,CAGKsD,EAAL,EAAsC,CAAA,CAAtC,GAAeG,CAAA,CAAczD,CAAd,CAAf,GACEuD,CAAA,CAAI,GAAJ,CAcA,CAbAA,CAAA,CAAIvD,CAAJ,CAaA,CAZAtB,CAAAgF,QAAA,CAAgB9C,CAAhB,CAAuB,QAAQ,CAAC+B,CAAD,CAAQgB,CAAR,CAAa,CAC1C,IAAIC,EAAKlF,CAAAiB,UAAA,CAAkBgE,CAAlB,CAAT,CACIE,EAAmB,KAAnBA,GAAW7D,CAAX6D,EAAqC,KAArCA,GAA4BD,CAA5BC;AAAyD,YAAzDA,GAAgDD,CAC3B,EAAA,CAAzB,GAAIE,CAAA,CAAWF,CAAX,CAAJ,EACsB,CAAA,CADtB,GACGG,CAAA,CAASH,CAAT,CADH,EAC8B,CAAAP,CAAA,CAAaV,CAAb,CAAoBkB,CAApB,CAD9B,GAEEN,CAAA,CAAI,GAAJ,CAIA,CAHAA,CAAA,CAAII,CAAJ,CAGA,CAFAJ,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIR,CAAA,CAAeJ,CAAf,CAAJ,CACA,CAAAY,CAAA,CAAI,GAAJ,CANF,CAH0C,CAA5C,CAYA,CAAAA,CAAA,CAAIpD,CAAA,CAAQ,IAAR,CAAe,GAAnB,CAfF,CALiC,CAD9B,CAwBLoB,IAAKA,QAAQ,CAACvB,CAAD,CAAM,CACfA,CAAA,CAAMtB,CAAAiB,UAAA,CAAkBK,CAAlB,CACDsD,EAAL,EAAsC,CAAA,CAAtC,GAAeG,CAAA,CAAczD,CAAd,CAAf,GACEuD,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIvD,CAAJ,CACA,CAAAuD,CAAA,CAAI,GAAJ,CAHF,CAKIvD,EAAJ,EAAWsD,CAAX,GACEA,CADF,CACW,CAAA,CADX,CAPe,CAxBd,CAmCLzE,MAAOA,QAAQ,CAACA,CAAD,CAAQ,CACdyE,CAAL,EACEC,CAAA,CAAIR,CAAA,CAAelE,CAAf,CAAJ,CAFiB,CAnClB,CAHsC,CA7c/C,IAAI6D,EAAkBhE,CAAAsF,SAAA,CAAiB,WAAjB,CAAtB,CAyJIvB,EACG,wGA1JP,CA2JEF,EAAiB,wBA3JnB,CA4JEzB,EAAc,yEA5JhB,CA6JE0B,EAAmB,IA7JrB,CA8JEF,EAAyB,MA9J3B,CA+JER,EAAiB,qBA/JnB,CAgKEM,EAAiB,qBAhKnB;AAiKEL,EAAe,yBAjKjB,CAkKEiB,EAAwB,iCAlK1B,CAoKEI,EAA0B,gBApK5B,CA6KI1C,EAAevB,CAAA,CAAQ,wBAAR,CAIf8E,EAAAA,CAA8B9E,CAAA,CAAQ,gDAAR,CAC9B+E,EAAAA,CAA+B/E,CAAA,CAAQ,OAAR,CADnC,KAEIsB,EAAyB/B,CAAAyF,OAAA,CAAe,EAAf,CACeD,CADf,CAEeD,CAFf,CAF7B,CAOI7D,EAAgB1B,CAAAyF,OAAA,CAAe,EAAf,CAAmBF,CAAnB,CAAgD9E,CAAA,CAAQ,4KAAR,CAAhD,CAPpB,CAYIoB,EAAiB7B,CAAAyF,OAAA,CAAe,EAAf,CAAmBD,CAAnB,CAAiD/E,CAAA,CAAQ,2JAAR,CAAjD,CAQjBiF;CAAAA,CAAcjF,CAAA,CAAQ,4NAAR,CAKlB,KAAIwC,EAAkBxC,CAAA,CAAQ,cAAR,CAAtB,CAEIsE,EAAgB/E,CAAAyF,OAAA,CAAe,EAAf,CACezD,CADf,CAEeN,CAFf,CAGeG,CAHf,CAIeE,CAJf,CAKe2D,CALf,CAFpB,CAUIL,EAAW5E,CAAA,CAAQ,qDAAR,CAEXkF,EAAAA,CAAYlF,CAAA,CAAQ,kTAAR,CAQZmF;CAAAA,CAAWnF,CAAA,CAAQ,guCAAR;AAcoE,CAAA,CAdpE,CAgBf,KAAI2E,EAAapF,CAAAyF,OAAA,CAAe,EAAf,CACeJ,CADf,CAEeO,CAFf,CAGeD,CAHf,CAAjB,CAgLIzB,EAAU2B,QAAAC,cAAA,CAAuB,KAAvB,CA+Fd9F,EAAA+F,OAAA,CAAe,YAAf,CAA6B,EAA7B,CAAAC,SAAA,CAA0C,WAA1C,CAzXAC,QAA0B,EAAG,CAC3B,IAAAC,KAAA,CAAY,CAAC,eAAD,CAAkB,QAAQ,CAACC,CAAD,CAAgB,CACpD,MAAO,SAAQ,CAAChF,CAAD,CAAO,CACpB,IAAIf,EAAM,EACVc,EAAA,CAAWC,CAAX,CAAiBd,CAAA,CAAmBD,CAAnB,CAAwB,QAAQ,CAACgG,CAAD,CAAMjB,CAAN,CAAe,CAC9D,MAAO,CAAC,SAAAxB,KAAA,CAAewC,CAAA,CAAcC,CAAd,CAAmBjB,CAAnB,CAAf,CADsD,CAA/C,CAAjB,CAGA,OAAO/E,EAAAI,KAAA,CAAS,EAAT,CALa,CAD8B,CAA1C,CADe,CAyX7B,CAwGAR,EAAA+F,OAAA,CAAe,YAAf,CAAAM,OAAA,CAAoC,OAApC,CAA6C,CAAC,WAAD,CAAc,QAAQ,CAACC,CAAD,CAAY,CAAA,IACzEC,EACE,yFAFuE,CAGzEC,EAAgB,WAEpB,OAAO,SAAQ,CAACzD,CAAD,CAAO0D,CAAP,CAAe,CAsB5BC,QAASA,EAAO,CAAC3D,CAAD,CAAO,CAChBA,CAAL,EAGA5B,CAAAc,KAAA,CAAU/B,CAAA,CAAa6C,CAAb,CAAV,CAJqB,CAOvB4D,QAASA,EAAO,CAACC,CAAD;AAAM7D,CAAN,CAAY,CAC1B5B,CAAAc,KAAA,CAAU,KAAV,CACIjC,EAAA6G,UAAA,CAAkBJ,CAAlB,CAAJ,EACEtF,CAAAc,KAAA,CAAU,UAAV,CACUwE,CADV,CAEU,IAFV,CAIFtF,EAAAc,KAAA,CAAU,QAAV,CACU2E,CAAAzE,QAAA,CAAY,IAAZ,CAAkB,QAAlB,CADV,CAEU,IAFV,CAGAuE,EAAA,CAAQ3D,CAAR,CACA5B,EAAAc,KAAA,CAAU,MAAV,CAX0B,CA5B5B,GAAKc,CAAAA,CAAL,CAAW,MAAOA,EAMlB,KALA,IAAIV,CAAJ,CACIyE,EAAM/D,CADV,CAEI5B,EAAO,EAFX,CAGIyF,CAHJ,CAII7F,CACJ,CAAQsB,CAAR,CAAgByE,CAAAzE,MAAA,CAAUkE,CAAV,CAAhB,CAAA,CAEEK,CAQA,CARMvE,CAAA,CAAM,CAAN,CAQN,CANKA,CAAA,CAAM,CAAN,CAML,EANkBA,CAAA,CAAM,CAAN,CAMlB,GALEuE,CAKF,EALSvE,CAAA,CAAM,CAAN,CAAA,CAAW,SAAX,CAAuB,SAKhC,EAL6CuE,CAK7C,EAHA7F,CAGA,CAHIsB,CAAAS,MAGJ,CAFA4D,CAAA,CAAQI,CAAAC,OAAA,CAAW,CAAX,CAAchG,CAAd,CAAR,CAEA,CADA4F,CAAA,CAAQC,CAAR,CAAavE,CAAA,CAAM,CAAN,CAAAF,QAAA,CAAiBqE,CAAjB,CAAgC,EAAhC,CAAb,CACA,CAAAM,CAAA,CAAMA,CAAArD,UAAA,CAAc1C,CAAd,CAAkBsB,CAAA,CAAM,CAAN,CAAArB,OAAlB,CAER0F,EAAA,CAAQI,CAAR,CACA,OAAOR,EAAA,CAAUnF,CAAAX,KAAA,CAAU,EAAV,CAAV,CApBqB,CAL+C,CAAlC,CAA7C,CAlnBsC,CAArC,CAAD,CAqqBGT,MArqBH,CAqqBWA,MAAAC,QArqBX;", +"lineCount":16, +"mappings":"A;;;;;aAKC,SAAQ,CAACA,CAAD,CAASC,CAAT,CAAkB,CAmqB3BC,QAASA,EAAY,CAACC,CAAD,CAAQ,CAC3B,IAAIC,EAAM,EACGC,EAAAC,CAAmBF,CAAnBE,CAAwBC,CAAxBD,CACbH,MAAA,CAAaA,CAAb,CACA,OAAOC,EAAAI,KAAA,CAAS,EAAT,CAJoB,CAtpB7B,IAAIC,EAAkBR,CAAAS,SAAA,CAAiB,WAAjB,CAAtB,CACIC,CADJ,CAEIC,CAFJ,CAGIC,CAHJ,CAIIC,CAJJ,CAKIC,CALJ,CAMIC,CANJ,CAOIT,CAPJ,CAQIU,CARJ,CASIC,CATJ,CAUIb,CAqpBJJ,EAAAkB,OAAA,CAAe,YAAf,CAA6B,EAA7B,CAAAC,SAAA,CACY,WADZ,CA1hBAC,QAA0B,EAAG,CAkQ3BC,QAASA,EAAW,CAACC,CAAD,CAAMC,CAAN,CAAqB,CACvC,MAAOC,EAAA,CAAWF,CAAAG,MAAA,CAAU,GAAV,CAAX,CAA2BF,CAA3B,CADgC,CAIzCC,QAASA,EAAU,CAACE,CAAD,CAAQH,CAAR,CAAuB,CAAA,IACpCI,EAAM,EAD8B,CAC1BC,CACd,KAAKA,CAAL,CAAS,CAAT,CAAYA,CAAZ,CAAgBF,CAAAG,OAAhB,CAA8BD,CAAA,EAA9B,CACED,CAAA,CAAIJ,CAAA,CAAgBR,CAAA,CAAUW,CAAA,CAAME,CAAN,CAAV,CAAhB,CAAsCF,CAAA,CAAME,CAAN,CAA1C,CAAA,CAAsD,CAAA,CAExD,OAAOD,EALiC,CAQ1CG,QAASA,EAAa,CAACC,CAAD,CAAcC,CAAd,CAA2B,CAC3CA,CAAJ,EAAmBA,CAAAH,OAAnB,EACElB,CAAA,CAAOoB,CAAP,CAAoBP,CAAA,CAAWQ,CAAX,CAApB,CAF6C,CAgIjDC,QAASA,EAAS,CAACC,CAAD,CAAQ,CAExB,IADA,IAAIC,EAAM,EAAV,CACSP,EAAI,CADb,CACgBQ,EAAKF,CAAAL,OAArB,CAAmCD,CAAnC,CAAuCQ,CAAvC,CAA2CR,CAAA,EAA3C,CAAgD,CAC9C,IAAIS,EAAOH,CAAA,CAAMN,CAAN,CACXO,EAAA,CAAIE,CAAAC,KAAJ,CAAA,CAAiBD,CAAAE,MAF6B,CAIhD,MAAOJ,EANiB,CAiB1BK,QAASA,EAAc,CAACD,CAAD,CAAQ,CAC7B,MAAOA,EAAAE,QAAA,CACG,IADH,CACS,OADT,CAAAA,QAAA,CAEGC,CAFH,CAE0B,QAAQ,CAACH,CAAD,CAAQ,CAC7C,IAAII;AAAKJ,CAAAK,WAAA,CAAiB,CAAjB,CACLC,EAAAA,CAAMN,CAAAK,WAAA,CAAiB,CAAjB,CACV,OAAO,IAAP,EAAgC,IAAhC,EAAiBD,CAAjB,CAAsB,KAAtB,GAA0CE,CAA1C,CAAgD,KAAhD,EAA0D,KAA1D,EAAqE,GAHxB,CAF1C,CAAAJ,QAAA,CAOGK,CAPH,CAO4B,QAAQ,CAACP,CAAD,CAAQ,CAC/C,MAAO,IAAP,CAAcA,CAAAK,WAAA,CAAiB,CAAjB,CAAd,CAAoC,GADW,CAP5C,CAAAH,QAAA,CAUG,IAVH,CAUS,MAVT,CAAAA,QAAA,CAWG,IAXH,CAWS,MAXT,CADsB,CAgF/BM,QAASA,EAAkB,CAACC,CAAD,CAAO,CAChC,IAAA,CAAOA,CAAP,CAAA,CAAa,CACX,GAAIA,CAAAC,SAAJ,GAAsBlD,CAAAmD,KAAAC,aAAtB,CAEE,IADA,IAAIjB,EAAQc,CAAAI,WAAZ,CACSxB,EAAI,CADb,CACgByB,EAAInB,CAAAL,OAApB,CAAkCD,CAAlC,CAAsCyB,CAAtC,CAAyCzB,CAAA,EAAzC,CAA8C,CAC5C,IAAI0B,EAAWpB,CAAA,CAAMN,CAAN,CAAf,CACI2B,EAAWD,CAAAhB,KAAAkB,YAAA,EACf,IAAiB,WAAjB,GAAID,CAAJ,EAAoE,CAApE,GAAgCA,CAAAE,YAAA,CAAqB,MAArB,CAA6B,CAA7B,CAAhC,CACET,CAAAU,oBAAA,CAAyBJ,CAAzB,CAEA,CADA1B,CAAA,EACA,CAAAyB,CAAA,EAN0C,CAYhD,CADIM,CACJ,CADeX,CAAAY,WACf,GACEb,CAAA,CAAmBY,CAAnB,CAGFX,EAAA,CAAOa,CAAA,CAAiB,aAAjB,CAAgCb,CAAhC,CAnBI,CADmB,CAwBlCa,QAASA,EAAgB,CAACC,CAAD,CAAWd,CAAX,CAAiB,CAExC,IAAIW,EAAWX,CAAA,CAAKc,CAAL,CACf,IAAIH,CAAJ,EAAgB3C,CAAA+C,KAAA,CAAkBf,CAAlB,CAAwBW,CAAxB,CAAhB,CACE,KAAMnD,EAAA,CAAgB,QAAhB;AAA2FwC,CAAAgB,UAA3F,EAA6GhB,CAAAiB,UAA7G,CAAN,CAEF,MAAON,EANiC,CAtgB1C,IAAIO,EAAsB,CAAA,CAA1B,CACIC,EAAa,CAAA,CAEjB,KAAAC,KAAA,CAAY,CAAC,eAAD,CAAkB,QAAQ,CAACC,CAAD,CAAgB,CACpDH,CAAA,CAAsB,CAAA,CAClBC,EAAJ,EACExD,CAAA,CAAO2D,CAAP,CAAsBC,CAAtB,CAEF,OAAO,SAAQ,CAACC,CAAD,CAAO,CACpB,IAAIrE,EAAM,EACVc,EAAA,CAAWuD,CAAX,CAAiBpE,CAAA,CAAmBD,CAAnB,CAAwB,QAAQ,CAACsE,CAAD,CAAMC,CAAN,CAAe,CAC9D,MAAO,CAAC,UAAAC,KAAA,CAAgBN,CAAA,CAAcI,CAAd,CAAmBC,CAAnB,CAAhB,CADsD,CAA/C,CAAjB,CAGA,OAAOvE,EAAAI,KAAA,CAAS,EAAT,CALa,CAL8B,CAA1C,CA6CZ,KAAAqE,UAAA,CAAiBC,QAAQ,CAACD,CAAD,CAAY,CACnC,MAAI9D,EAAA,CAAU8D,CAAV,CAAJ,EACET,CACO,CADMS,CACN,CAAA,IAFT,EAIST,CAL0B,CAwDrC,KAAAW,iBAAA,CAAwBC,QAAQ,CAACC,CAAD,CAAW,CACpCd,CAAL,GACMrD,CAAA,CAAQmE,CAAR,CAOJ,GANEA,CAMF,CANa,CAACC,aAAcD,CAAf,CAMb,EAHAlD,CAAA,CAAcyC,CAAd,CAA2BS,CAAAT,YAA3B,CAGA,CAFAzC,CAAA,CAAcoD,CAAd,CAA4BF,CAAAG,iBAA5B,CAEA,CADArD,CAAA,CAAcwC,CAAd,CAA6BU,CAAAG,iBAA7B,CACA,CAAArD,CAAA,CAAcwC,CAAd,CAA6BU,CAAAC,aAA7B,CARF,CAWA,OAAO,KAZkC,CA6C3C,KAAAG,cAAA,CAAqBC,QAAQ,CAACnD,CAAD,CAAQ,CAC9BgC,CAAL,EACEvD,CAAA,CAAO2E,CAAP,CAAmB9D,CAAA,CAAWU,CAAX,CAAkB,CAAA,CAAlB,CAAnB,CAEF,OAAO,KAJ4B,CAWrCxB,EAAA,CAAOV,CAAAU,KACPC,EAAA,CAASX,CAAAW,OACTC;CAAA,CAAUZ,CAAAY,QACVC,EAAA,CAAUb,CAAAa,QACVC,EAAA,CAAYd,CAAAc,UACZC,EAAA,CAAYf,CAAAuF,YACZjF,EAAA,CAAON,CAAAM,KAEPW,EAAA,CA0KAuE,QAAuB,CAAChB,CAAD,CAAOiB,CAAP,CAAgB,CACxB,IAAb,GAAIjB,CAAJ,EAA8BkB,IAAAA,EAA9B,GAAqBlB,CAArB,CACEA,CADF,CACS,EADT,CAE2B,QAF3B,GAEW,MAAOA,EAFlB,GAGEA,CAHF,CAGS,EAHT,CAGcA,CAHd,CAMA,KAAImB,EAAmBC,CAAA,CAAoBpB,CAApB,CACvB,IAAKmB,CAAAA,CAAL,CAAuB,MAAO,EAG9B,KAAIE,EAAe,CACnB,GAAG,CACD,GAAqB,CAArB,GAAIA,CAAJ,CACE,KAAMrF,EAAA,CAAgB,QAAhB,CAAN,CAEFqF,CAAA,EAGArB,EAAA,CAAOmB,CAAAG,UACPH,EAAA,CAAmBC,CAAA,CAAoBpB,CAApB,CARlB,CAAH,MASSA,CATT,GASkBmB,CAAAG,UATlB,CAYA,KADI9C,CACJ,CADW2C,CAAA/B,WACX,CAAOZ,CAAP,CAAA,CAAa,CACX,OAAQA,CAAAC,SAAR,EACE,KAAK,CAAL,CACEwC,CAAAM,MAAA,CAAc/C,CAAAgD,SAAAxC,YAAA,EAAd,CAA2CvB,CAAA,CAAUe,CAAAI,WAAV,CAA3C,CACA,MACF,MAAK,CAAL,CACEqC,CAAAvF,MAAA,CAAc8C,CAAAiD,YAAd,CALJ,CASA,IAAItC,CACJ,IAAM,EAAAA,CAAA,CAAWX,CAAAY,WAAX,CAAN,GACwB,CAIjBD,GAJDX,CAAAC,SAICU,EAHH8B,CAAAS,IAAA,CAAYlD,CAAAgD,SAAAxC,YAAA,EAAZ,CAGGG,CADLA,CACKA,CADME,CAAA,CAAiB,aAAjB,CAAgCb,CAAhC,CACNW,CAAAA,CAAAA,CALP,EAMI,IAAA,CAAmB,IAAnB,EAAOA,CAAP,CAAA,CAAyB,CACvBX,CAAA;AAAOa,CAAA,CAAiB,YAAjB,CAA+Bb,CAA/B,CACP,IAAIA,CAAJ,GAAa2C,CAAb,CAA+B,KAC/BhC,EAAA,CAAWE,CAAA,CAAiB,aAAjB,CAAgCb,CAAhC,CACW,EAAtB,GAAIA,CAAAC,SAAJ,EACEwC,CAAAS,IAAA,CAAYlD,CAAAgD,SAAAxC,YAAA,EAAZ,CALqB,CAU7BR,CAAA,CAAOW,CA3BI,CA8Bb,IAAA,CAAQX,CAAR,CAAe2C,CAAA/B,WAAf,CAAA,CACE+B,CAAAQ,YAAA,CAA6BnD,CAA7B,CAvDmC,CAzKvC5C,EAAA,CA8QAgG,QAA+B,CAACjG,CAAD,CAAMkG,CAAN,CAAoB,CACjD,IAAIC,EAAuB,CAAA,CAA3B,CACIC,EAAM7F,CAAA,CAAKP,CAAL,CAAUA,CAAAqG,KAAV,CACV,OAAO,CACLT,MAAOA,QAAQ,CAACU,CAAD,CAAMvE,CAAN,CAAa,CAC1BuE,CAAA,CAAM1F,CAAA,CAAU0F,CAAV,CACDH,EAAAA,CAAL,EAA6BI,CAAA,CAAgBD,CAAhB,CAA7B,GACEH,CADF,CACyBG,CADzB,CAGKH,EAAL,EAAoD,CAAA,CAApD,GAA6BhC,CAAA,CAAcmC,CAAd,CAA7B,GACEF,CAAA,CAAI,GAAJ,CAcA,CAbAA,CAAA,CAAIE,CAAJ,CAaA,CAZA7F,CAAA,CAAQsB,CAAR,CAAe,QAAQ,CAACK,CAAD,CAAQoE,CAAR,CAAa,CAClC,IAAIC,EAAO7F,CAAA,CAAU4F,CAAV,CAAX,CACIjC,EAAmB,KAAnBA,GAAW+B,CAAX/B,EAAqC,KAArCA,GAA4BkC,CAA5BlC,EAAyD,YAAzDA,GAAgDkC,CAC3B,EAAA,CAAzB,GAAItB,CAAA,CAAWsB,CAAX,CAAJ,EACsB,CAAA,CADtB,GACGC,CAAA,CAASD,CAAT,CADH,EAC8B,CAAAP,CAAA,CAAa9D,CAAb,CAAoBmC,CAApB,CAD9B,GAEE6B,CAAA,CAAI,GAAJ,CAIA,CAHAA,CAAA,CAAII,CAAJ,CAGA,CAFAJ,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAI/D,CAAA,CAAeD,CAAf,CAAJ,CACA,CAAAgE,CAAA,CAAI,GAAJ,CANF,CAHkC,CAApC,CAYA,CAAAA,CAAA,CAAI,GAAJ,CAfF,CAL0B,CADvB,CAwBLL,IAAKA,QAAQ,CAACO,CAAD,CAAM,CACjBA,CAAA,CAAM1F,CAAA,CAAU0F,CAAV,CACDH,EAAL,EAAoD,CAAA,CAApD,GAA6BhC,CAAA,CAAcmC,CAAd,CAA7B,EAAkF,CAAA,CAAlF,GAA4DvB,CAAA,CAAauB,CAAb,CAA5D,GACEF,CAAA,CAAI,IAAJ,CAEA,CADAA,CAAA,CAAIE,CAAJ,CACA,CAAAF,CAAA,CAAI,GAAJ,CAHF,CAMIE,EAAJ,EAAWH,CAAX,GACEA,CADF,CACyB,CAAA,CADzB,CARiB,CAxBd,CAoCLpG,MAAOA,QAAQ,CAACA,CAAD,CAAQ,CAChBoG,CAAL;AACEC,CAAA,CAAI/D,CAAA,CAAetC,CAAf,CAAJ,CAFmB,CApClB,CAH0C,CA5QnDc,EAAA,CAAejB,CAAAmD,KAAA4D,UAAAC,SAAf,EAA8D,QAAQ,CAACC,CAAD,CAAM,CAE1E,MAAO,CAAG,EAAA,IAAAC,wBAAA,CAA6BD,CAA7B,CAAA,CAAoC,EAApC,CAFgE,CA5KjD,KAkLvBtE,EAAwB,iCAlLD,CAoLzBI,EAA0B,cApLD,CA6LvBoC,EAAe7D,CAAA,CAAY,wBAAZ,CA7LQ,CAiMvB6F,EAA8B7F,CAAA,CAAY,gDAAZ,CAjMP,CAkMvB8F,EAA+B9F,CAAA,CAAY,OAAZ,CAlMR,CAmMvB+F,EAAyBzG,CAAA,CAAO,EAAP,CACewG,CADf,CAEeD,CAFf,CAnMF,CAwMvBG,EAAgB1G,CAAA,CAAO,EAAP,CAAWuG,CAAX,CAAwC7F,CAAA,CAAY,qKAAZ,CAAxC,CAxMO,CA6MvBiG,EAAiB3G,CAAA,CAAO,EAAP,CAAWwG,CAAX,CAAyC9F,CAAA,CAAY,2JAAZ,CAAzC,CA7MM;AAqNvBkD,EAAclD,CAAA,CAAY,wNAAZ,CArNS,CA0NvBqF,EAAkBrF,CAAA,CAAY,cAAZ,CA1NK,CA4NvBiD,EAAgB3D,CAAA,CAAO,EAAP,CACeuE,CADf,CAEemC,CAFf,CAGeC,CAHf,CAIeF,CAJf,CA5NO,CAmOvBP,EAAWxF,CAAA,CAAY,uDAAZ,CAnOY,CAqOvBkG,EAAYlG,CAAA,CAAY,kTAAZ,CArOW;AA6OvBmG,EAAWnG,CAAA,CAAY,guCAAZ;AAcoE,CAAA,CAdpE,CA7OY,CA6PvBiE,EAAa3E,CAAA,CAAO,EAAP,CACekG,CADf,CAEeW,CAFf,CAGeD,CAHf,CA7PU,CAyRvB3B,EAAqE,QAAQ,CAAC7F,CAAD,CAAS0H,CAAT,CAAmB,CAoBlGC,QAASA,EAA6B,CAAClD,CAAD,CAAO,CAG3CA,CAAA,CAAO,mBAAP,CAA6BA,CAC7B,IAAI,CACF,IAAImD,EAAOC,CAAA,IAAI7H,CAAA8H,UAAJD,iBAAA,CAAuCpD,CAAvC,CAA6C,WAA7C,CAAAmD,KACXA,EAAA/D,WAAAkE,OAAA,EACA,OAAOH,EAHL,CAIF,MAAOI,CAAP,CAAU,EAR+B,CAnBzC,IAAA,CAYF,IAAI,CACF,CAAA,CAAO,CAAE,CAAAL,CAAA,CAA8B,EAA9B,CADP,CAEF,MAAOK,CAAP,CAAU,CACV,CAAA,CAAO,CAAA,CADG,CAdd,GAAI,CAAJ,CACE,MAAOL,EAGT,IAAKD,CAAAA,CAAL,EAAkBO,CAAAP,CAAAO,eAAlB,CACE,KAAMxH,EAAA,CAAgB,SAAhB,CAAN,CAEEyH,CAAAA,CAAgBR,CAAAO,eAAAE,mBAAA,CAA2C,OAA3C,CACpB,KAAIvC,EAAmBwC,CAACF,CAAAG,gBAADD,EAAkCF,CAAAI,mBAAA,EAAlCF,eAAA,CAAoF,MAApF,CACvB,OAuBAG,SAA0C,CAAC9D,CAAD,CAAO,CAC/CmB,CAAAG,UAAA,CAA6BtB,CAIzBiD,EAAAc,aAAJ,EACExF,CAAA,CAAmB4C,CAAnB,CAGF,OAAOA,EATwC,CAjCiD,CAA5B,CA4CrE5F,CA5CqE,CA4C7DA,CAAA0H,SA5C6D,CAzR7C,CA0hB7B,CAAAe,KAAA,CAEQ,CAAEC,eAAgB,OAAlB,CAFR,CAmIAzI;CAAAkB,OAAA,CAAe,YAAf,CAAAwH,OAAA,CAAoC,OAApC,CAA6C,CAAC,WAAD,CAAc,QAAQ,CAACC,CAAD,CAAY,CAAA,IACzEC,EACE,2FAFuE,CAGzEC,EAAgB,WAHyD,CAKzEC,EAAc9I,CAAAS,SAAA,CAAiB,OAAjB,CAL2D,CAMzEK,EAAYd,CAAAc,UAN6D,CAOzEiI,EAAa/I,CAAA+I,WAP4D,CAQzEC,EAAWhJ,CAAAgJ,SAR8D,CASzEC,EAAWjJ,CAAAiJ,SAEf,OAAO,SAAQ,CAACC,CAAD,CAAOC,CAAP,CAAe/F,CAAf,CAA2B,CA6BxCgG,QAASA,EAAO,CAACF,CAAD,CAAO,CAChBA,CAAL,EAGA1E,CAAAgC,KAAA,CAAUvG,CAAA,CAAaiJ,CAAb,CAAV,CAJqB,CAOvBG,QAASA,EAAO,CAACC,CAAD,CAAMJ,CAAN,CAAY,CAAA,IACtBvC,CADsB,CACjB4C,EAAiBC,CAAA,CAAaF,CAAb,CAC1B9E,EAAAgC,KAAA,CAAU,KAAV,CAEA,KAAKG,CAAL,GAAY4C,EAAZ,CACE/E,CAAAgC,KAAA,CAAUG,CAAV,CAAgB,IAAhB,CAAuB4C,CAAA,CAAe5C,CAAf,CAAvB,CAA6C,IAA7C,CAGE,EAAA7F,CAAA,CAAUqI,CAAV,CAAJ,EAA2B,QAA3B,EAAuCI,EAAvC,EACE/E,CAAAgC,KAAA,CAAU,UAAV,CACU2C,CADV,CAEU,IAFV,CAIF3E,EAAAgC,KAAA,CAAU,QAAV,CACU8C,CAAA7G,QAAA,CAAY,IAAZ,CAAkB,QAAlB,CADV,CAEU,IAFV,CAGA2G,EAAA,CAAQF,CAAR,CACA1E,EAAAgC,KAAA,CAAU,MAAV,CAjB0B,CAnC5B,GAAY,IAAZ;AAAI0C,CAAJ,EAA6B,EAA7B,GAAoBA,CAApB,CAAiC,MAAOA,EACxC,IAAK,CAAAD,CAAA,CAASC,CAAT,CAAL,CAAqB,KAAMJ,EAAA,CAAY,WAAZ,CAA8DI,CAA9D,CAAN,CAYrB,IAVA,IAAIM,EACFT,CAAA,CAAW3F,CAAX,CAAA,CAAyBA,CAAzB,CACA4F,CAAA,CAAS5F,CAAT,CAAA,CAAuBqG,QAA4B,EAAG,CAAC,MAAOrG,EAAR,CAAtD,CACAsG,QAAiC,EAAG,CAAC,MAAO,EAAR,CAHtC,CAMIC,EAAMT,CANV,CAOI1E,EAAO,EAPX,CAQI8E,CARJ,CASI1H,CACJ,CAAQgI,CAAR,CAAgBD,CAAAC,MAAA,CAAUhB,CAAV,CAAhB,CAAA,CAEEU,CAQA,CARMM,CAAA,CAAM,CAAN,CAQN,CANKA,CAAA,CAAM,CAAN,CAML,EANkBA,CAAA,CAAM,CAAN,CAMlB,GALEN,CAKF,EALSM,CAAA,CAAM,CAAN,CAAA,CAAW,SAAX,CAAuB,SAKhC,EAL6CN,CAK7C,EAHA1H,CAGA,CAHIgI,CAAAC,MAGJ,CAFAT,CAAA,CAAQO,CAAAG,OAAA,CAAW,CAAX,CAAclI,CAAd,CAAR,CAEA,CADAyH,CAAA,CAAQC,CAAR,CAAaM,CAAA,CAAM,CAAN,CAAAnH,QAAA,CAAiBoG,CAAjB,CAAgC,EAAhC,CAAb,CACA,CAAAc,CAAA,CAAMA,CAAAI,UAAA,CAAcnI,CAAd,CAAkBgI,CAAA,CAAM,CAAN,CAAA/H,OAAlB,CAERuH,EAAA,CAAQO,CAAR,CACA,OAAOhB,EAAA,CAAUnE,CAAAjE,KAAA,CAAU,EAAV,CAAV,CA3BiC,CAXmC,CAAlC,CAA7C,CA/yB2B,CAA1B,CAAD,CAq3BGR,MAr3BH,CAq3BWA,MAAAC,QAr3BX;", "sources":["angular-sanitize.js"], -"names":["window","angular","undefined","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","makeMap","str","lowercaseKeys","obj","items","split","i","length","lowercase","htmlParser","html","handler","parseStartTag","tag","tagName","rest","unary","blockElements","stack","last","inlineElements","parseEndTag","optionalEndTagElements","voidElements","push","attrs","replace","ATTR_REGEXP","match","name","doubleQuotedValue","singleQuotedValue","unquotedValue","decodeEntities","start","pos","end","index","text","stack.last","specialElements","RegExp","all","COMMENT_REGEXP","CDATA_REGEXP","indexOf","lastIndexOf","comment","substring","DOCTYPE_REGEXP","test","BEGING_END_TAGE_REGEXP","END_TAG_REGEXP","BEGIN_TAG_REGEXP","START_TAG_REGEXP","$sanitizeMinErr","value","hiddenPre","innerHTML","textContent","encodeEntities","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","uriValidator","ignore","out","bind","validElements","forEach","key","lkey","isImage","validAttrs","uriAttrs","$$minErr","optionalEndTagBlockElements","optionalEndTagInlineElements","extend","svgElements","htmlAttrs","svgAttrs","document","createElement","module","provider","$SanitizeProvider","$get","$$sanitizeUri","uri","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","target","addText","addLink","url","isDefined","raw","substr"] -} \ No newline at end of file +"names":["window","angular","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","$sanitizeMinErr","$$minErr","bind","extend","forEach","isArray","isDefined","lowercase","nodeContains","htmlParser","module","provider","$SanitizeProvider","stringToMap","str","lowercaseKeys","arrayToMap","split","items","obj","i","length","addElementsTo","elementsMap","newElements","attrToMap","attrs","map","ii","attr","name","value","encodeEntities","replace","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","stripCustomNsAttrs","node","nodeType","Node","ELEMENT_NODE","attributes","l","attrNode","attrName","toLowerCase","lastIndexOf","removeAttributeNode","nextNode","firstChild","getNonDescendant","propName","call","outerHTML","outerText","hasBeenInstantiated","svgEnabled","$get","$$sanitizeUri","validElements","svgElements","html","uri","isImage","test","enableSvg","this.enableSvg","addValidElements","this.addValidElements","elements","htmlElements","voidElements","htmlVoidElements","addValidAttrs","this.addValidAttrs","validAttrs","$$lowercase","htmlParserImpl","handler","undefined","inertBodyElement","getInertBodyElement","mXSSAttempts","innerHTML","start","nodeName","textContent","end","removeChild","htmlSanitizeWriterImpl","uriValidator","ignoreCurrentElement","out","push","tag","blockedElements","key","lkey","uriAttrs","prototype","contains","arg","compareDocumentPosition","optionalEndTagBlockElements","optionalEndTagInlineElements","optionalEndTagElements","blockElements","inlineElements","htmlAttrs","svgAttrs","document","getInertBodyElement_DOMParser","body","parseFromString","DOMParser","remove","e","implementation","inertDocument","createHTMLDocument","querySelector","documentElement","getDocumentElement","getInertBodyElement_InertDocument","documentMode","info","angularVersion","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","linkyMinErr","isFunction","isObject","isString","text","target","addText","addLink","url","linkAttributes","attributesFn","getAttributesObject","getEmptyAttributesObject","raw","match","index","substr","substring"] +} diff --git a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular.js b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular.js index a3aee7d6a..e67a6906e 100644 --- a/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular.js +++ b/Telerik.Sitefinity.Frontend/Mvc/Scripts/Angular/angular.js @@ -1,15 +1,76 @@ /** - * @license AngularJS v1.4.8 - * (c) 2010-2015 Google, Inc. http://angularjs.org + * @license AngularJS v1.8.2 + * (c) 2010-2020 Google LLC. http://angularjs.org * License: MIT */ -(function(window, document, undefined) {'use strict'; +(function(window) {'use strict'; + +/* exported + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth +*/ + +var minErrConfig = { + objectMaxDepth: 5, + urlErrorParamsEnabled: true +}; + +/** + * @ngdoc function + * @name angular.errorHandlingConfig + * @module ng + * @kind function + * + * @description + * Configure several aspects of error handling in AngularJS if used as a setter or return the + * current configuration if used as a getter. The following options are supported: + * + * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages. + * + * Omitted or undefined options will leave the corresponding configuration values unchanged. + * + * @param {Object=} config - The configuration object. May only contain the options that need to be + * updated. Supported keys: + * + * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a + * non-positive or non-numeric value, removes the max depth limit. + * Default: 5 + * + * * `urlErrorParamsEnabled` **{Boolean}** - Specifies whether the generated error url will + * contain the parameters of the thrown error. Disabling the parameters can be useful if the + * generated error url is very long. + * + * Default: true. When used without argument, it returns the current value. + */ +function errorHandlingConfig(config) { + if (isObject(config)) { + if (isDefined(config.objectMaxDepth)) { + minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; + } + if (isDefined(config.urlErrorParamsEnabled) && isBoolean(config.urlErrorParamsEnabled)) { + minErrConfig.urlErrorParamsEnabled = config.urlErrorParamsEnabled; + } + } else { + return minErrConfig; + } +} + +/** + * @private + * @param {Number} maxDepth + * @return {boolean} + */ +function isValidObjectMaxDepth(maxDepth) { + return isNumber(maxDepth) && maxDepth > 0; +} + /** * @description * * This object provides a utility for producing rich Error messages within - * Angular. It can be called as follows: + * AngularJS. It can be called as follows: * * var exampleMinErr = minErr('example'); * throw exampleMinErr('one', 'This {0} is {1}', foo, bar); @@ -26,7 +87,7 @@ * Since data will be parsed statically during a build step, some restrictions * are applied with respect to how minErr instances are created and called. * Instances should have names of the form namespaceMinErr for a minErr created - * using minErr('namespace') . Error codes, namespaces and template strings + * using minErr('namespace'). Error codes, namespaces and template strings * should all be static strings, not variables or general expressions. * * @param {string} module The namespace to use for the new minErr instance. @@ -37,132 +98,148 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; - return function() { - var SKIP_INDEXES = 2; - var templateArgs = arguments, - code = templateArgs[0], + var url = 'https://errors.angularjs.org/1.8.2/'; + var regex = url.replace('.', '\\.') + '[\\s\\S]*'; + var errRegExp = new RegExp(regex, 'g'); + + return function() { + var code = arguments[0], + template = arguments[1], message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], + templateArgs = sliceArgs(arguments, 2).map(function(arg) { + return toDebugString(arg, minErrConfig.objectMaxDepth); + }), paramPrefix, i; + // A minErr message has two parts: the message itself and the url that contains the + // encoded message. + // The message's parameters can contain other error messages which also include error urls. + // To prevent the messages from getting too long, we strip the error urls from the parameters. + message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; + var index = +match.slice(1, -1); - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); + if (index < templateArgs.length) { + return templateArgs[index].replace(errRegExp, ''); } return match; }); - message += '\nhttp://errors.angularjs.org/1.4.8/' + - (module ? module + '/' : '') + code; + message += '\n' + url + (module ? module + '/' : '') + code; - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); + if (minErrConfig.urlErrorParamsEnabled) { + for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]); + } } return new ErrorConstructor(message); }; } -/* We need to tell jshint what variables are being exported */ -/* 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, - forEachSorted: true, - reverseParams: true, - nextUid: true, - setHashKey: true, - extend: true, - toInt: true, - inherit: true, - merge: true, - noop: true, - identity: true, - valueFn: true, - isUndefined: true, - isDefined: true, - isObject: true, - isBlankObject: true, - isString: true, - isNumber: true, - isDate: true, - 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, - jq: true, - concat: true, - sliceArgs: true, - bind: true, - toJsonReplacer: true, - toJson: true, - fromJson: true, - convertTimezoneToLocal: true, - timezoneToOffset: 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_ATTRIBUTE: true, - NODE_TYPE_TEXT: true, - NODE_TYPE_COMMENT: true, - NODE_TYPE_DOCUMENT: true, - NODE_TYPE_DOCUMENT_FRAGMENT: true, +/* We need to tell ESLint what variables are being exported */ +/* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth, + ngMinErr, + angularModule, + uid, + REGEX_STRING_REGEXP, + VALIDITY_STATE_PROPERTY, + + lowercase, + uppercase, + nodeName_, + isArrayLike, + forEach, + forEachSorted, + reverseParams, + nextUid, + setHashKey, + extend, + toInt, + inherit, + merge, + noop, + identity, + valueFn, + isUndefined, + isDefined, + isObject, + isBlankObject, + isString, + isNumber, + isNumberNaN, + isDate, + isError, + isArray, + isFunction, + isRegExp, + isWindow, + isScope, + isFile, + isFormData, + isBlob, + isBoolean, + isPromiseLike, + trim, + escapeForRegexp, + isElement, + makeMap, + includes, + arrayRemove, + copy, + simpleCompare, + equals, + csp, + jq, + concat, + sliceArgs, + bind, + toJsonReplacer, + toJson, + fromJson, + convertTimezoneToLocal, + timezoneToOffset, + addDateMinutes, + startingTag, + tryDecodeURIComponent, + parseKeyValue, + toKeyValue, + encodeUriSegment, + encodeUriQuery, + angularInit, + bootstrap, + getTestability, + snake_case, + bindJQuery, + assertArg, + assertArgFn, + assertNotHasOwnProperty, + getter, + getBlockNodes, + hasOwnProperty, + createMap, + stringify, + UNSAFE_restoreLegacyJqLiteXHTMLReplacement, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT */ //////////////////////////////////// @@ -171,15 +248,14 @@ function minErr(module, ErrorConstructor) { * @ngdoc module * @name ng * @module ng + * @installation * @description * - * # ng (core module) * The ng module is loaded by default when an AngularJS application is started. The module itself * contains the essential components for an AngularJS application to function. The table below * lists a high level breakdown of each of the services/factories, filters, directives and testing * components available within this core module. * - *
*/ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; @@ -188,24 +264,20 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + /** - * @ngdoc function - * @name angular.lowercase - * @module ng - * @kind function + * @private * * @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 hasOwnProperty = Object.prototype.hasOwnProperty; /** - * @ngdoc function - * @name angular.uppercase - * @module ng - * @kind function + * @private * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. @@ -214,29 +286,6 @@ var hasOwnProperty = Object.prototype.hasOwnProperty; var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;}; -var manualLowercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) - : s; -}; -var manualUppercase = function(s) { - /* jshint bitwise: false */ - return isString(s) - ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) - : s; -}; - - -// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish -// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods -// with correct but slower alternatives. -if ('i' !== 'I'.toLowerCase()) { - lowercase = manualLowercase; - uppercase = manualUppercase; -} - - 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. @@ -253,11 +302,12 @@ var angularModule, uid = 0; +// Support: IE 9-11 only /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = document.documentMode; +msie = window.document.documentMode; /** @@ -273,18 +323,18 @@ function isArrayLike(obj) { // arrays, strings and jQuery/jqLite objects are array like // * jqLite is either the jQuery or jqLite constructor function - // * we have to check the existance of jqLite first as this method is called + // * we have to check the existence of jqLite first as this method is called // via the forEach method when constructing the jqLite object in the first place if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; // Support: iOS 8.2 (not reproducible in simulator) // "length" in obj used to prevent JIT error (gh-11508) - var length = "length" in Object(obj) && obj.length; + var length = 'length' in Object(obj) && obj.length; // NodeList objects (with `item` method) and // other objects with suitable length characteristics are array-like - return isNumber(length) && - (length >= 0 && (length - 1) in obj || typeof obj.item == 'function'); + return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function'); + } /** @@ -304,7 +354,7 @@ function isArrayLike(obj) { * * Unlike ES262's * [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18), - * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just + * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just * return the value provided. * ```js @@ -327,9 +377,7 @@ function forEach(obj, iterator, context) { if (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))) { + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } @@ -381,7 +429,7 @@ function forEachSorted(obj, iterator, context) { * @returns {function(*, string)} */ function reverseParams(iteratorFn) { - return function(value, key) { iteratorFn(key, value); }; + return function(value, key) {iteratorFn(key, value);}; } /** @@ -434,8 +482,10 @@ function baseExtend(dst, objs, deep) { } else if (isElement(src)) { dst[key] = src.clone(); } else { - if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; - baseExtend(dst[key], [src], true); + if (key !== '__proto__') { + if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {}; + baseExtend(dst[key], [src], true); + } } } else { dst[key] = src; @@ -484,6 +534,22 @@ function extend(dst) { * Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source * objects, performing a deep copy. * +* @deprecated +* sinceVersion="1.6.5" +* This function is deprecated, but will not be removed in the 1.x lifecycle. +* There are edge cases (see {@link angular.merge#known-issues known issues}) that are not +* supported by this function. We suggest using another, similar library for all-purpose merging, +* such as [lodash's merge()](https://lodash.com/docs/4.17.4#merge). +* +* @knownIssue +* This is a list of (known) object types that are not handled correctly by this function: +* - [`Blob`](https://developer.mozilla.org/docs/Web/API/Blob) +* - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream) +* - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient) +* - AngularJS {@link $rootScope.Scope scopes}; +* +* `angular.merge` also does not support merging objects with circular references. +* * @param {Object} dst Destination object. * @param {...Object} src Source object(s). * @returns {Object} Reference to `dst`. @@ -498,6 +564,11 @@ function toInt(str) { return parseInt(str, 10); } +var isNumberNaN = Number.isNaN || function isNumberNaN(num) { + // eslint-disable-next-line no-self-compare + return num !== num; +}; + function inherit(parent, extra) { return extend(Object.create(parent), extra); @@ -534,18 +605,28 @@ noop.$inject = []; * functional style. * ```js - function transformer(transformationFn, value) { - return (transformationFn || angular.identity)(value); - }; + function transformer(transformationFn, value) { + return (transformationFn || angular.identity)(value); + }; + + // E.g. + function getResult(fn, input) { + return (fn || angular.identity)(input); + }; + + getResult(function(n) { return n * 2; }, 21); // returns 42 + getResult(null, 21); // returns 21 + getResult(undefined, 21); // returns 21 ``` - * @param {*} value to be returned. - * @returns {*} the value passed in. + * + * @param {*} value to be returned. + * @returns {*} the value passed in. */ function identity($) {return $;} identity.$inject = []; -function valueFn(value) {return function() {return value;};} +function valueFn(value) {return function valueRef() {return value;};} function hasCustomToString(obj) { return isFunction(obj.toString) && obj.toString !== toString; @@ -676,7 +757,27 @@ function isDate(value) { * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. */ -var isArray = Array.isArray; +function isArray(arr) { + return Array.isArray(arr) || arr instanceof Array; +} + +/** + * @description + * Determines if a reference is an `Error`. + * Loosely based on https://www.npmjs.com/package/iserror + * + * @param {*} value Reference to check. + * @returns {boolean} True if `value` is an `Error`. + */ +function isError(value) { + var tag = toString.call(value); + switch (tag) { + case '[object Error]': return true; + case '[object Exception]': return true; + case '[object DOMException]': return true; + default: return value instanceof Error; + } +} /** * @ngdoc function @@ -747,11 +848,15 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/; function isTypedArray(value) { return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value)); } +function isArrayBuffer(obj) { + return toString.call(obj) === '[object ArrayBuffer]'; +} + var trim = function(value) { return isString(value) ? value.trim() : value; @@ -761,8 +866,10 @@ var trim = function(value) { // 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(/([-()\[\]{}+?*.$\^|,:# + * + *
+ * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` + * and on `destination`) will be ignored. + *
+ * + *
+ * `angular.copy` does not check if destination and source are of the same type. It's the + * developer's responsibility to make sure they are compatible. + *
* - * @param {*} source The source that will be used to make a copy. - * Can be any type, including primitives, `null`, and `undefined`. - * @param {(Object|Array)=} destination Destination into which the source is copied. If - * provided, must be of the same type as `source`. + * @knownIssue + * This is a non-exhaustive list of object types / features that are not handled correctly by + * `angular.copy`. Note that since this functions is used by the change detection code, this + * means binding or watching objects of these types (or that include these types) might not work + * correctly. + * - [`File`](https://developer.mozilla.org/docs/Web/API/File) + * - [`Map`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map) + * - [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData) + * - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream) + * - [`Set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set) + * - [`WeakMap`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WeakMap) + * - [`getter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/ + * [`setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set) + * + * @param {*} source The source that will be used to make a copy. Can be any type, including + * primitives, `null`, and `undefined`. + * @param {(Object|Array)=} destination Destination into which the source is copied. If provided, + * must be of the same type as `source`. * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - - -
-
- Name:
- E-mail:
- Gender: male - female
- - -
-
form = {{user | json}}
-
master = {{master | json}}
-
- - -
-
+ $scope.reset(); + }]); + + */ -function copy(source, destination) { +function copy(source, destination, maxDepth) { var stackSource = []; var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; if (destination) { - if (isTypedArray(destination)) { - throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated."); + if (isTypedArray(destination) || isArrayBuffer(destination)) { + throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.'); } if (source === destination) { - throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); } // Empty the destination object @@ -896,35 +1033,39 @@ function copy(source, destination) { stackSource.push(source); stackDest.push(destination); - return copyRecurse(source, destination); + return copyRecurse(source, destination, maxDepth); } - return copyElement(source); + return copyElement(source, maxDepth); - function copyRecurse(source, destination) { + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; + } var h = destination.$$hashKey; - var result, key; + var key; if (isArray(source)) { for (var i = 0, ii = source.length; i < ii; i++) { - destination.push(copyElement(source[i])); + destination.push(copyElement(source[i], maxDepth)); } } else if (isBlankObject(source)) { // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty for (key in source) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } else if (source && typeof source.hasOwnProperty === 'function') { // Slow path, which must rely on hasOwnProperty for (key in source) { if (source.hasOwnProperty(key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } else { // Slowest path --- hasOwnProperty can't be called as a method for (key in source) { if (hasOwnProperty.call(source, key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } @@ -932,7 +1073,7 @@ function copy(source, destination) { return destination; } - function copyElement(source) { + function copyElement(source, maxDepth) { // Simple values if (!isObject(source)) { return source; @@ -946,26 +1087,14 @@ function copy(source, destination) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); } var needsRecurse = false; - var destination; + var destination = copyType(source); - if (isArray(source)) { - destination = []; - needsRecurse = true; - } else if (isTypedArray(source)) { - destination = new source.constructor(source); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - destination.lastIndex = source.lastIndex; - } else if (isFunction(source.cloneNode)) { - destination = source.cloneNode(true); - } else { - destination = Object.create(getPrototypeOf(source)); + if (destination === undefined) { + destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); needsRecurse = true; } @@ -973,37 +1102,61 @@ function copy(source, destination) { stackDest.push(destination); return needsRecurse - ? copyRecurse(source, destination) + ? copyRecurse(source, destination, maxDepth) : destination; } -} -/** - * 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) { - if (isArray(src)) { - dst = dst || []; + function copyType(source) { + switch (toString.call(source)) { + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); + + case '[object ArrayBuffer]': + // Support: IE10 + if (!source.slice) { + // If we're in this case we know the environment supports ArrayBuffer + /* eslint-disable no-undef */ + var copied = new ArrayBuffer(source.byteLength); + new Uint8Array(copied).set(new Uint8Array(source)); + /* eslint-enable */ + return copied; + } + return source.slice(0); - for (var i = 0, ii = src.length; i < ii; i++) { - dst[i] = src[i]; + case '[object Boolean]': + case '[object Number]': + case '[object String]': + case '[object Date]': + return new source.constructor(source.valueOf()); + + case '[object RegExp]': + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); + re.lastIndex = source.lastIndex; + return re; + + case '[object Blob]': + return new source.constructor([source], {type: source.type}); } - } else if (isObject(src)) { - dst = dst || {}; - for (var key in src) { - if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } + if (isFunction(source.cloneNode)) { + return source.cloneNode(true); } } - - return dst || src; } +// eslint-disable-next-line no-self-compare +function simpleCompare(a, b) { return a === b || (a !== a && b !== b); } + + /** * @ngdoc function * @name angular.equals @@ -1032,44 +1185,78 @@ function shallowCopy(src, dst) { * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. + * + * @example + + +
+
+

User 1

+ Name: + Age: + +

User 2

+ Name: + Age: + +
+
+ +
+ User 1:
{{user1 | json}}
+ User 2:
{{user2 | json}}
+ Equal:
{{result}}
+
+
+
+ + angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { + $scope.user1 = {}; + $scope.user2 = {}; + $scope.compare = function() { + $scope.result = angular.equals($scope.user1, $scope.user2); + }; + }]); + +
*/ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; + // eslint-disable-next-line no-self-compare if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for (key = 0; key < length; key++) { - if (!equals(o1[key], o2[key])) return false; - } - return true; - } - } else if (isDate(o1)) { - if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); - } else if (isRegExp(o1)) { - return isRegExp(o2) ? o1.toString() == o2.toString() : false; - } else { - if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || - isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = createMap(); - for (key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (t1 === t2 && t1 === 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) === o2.length) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for (key in o2) { - if (!(key in keySet) && - key.charAt(0) !== '$' && - isDefined(o2[key]) && - !isFunction(o2[key])) return false; } return true; } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return simpleCompare(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + if (!isRegExp(o2)) return false; + return o1.toString() === o2.toString(); + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + isDefined(o2[key]) && + !isFunction(o2[key])) return false; + } + return true; } } return false; @@ -1079,8 +1266,8 @@ var csp = function() { if (!isDefined(csp.rules)) { - var ngCspElement = (document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); + var ngCspElement = (window.document.querySelector('[ng-csp]') || + window.document.querySelector('[data-ng-csp]')); if (ngCspElement) { var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || @@ -1101,9 +1288,8 @@ var csp = function() { function noUnsafeEval() { try { - /* jshint -W031, -W054 */ + // eslint-disable-next-line no-new, no-new-func new Function(''); - /* jshint +W031, +W054 */ return false; } catch (e) { return true; @@ -1124,7 +1310,7 @@ var csp = function() { * used to force either jqLite by leaving ng-jq blank or setting the name of * the jquery variable under window (eg. jQuery). * - * Since angular looks for this directive when it is loaded (doesn't wait for the + * Since AngularJS looks for this directive when it is loaded (doesn't wait for the * DOMContentLoaded event), it must be placed on an element that comes before the script * which loads angular. Also, only the first instance of `ng-jq` will be used and all * others ignored. @@ -1155,7 +1341,8 @@ var jq = function() { var i, ii = ngAttrPrefixes.length, prefix, name; for (i = 0; i < ii; ++i) { prefix = ngAttrPrefixes[i]; - if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { name = el.getAttribute(prefix + 'jq'); break; } @@ -1173,7 +1360,6 @@ function sliceArgs(args, startIndex) { } -/* jshint -W101 */ /** * @ngdoc function * @name angular.bind @@ -1191,7 +1377,6 @@ function sliceArgs(args, startIndex) { * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ -/* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -1207,7 +1392,7 @@ function bind(self, fn) { : fn.call(self); }; } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } } @@ -1220,7 +1405,7 @@ function toJsonReplacer(key, value) { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; - } else if (value && document === value) { + } else if (value && window.document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; @@ -1238,15 +1423,36 @@ function toJsonReplacer(key, value) { * * @description * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be - * stripped since angular uses this notation internally. + * stripped since AngularJS uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. * @param {boolean|number} [pretty=2] 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. * @returns {string|undefined} JSON-ified string representing `obj`. + * @knownIssue + * + * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` + * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the + * `Date.prototype.toJSON` method as follows: + * + * ``` + * var _DatetoJSON = Date.prototype.toJSON; + * Date.prototype.toJSON = function() { + * try { + * return _DatetoJSON.call(this); + * } catch(e) { + * if (e instanceof RangeError) { + * return null; + * } + * throw e; + * } + * }; + * ``` + * + * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; + if (isUndefined(obj)) return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } @@ -1273,9 +1479,13 @@ function fromJson(json) { } +var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-15+ + // IE/Edge do not "understand" colon (`:`) in timezone + timezone = timezone.replace(ALL_COLONS, ''); var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000; - return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } @@ -1288,8 +1498,9 @@ function addDateMinutes(date, minutes) { function convertTimezoneToLocal(date, timezone, reverse) { reverse = reverse ? -1 : 1; - var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); - return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); } @@ -1297,18 +1508,13 @@ function convertTimezoneToLocal(date, timezone, reverse) { * @returns {string} Returns the string representation of the element. */ function startingTag(element) { - element = jqLite(element).clone(); - try { - // 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) {} - var elemHtml = jqLite('
').append(element).html(); + element = jqLite(element).clone().empty(); + var elemHtml = jqLite('
').append(element).html(); try { return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) : elemHtml. match(/^(<[^>]+>)/)[1]. - replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); + replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);}); } catch (e) { return lowercase(elemHtml); } @@ -1330,7 +1536,7 @@ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch (e) { - // Ignore any invalid uri component + // Ignore any invalid uri component. } } @@ -1341,7 +1547,7 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}; - forEach((keyValue || "").split('&'), function(keyValue) { + forEach((keyValue || '').split('&'), function(keyValue) { var splitPoint, key, val; if (keyValue) { key = keyValue = keyValue.replace(/\+/g,'%20'); @@ -1406,7 +1612,7 @@ function encodeUriSegment(val) { * 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 / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1436,6 +1642,58 @@ function getNgAttribute(element, ngAttr) { return null; } +function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // Support: IE 9-11 only + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for banned URL schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); +} + +// Cached as it has to run during loading so that document.currentScript is available. +var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); + /** * @ngdoc directive * @name ngApp @@ -1456,10 +1714,17 @@ function getNgAttribute(element, ngAttr) { * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `` or `` tags. * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * There are a few things to keep in mind when using `ngApp`: + * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. + * - AngularJS applications cannot be nested within each other. + * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. + * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and + * {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. * * You can specify an **AngularJS module** to be used as the root module for the application. This * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It @@ -1470,9 +1735,13 @@ function getNgAttribute(element, ngAttr) { * document would not be compiled, the `AppController` would not be instantiated and the `{{ a+b }}` * would not be resolved to `3`. * + * @example + * + * ### Simple Usage + * * `ngApp` is the easiest, and most common way to bootstrap an application. * - +
I can add: {{a}} + {{b}} = {{ a+b }} @@ -1486,9 +1755,13 @@ function getNgAttribute(element, ngAttr) { * + * @example + * + * ### With `ngStrictDi` + * * Using `ngStrictDi`, you would see something like this: * - +
@@ -1537,7 +1810,7 @@ function getNgAttribute(element, ngAttr) { }]) .controller('GoodController2', GoodController2); function GoodController2($scope) { - $scope.name = "World"; + $scope.name = 'World'; } GoodController2.$inject = ['$scope']; @@ -1568,7 +1841,7 @@ function angularInit(element, bootstrap) { module, config = {}; - // The element `element` has priority over any other element + // The element `element` has priority over any other element. forEach(ngAttrPrefixes, function(prefix) { var name = prefix + 'app'; @@ -1587,7 +1860,12 @@ function angularInit(element, bootstrap) { } }); if (appElement) { - config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + if (!isAutoBootstrapAllowed) { + window.console.error('AngularJS: disabling automatic bootstrap. @@ -6918,11 +8216,11 @@ function $TemplateCacheProvider() { it('should auto compile', function() { var textarea = $('textarea'); var output = $('div[compile]'); - // The initial state reads 'Hello Angular'. - expect(output.getText()).toBe('Hello Angular'); + // The initial state reads 'Hello AngularJS'. + expect(output.getText()).toBe('Hello AngularJS'); textarea.clear(); textarea.sendKeys('{{name}}!'); - expect(output.getText()).toBe('Angular!'); + expect(output.getText()).toBe('AngularJS!'); }); @@ -6959,47 +8257,440 @@ function $TemplateCacheProvider() { * directives; if given, it will be passed through to the link functions of * directives found in `element` during compilation. * * `transcludeControllers` - an object hash with keys that map controller names - * to controller instances; if given, it will make the controllers - * available to directives. + * to a hash with the key `instance`, which maps to the controller instance; + * if given, it will make the controllers available to directives on the compileNode: + * ``` + * { + * parent: { + * instance: parentControllerInstance + * } + * } + * ``` * * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add - * the cloned elements; only needed for transcludes that are allowed to contain non html - * elements (e.g. SVG elements). See also the directive.controller property. + * 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. * - * After linking the view is not updated until after a call to $digest which typically is done by - * Angular automatically. + * After linking the view is not updated until after a call to `$digest`, which typically is done by + * AngularJS automatically. * * If you need access to the bound view, there are two ways to do it: * * - If you are not asking the linking function to clone the template, create the DOM element(s) * before you send them to the compiler and keep this reference around. * ```js - * var element = $compile('

{{total}}

')(scope); + * var element = angular.element('

{{total}}

'); + * $compile(element)(scope); * ``` * * - if on the other hand, you need the element to be cloned, the view reference from the original * example would not point to the clone, but rather to the original template that was cloned. In - * this case, you can access the clone via the cloneAttachFn: + * this case, you can access the clone either via the `cloneAttachFn` or the value returned by the + * linking function: * ```js - * var templateElement = angular.element('

{{total}}

'), - * scope = ....; - * + * var templateElement = angular.element('

{{total}}

'); * var clonedElement = $compile(templateElement)(scope, function(clonedElement, scope) { - * //attach the clone to DOM document at the right place + * // Attach the clone to DOM document at the right place. * }); * - * //now we have reference to the cloned DOM via `clonedElement` + * // Now we have reference to the cloned DOM via `clonedElement`. + * // NOTE: The `clonedElement` returned by the linking function is the same as the + * // `clonedElement` passed to `cloneAttachFn`. * ``` * * * For information on how the compiler works, see the - * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * {@link guide/compiler AngularJS HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. + + * @knownIssue + + ### Issues with `replace: true` + * + *
+ * **Note**: {@link $compile#-replace- `replace: true`} is deprecated and not recommended to use, + * mainly due to the issues listed here. It has been completely removed in the new Angular. + *
+ * + * #### Attribute values are not merged + * + * When a `replace` directive encounters the same attribute on the original and the replace node, + * it will simply deduplicate the attribute and join the values with a space or with a `;` in case of + * the `style` attribute. + * ```html + * Original Node: + * Replace Template: + * Result: + * ``` + * + * That means attributes that contain AngularJS expressions will not be merged correctly, e.g. + * {@link ngShow} or {@link ngClass} will cause a {@link $parse} error: + * + * ```html + * Original Node: + * Replace Template: + * Result: + * ``` + * + * See issue [#5695](https://github.com/angular/angular.js/issues/5695). + * + * #### Directives are not deduplicated before compilation + * + * When the original node and the replace template declare the same directive(s), they will be + * {@link guide/compiler#double-compilation-and-how-to-avoid-it compiled twice} because the compiler + * does not deduplicate them. In many cases, this is not noticeable, but e.g. {@link ngModel} will + * attach `$formatters` and `$parsers` twice. + * + * See issue [#2573](https://github.com/angular/angular.js/issues/2573). + * + * #### `transclude: element` in the replace template root can have unexpected effects + * + * When the replace template has a directive at the root node that uses + * {@link $compile#-transclude- `transclude: element`}, e.g. + * {@link ngIf} or {@link ngRepeat}, the DOM structure or scope inheritance can be incorrect. + * See the following issues: + * + * - Incorrect scope on replaced element: + * [#9837](https://github.com/angular/angular.js/issues/9837) + * - Different DOM between `template` and `templateUrl`: + * [#10612](https://github.com/angular/angular.js/issues/14326) + * + */ + +/** + * @ngdoc directive + * @name ngProp + * @restrict A + * @element ANY + * + * @usage + * + * ```html + * + * + * ``` + * + * or with uppercase letters in property (e.g. "propName"): + * + * + * ```html + * + * + * ``` + * + * + * @description + * The `ngProp` directive binds an expression to a DOM element property. + * `ngProp` allows writing to arbitrary properties by including + * the property name in the attribute, e.g. `ng-prop-value="'my value'"` binds 'my value' to + * the `value` property. + * + * Usually, it's not necessary to write to properties in AngularJS, as the built-in directives + * handle the most common use cases (instead of the above example, you would use {@link ngValue}). + * + * However, [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements) + * often use custom properties to hold data, and `ngProp` can be used to provide input to these + * custom elements. + * + * ## Binding to camelCase properties + * + * Since HTML attributes are case-insensitive, camelCase properties like `innerHTML` must be escaped. + * AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so + * `innerHTML` must be written as `ng-prop-inner_h_t_m_l="expression"` (Note that this is just an + * example, and for binding HTML {@link ngBindHtml} should be used. + * + * ## Security + * + * Binding expressions to arbitrary properties poses a security risk, as properties like `innerHTML` + * can insert potentially dangerous HTML into the application, e.g. script tags that execute + * malicious code. + * For this reason, `ngProp` applies Strict Contextual Escaping with the {@link ng.$sce $sce service}. + * This means vulnerable properties require their content to be "trusted", based on the + * context of the property. For example, the `innerHTML` is in the `HTML` context, and the + * `iframe.src` property is in the `RESOURCE_URL` context, which requires that values written to + * this property are trusted as a `RESOURCE_URL`. + * + * This can be set explicitly by calling $sce.trustAs(type, value) on the value that is + * trusted before passing it to the `ng-prop-*` directive. There are exist shorthand methods for + * each context type in the form of {@link ng.$sce#trustAsResourceUrl $sce.trustAsResourceUrl()} et al. + * + * In some cases you can also rely upon automatic sanitization of untrusted values - see below. + * + * Based on the context, other options may exist to mark a value as trusted / configure the behavior + * of {@link ng.$sce}. For example, to restrict the `RESOURCE_URL` context to specific origins, use + * the {@link $sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList()} + * and {@link $sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList()}. + * + * {@link ng.$sce#what-trusted-context-types-are-supported- Find out more about the different context types}. + * + * ### HTML Sanitization + * + * By default, `$sce` will throw an error if it detects untrusted HTML content, and will not bind the + * content. + * However, if you include the {@link ngSanitize ngSanitize module}, it will try to sanitize the + * potentially dangerous HTML, e.g. strip non-trusted tags and attributes when binding to + * `innerHTML`. + * + * @example + * ### Binding to different contexts + * + * + * + * angular.module('exampleNgProp', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function($sce) { + * this.safeContent = 'Safe content'; + * this.unsafeContent = ''; + * this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent); + * } + * }); + * + * + *
+ *
+ * Binding to a property without security context: + *
+ * innerText (safeContent) + *
+ * + *
+ * "Safe" content that requires a security context will throw because the contents could potentially be dangerous ... + *
+ * innerHTML (safeContent) + *
+ * + *
+ * ... so that actually dangerous content cannot be executed: + *
+ * innerHTML (unsafeContent) + *
+ * + *
+ * ... but unsafe Content that has been trusted explicitly works - only do this if you are 100% sure! + *
+ * innerHTML (trustedUnsafeContent) + *
+ *
+ *
+ * + *
+ *
+ * + * .prop-unit { + * margin-bottom: 10px; + * } + * + * .prop-binding { + * min-height: 30px; + * border: 1px solid blue; + * } + * + * .prop-note { + * font-family: Monospace; + * } + * + *
+ * + * + * @example + * ### Binding to innerHTML with ngSanitize + * + * + * + * angular.module('exampleNgProp', ['ngSanitize']) + * .component('main', { + * templateUrl: 'main.html', + * controller: function($sce) { + * this.safeContent = 'Safe content'; + * this.unsafeContent = ''; + * this.trustedUnsafeContent = $sce.trustAsHtml(this.unsafeContent); + * } + * }); + * + * + *
+ *
+ * "Safe" content will be sanitized ... + *
+ * innerHTML (safeContent) + *
+ * + *
+ * ... as will dangerous content: + *
+ * innerHTML (unsafeContent) + *
+ * + *
+ * ... and content that has been trusted explicitly works the same as without ngSanitize: + *
+ * innerHTML (trustedUnsafeContent) + *
+ *
+ *
+ * + *
+ *
+ * + * .prop-unit { + * margin-bottom: 10px; + * } + * + * .prop-binding { + * min-height: 30px; + * border: 1px solid blue; + * } + * + * .prop-note { + * font-family: Monospace; + * } + * + *
+ * + */ + +/** @ngdoc directive + * @name ngOn + * @restrict A + * @element ANY + * + * @usage + * + * ```html + * + * + * ``` + * + * or with uppercase letters in property (e.g. "eventName"): + * + * + * ```html + * + * + * ``` + * + * @description + * The `ngOn` directive adds an event listener to a DOM element via + * {@link angular.element angular.element().on()}, and evaluates an expression when the event is + * fired. + * `ngOn` allows adding listeners for arbitrary events by including + * the event name in the attribute, e.g. `ng-on-drop="onDrop()"` executes the 'onDrop()' expression + * when the `drop` event is fired. + * + * AngularJS provides specific directives for many events, such as {@link ngClick}, so in most + * cases it is not necessary to use `ngOn`. However, AngularJS does not support all events + * (e.g. the `drop` event in the example above), and new events might be introduced in later DOM + * standards. + * + * Another use-case for `ngOn` is listening to + * [custom events](https://developer.mozilla.org/docs/Web/Guide/Events/Creating_and_triggering_events) + * fired by + * [custom elements](https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements). + * + * ## Binding to camelCase properties + * + * Since HTML attributes are case-insensitive, camelCase properties like `myEvent` must be escaped. + * AngularJS uses the underscore (_) in front of a character to indicate that it is uppercase, so + * `myEvent` must be written as `ng-on-my_event="expression"`. + * + * @example + * ### Bind to built-in DOM events + * + * + * + * angular.module('exampleNgOn', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function() { + * this.clickCount = 0; + * this.mouseoverCount = 0; + * + * this.loadingState = 0; + * } + * }); + * + * + *
+ * This is equivalent to `ngClick` and `ngMouseover`:
+ *
+ * clickCount: {{$ctrl.clickCount}}
+ * mouseover: {{$ctrl.mouseoverCount}} + * + *
+ * + * For the `error` and `load` event on images no built-in AngularJS directives exist:
+ *
+ *
+ * Image is loading + * Image load error + * Image loaded successfully + *
+ *
+ *
+ * + *
+ *
+ *
+ * + * + * @example + * ### Bind to custom DOM events + * + * + * + * angular.module('exampleNgOn', []) + * .component('main', { + * templateUrl: 'main.html', + * controller: function() { + * this.eventLog = ''; + * + * this.listener = function($event) { + * this.eventLog = 'Event with type "' + $event.type + '" fired at ' + $event.detail; + * }; + * } + * }) + * .component('childComponent', { + * templateUrl: 'child.html', + * controller: function($element) { + * this.fireEvent = function() { + * var event = new CustomEvent('customtype', { detail: new Date()}); + * + * $element[0].dispatchEvent(event); + * }; + * } + * }); + * + * + *
+ * Event log: {{$ctrl.eventLog}} + *
+ * + + * + * + *
+ *
+ *
*/ var $compileMinErr = minErr('$compile'); +function UNINITIALIZED_VALUE() {} +var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); + /** * @ngdoc provider * @name $compileProvider @@ -7007,11 +8698,12 @@ var $compileMinErr = minErr('$compile'); * @description */ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +/** @this */ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\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 = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; @@ -7019,22 +8711,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // The assumption is that future DOM event attribute names will begin with // 'on' and be composed of only English letters. var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/; + var bindingCache = createMap(); function parseIsolateBindings(scope, directiveName, isController) { - var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^([@&]|[=<](\*?))(\??)\s*([\w$]*)$/; - var bindings = {}; + var bindings = createMap(); forEach(scope, function(definition, scopeName) { + definition = definition.trim(); + + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } var match = definition.match(LOCAL_REGEXP); if (!match) { throw $compileMinErr('iscp', - "Invalid {3} for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', directiveName, scopeName, definition, - (isController ? "controller bindings definition" : - "isolate scope definition")); + (isController ? 'controller bindings definition' : + 'isolate scope definition')); } bindings[scopeName] = { @@ -7043,6 +8742,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { optional: match[3] === '?', attrName: match[4] || scopeName }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } }); return bindings; @@ -7067,20 +8769,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { bindings.bindToController = parseIsolateBindings(directive.bindToController, directiveName, true); } - if (isObject(bindings.bindToController)) { - var controller = directive.controller; - var controllerAs = directive.controllerAs; - if (!controller) { - // There is no controller, there may or may not be a controllerAs property - throw $compileMinErr('noctrl', - "Cannot bind to controller without directive '{0}'s controller.", - directiveName); - } else if (!identifierForController(controller, controllerAs)) { - // There is a controller, but no identifier or controllerAs property - throw $compileMinErr('noident', - "Cannot bind to controller without identifier for directive '{0}'.", - directiveName); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -7088,15 +8781,40 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function assertValidDirectiveName(name) { var letter = name.charAt(0); if (!letter || letter !== lowercase(letter)) { - throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name); + throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name); } if (name !== name.trim()) { throw $compileMinErr('baddir', - "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', name); } } + function getDirectiveRequire(directive) { + var require = directive.require || (directive.controller && directive.name); + + if (!isArray(require) && isObject(require)) { + forEach(require, function(value, key) { + var match = value.match(REQUIRE_PREFIX_REGEXP); + var name = value.substring(match[0].length); + if (!name) require[key] = match[0] + key; + }); + } + + return require; + } + + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } + /** * @ngdoc method * @name $compileProvider#directive @@ -7105,14 +8823,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @description * Register a new directive with the compiler. * - * @param {string|Object} name Name of the directive in camel-case (i.e. ngBind which - * will match as ng-bind), or an object map of directives where the keys are the - * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {string|Object} name Name of the directive in camel-case (i.e. `ngBind` which will match + * as `ng-bind`), or an object map of directives where the keys are the names and the values + * are the factories. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ - this.directive = function registerDirective(name, directiveFactory) { + this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7133,13 +8852,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'EA'; - var bindings = directive.$$bindings = - parseDirectiveBindings(directive, directive.name); - if (isObject(bindings.isolateScope)) { - directive.$$isolateBindings = bindings.isolateScope; - } + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -7156,82 +8870,276 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string|Object} name Name of the component in camelCase (i.e. `myComp` which will match ``), + * or an object map of components where the keys are the names and the values are the component definition objects. + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * with the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. An empty `noop` function by default. + * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be `$ctrl`. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties. + * Component properties are always bound to the component controller and not to the scope. + * See {@link ng.$compile#-bindtocontroller- `bindToController`}. + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled. + * Disabled by default. + * - `require` - `{Object=}` - requires the controllers of other directives and binds them to + * this component's controller. The object keys specify the property names under which the required + * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}. + * - `$...` – additional properties to attach to the directive factory function and the controller + * constructor function. (This is used by the component router to annotate) + * + * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls. + * @description + * Register a **component definition** with the compiler. This is a shorthand for registering a special + * type of directive, which represents a self-contained UI component in your application. Such components + * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`). + * + * Component definitions are very simple and do not require as much configuration as defining general + * directives. Component definitions usually consist only of a template and a controller backing it. + * + * In order to make the definition easier, components enforce best practices like use of `controllerAs`, + * `bindToController`. They always have **isolate scope** and are restricted to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '
My name is {{$ctrl.name}}
', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '
My name is {{$ctrl.name}}
', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl', + * controllerAs: 'ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * For more examples, and an in-depth guide, see the {@link guide/component component guide}. + * + *
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + if (!isString(name)) { + forEach(name, reverseParams(bind(this, registerComponent))); + return this; + } + + var controller = options.controller || function() {}; + + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || isArray(fn)) { + return /** @this */ function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + var ddo = { + controller: controller, + controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude, + scope: {}, + bindToController: options.bindings || {}, + restrict: 'E', + require: options.require + }; + + // Copy annotations (starting with $) over to the DDO + forEach(options, function(val, key) { + if (key.charAt(0) === '$') ddo[key] = val; + }); + + return ddo; + } + + // TODO(pete) remove the following `forEach` before we release 1.6.0 + // The component-router@0.2.0 looks for the annotations on the controller constructor + // Nothing in AngularJS looks for annotations on the factory function but we can't remove + // it from 1.5.x yet. + + // Copy any annotation properties (starting with $) over to the factory and controller constructor functions + // These could be used by libraries such as the new component router + forEach(options, function(val, key) { + if (key.charAt(0) === '$') { + factory[key] = val; + // Don't try to copy over annotations to named controller + if (isFunction(controller)) controller[key] = val; + } + }); + + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** * @ngdoc method - * @name $compileProvider#aHrefSanitizationWhitelist + * @name $compileProvider#aHrefSanitizationTrustedUrlList * @kind function * * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during a[href] sanitization. * * 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` + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationTrustedUrlList` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.aHrefSanitizationWhitelist = function(regexp) { + this.aHrefSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); + $$sanitizeUriProvider.aHrefSanitizationTrustedUrlList(regexp); return this; } else { - return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); + return $$sanitizeUriProvider.aHrefSanitizationTrustedUrlList(); } }; /** * @ngdoc method - * @name $compileProvider#imgSrcSanitizationWhitelist + * @name $compileProvider#aHrefSanitizationWhitelist + * @kind function + * + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $compileProvider#aHrefSanitizationTrustedUrlList + * aHrefSanitizationTrustedUrlList} instead. + */ + Object.defineProperty(this, 'aHrefSanitizationWhitelist', { + get: function() { + return this.aHrefSanitizationTrustedUrlList; + }, + set: function(value) { + this.aHrefSanitizationTrustedUrlList = value; + } + }); + + + /** + * @ngdoc method + * @name $compileProvider#imgSrcSanitizationTrustedUrlList * @kind function * * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during img[src] sanitization. * * The sanitization is a security measure aimed at prevent XSS attacks via html links. * * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationTrustedUrlList` * regular expression. If a match is found, the original url is written into the dom. Otherwise, * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.imgSrcSanitizationWhitelist = function(regexp) { + this.imgSrcSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); + $$sanitizeUriProvider.imgSrcSanitizationTrustedUrlList(regexp); return this; } else { - return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); + return $$sanitizeUriProvider.imgSrcSanitizationTrustedUrlList(); } }; + /** * @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 - * + * @name $compileProvider#imgSrcSanitizationWhitelist * @kind function * - * @description + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $compileProvider#imgSrcSanitizationTrustedUrlList + * imgSrcSanitizationTrustedUrlList} instead. + */ + Object.defineProperty(this, 'imgSrcSanitizationWhitelist', { + get: function() { + return this.imgSrcSanitizationTrustedUrlList; + }, + set: function(value) { + this.imgSrcSanitizationTrustedUrlList = value; + } + }); + + /** + * @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 + * * `ng-scope` and `ng-isolated-scope` CSS classes * * `$binding` data property containing an array of the binding expressions + * * Data properties used by the {@link angular.element#methods `scope()`/`isolateScope()` methods} to return + * the element's scope. + * * Placeholder comments will contain information about what directive and binding caused the placeholder. + * E.g. ``. * * 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. @@ -7247,13 +9155,303 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#strictComponentBindingsEnabled + * + * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, + * otherwise return the current strictComponentBindingsEnabled state. + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable / disable the strict component bindings check. If enabled, the + * compiler will enforce that all scope / controller bindings of a + * {@link $compileProvider#directive directive} / {@link $compileProvider#component component} + * that are not set as optional with `?`, must be provided when the directive is instantiated. + * If not provided, the compiler will throw the + * {@link error/$compile/missingattr $compile:missingattr error}. + * + * The default value is false. + */ + var strictComponentBindingsEnabled = false; + this.strictComponentBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + strictComponentBindingsEnabled = enabled; + return this; + } + return strictComponentBindingsEnabled; + }; + + var TTL = 10; + /** + * @ngdoc method + * @name $compileProvider#onChangesTtl + * @description + * + * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result + * in several iterations of calls to these hooks. However if an application needs more than the default 10 + * iterations to stabilize then you should investigate what is causing the model to continuously change during + * the `$onChanges` hook execution. + * + * Increasing the TTL could have performance implications, so you should not change it without proper justification. + * + * @param {number} limit The number of `$onChanges` hook iterations. + * @returns {number|object} the current limit (or `this` if called as a setter for chaining) + */ + this.onChangesTtl = function(value) { + if (arguments.length) { + TTL = value; + return this; + } + return TTL; + }; + + var commentDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#commentDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on comments should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on comments for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check comments when looking for directives. + * This should however only be used if you are sure that no comment directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on comments + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.commentDirectivesEnabled = function(value) { + if (arguments.length) { + commentDirectivesEnabledConfig = value; + return this; + } + return commentDirectivesEnabledConfig; + }; + + + var cssClassDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#cssClassDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on element classes should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on element classes for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check element classes when looking for directives. + * This should however only be used if you are sure that no class directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on element classes + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.cssClassDirectivesEnabled = function(value) { + if (arguments.length) { + cssClassDirectivesEnabledConfig = value; + return this; + } + return cssClassDirectivesEnabledConfig; + }; + + + /** + * The security context of DOM Properties. + * @private + */ + var PROP_CONTEXTS = createMap(); + + /** + * @ngdoc method + * @name $compileProvider#addPropertySecurityContext + * @description + * + * Defines the security context for DOM properties bound by ng-prop-*. + * + * @param {string} elementName The element name or '*' to match any element. + * @param {string} propertyName The DOM property name. + * @param {string} ctx The {@link $sce} security context in which this value is safe for use, e.g. `$sce.URL` + * @returns {object} `this` for chaining + */ + this.addPropertySecurityContext = function(elementName, propertyName, ctx) { + var key = (elementName.toLowerCase() + '|' + propertyName.toLowerCase()); + + if (key in PROP_CONTEXTS && PROP_CONTEXTS[key] !== ctx) { + throw $compileMinErr('ctxoverride', 'Property context \'{0}.{1}\' already set to \'{2}\', cannot override to \'{3}\'.', elementName, propertyName, PROP_CONTEXTS[key], ctx); + } + + PROP_CONTEXTS[key] = ctx; + return this; + }; + + /* Default property contexts. + * + * Copy of https://github.com/angular/angular/blob/6.0.6/packages/compiler/src/schema/dom_security_schema.ts#L31-L58 + * Changing: + * - SecurityContext.* => SCE_CONTEXTS/$sce.* + * - STYLE => CSS + * - various URL => MEDIA_URL + * - *|formAction, form|action URL => RESOURCE_URL (like the attribute) + */ + (function registerNativePropertyContexts() { + function registerContext(ctx, values) { + forEach(values, function(v) { PROP_CONTEXTS[v.toLowerCase()] = ctx; }); + } + + registerContext(SCE_CONTEXTS.HTML, [ + 'iframe|srcdoc', + '*|innerHTML', + '*|outerHTML' + ]); + registerContext(SCE_CONTEXTS.CSS, ['*|style']); + registerContext(SCE_CONTEXTS.URL, [ + 'area|href', 'area|ping', + 'a|href', 'a|ping', + 'blockquote|cite', + 'body|background', + 'del|cite', + 'input|src', + 'ins|cite', + 'q|cite' + ]); + registerContext(SCE_CONTEXTS.MEDIA_URL, [ + 'audio|src', + 'img|src', 'img|srcset', + 'source|src', 'source|srcset', + 'track|src', + 'video|src', 'video|poster' + ]); + registerContext(SCE_CONTEXTS.RESOURCE_URL, [ + '*|formAction', + 'applet|code', 'applet|codebase', + 'base|href', + 'embed|src', + 'frame|src', + 'form|action', + 'head|profile', + 'html|manifest', + 'iframe|src', + 'link|href', + 'media|src', + 'object|codebase', 'object|data', + 'script|src' + ]); + })(); + + this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', + '$controller', '$rootScope', '$sce', '$animate', function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { + $controller, $rootScope, $sce, $animate) { + + var SIMPLE_ATTR_NAME = /^\w/; + var specialAttrHolder = window.document.createElement('div'); + + + var commentDirectivesEnabled = commentDirectivesEnabledConfig; + var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig; + + + var onChangesTtl = TTL; + // The onChanges hooks should all be run together in a single digest + // When changes occur, the call to trigger their hooks will be added to this queue + var onChangesQueue; + + // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest + function flushOnChangesQueue() { + try { + if (!(--onChangesTtl)) { + // We have hit the TTL limit so reset everything + onChangesQueue = undefined; + throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL); + } + // We must run this hook in an apply since the $$postDigest runs outside apply + $rootScope.$apply(function() { + for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { + try { + onChangesQueue[i](); + } catch (e) { + $exceptionHandler(e); + } + } + // Reset the queue to trigger a new schedule next time there is a change + onChangesQueue = undefined; + }); + } finally { + onChangesTtl++; + } + } + + + function sanitizeSrcset(value, invokeType) { + if (!value) { + return value; + } + if (!isString(value)) { + throw $compileMinErr('srcset', 'Can\'t pass trusted values to `{0}`: "{1}"', invokeType, value.toString()); + } + + // Such values are a bit too complex to handle automatically inside $sce. + // Instead, we sanitize each of the URIs individually, which works, even dynamically. + + // It's not possible to work around this using `$sce.trustAsMediaUrl`. + // If you want to programmatically set explicitly trusted unsafe URLs, you should use + // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the + // `ng-bind-html` directive. + + 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 += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx])); + // 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 += $sce.getTrustedMediaUrl(trim(lastTuple[0])); + + // and add the last descriptor if any + if (lastTuple.length === 2) { + result += (' ' + trim(lastTuple[1])); + } + return result; + } - var Attributes = function(element, attributesToCopy) { + + function Attributes(element, attributesToCopy) { if (attributesToCopy) { var keys = Object.keys(attributesToCopy); var i, l, key; @@ -7267,7 +9465,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } this.$$element = element; - }; + } Attributes.prototype = { /** @@ -7357,8 +9555,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ $set: function(key, value, writeAttr, attrName) { // TODO: decide whether or not to throw an error if "class" - //is set through this function since it may cause $updateClass to - //become unstable. + // is set through this function since it may cause $updateClass to + // become unstable. var node = this.$$element[0], booleanKey = getBooleanAttrName(node, key), @@ -7388,63 +9586,43 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nodeName = nodeName_(this.$$element); - 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; + // Sanitize img[srcset] values. + if (nodeName === 'img' && key === 'srcset') { + this[key] = value = sanitizeSrcset(value, '$set(\'srcset\', value)'); } if (writeAttr !== false) { if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { - this.$$element.attr(attrName, value); + if (SIMPLE_ATTR_NAME.test(attrName)) { + // jQuery skips special boolean attrs treatment in XML nodes for + // historical reasons and hence AngularJS cannot freely call + // `.attr(attrName, false) with such attributes. To avoid issues + // in XHTML, call `removeAttr` in such cases instead. + // See https://github.com/jquery/jquery/issues/4249 + if (booleanKey && value === false) { + this.$$element.removeAttr(attrName); + } else { + this.$$element.attr(attrName, value); + } + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } } } // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[observer], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, @@ -7463,7 +9641,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string} key Normalized key. (ie ngAttribute) . * @param {function(interpolatedValue)} fn Function that will be called whenever the interpolated value of the attribute changes. - * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info. + * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation + * guide} for more info. * @returns {function()} Returns a deregistration function for this observer. */ $observe: function(key, fn) { @@ -7485,6 +9664,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + function setSpecialAttr(element, attrName, value) { + // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute` + // so we have to jump through some hoops to get such an attribute + // https://github.com/angular/angular.js/pull/13318 + specialAttrHolder.innerHTML = ''; + var attributes = specialAttrHolder.firstChild.attributes; + var attribute = attributes[0]; + // We have to remove the attribute from its container element before we can add it to the destination element + attributes.removeNamedItem(attribute.name); + attribute.value = value; + element.attributes.setNamedItem(attribute); + } function safeAddClass($element, className) { try { @@ -7498,12 +9689,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var startSymbol = $interpolate.startSymbol(), endSymbol = $interpolate.endSymbol(), - denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') + denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}') ? identity : function denormalizeTemplate(template) { return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); }, - NG_ATTR_BINDING = /^ngAttr[A-Z]/; + NG_PREFIX_BINDING = /^ng(Attr|Prop|On)([A-Z].*)$/; var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/; compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) { @@ -7531,6 +9722,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); } : noop; + compile.$$createComment = function(directiveName, comment) { + var content = ''; + if (debugInfoEnabled) { + content = ' ' + (directiveName || '') + ': '; + if (comment) content += comment + ' '; + } + return window.document.createComment(content); + }; + return compile; //================================ @@ -7542,19 +9742,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // modify it. $compileNodes = jqLite($compileNodes); } - // 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 - forEach($compileNodes, function(node, index) { - if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) { - $compileNodes[index] = jqLite(node).wrap('').parent()[0]; - } - }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); if (previousCompileContext && previousCompileContext.needsNewScope) { @@ -7589,7 +9785,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // for call to the link function. // Note: This will already clone the nodes... $linkNode = jqLite( - wrapTemplate(namespace, jqLite('
').append($compileNodes).html()) + wrapTemplate(namespace, jqLite('
').append($compileNodes).html()) ); } else if (cloneConnectFn) { // important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart @@ -7609,6 +9805,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -7619,7 +9819,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!node) { return 'html'; } else { - return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; + return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; } } @@ -7641,12 +9841,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); @@ -7693,7 +9904,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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) { + for (i = 0; i < linkFns.length; i += 3) { idx = linkFns[i]; stableNodeList[idx] = nodeList[idx]; } @@ -7737,9 +9948,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); @@ -7751,7 +9987,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { transcludeControllers: controllers, futureParentElement: futureParentElement }); - }; + } + + // We need to attach the transclusion slots onto the `boundTranscludeFn` + // so that they are available inside the `controllersBoundTransclude` function + var boundSlots = boundTranscludeFn.$$slots = createMap(); + for (var slotName in transcludeFn.$$slots) { + if (transcludeFn.$$slots[slotName]) { + boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + } else { + boundSlots[slotName] = null; + } + } return boundTranscludeFn; } @@ -7770,61 +10017,96 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodeType = node.nodeType, attrsMap = attrs.$attr, match, + nodeName, className; switch (nodeType) { case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + // use the node name: addDirective(directives, - directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes - for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, + for (var attr, name, nName, value, ngPrefixMatch, nAttrs = node.attributes, j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { var attrStartName = false; var attrEndName = false; + var isNgAttr = false, isNgProp = false, isNgEvent = false; + var multiElementMatch; + attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; + + nName = directiveNormalize(name.toLowerCase()); - // support ngAttr attribute binding - ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + // Support ng-attr-*, ng-prop-* and ng-on-* + if ((ngPrefixMatch = nName.match(NG_PREFIX_BINDING))) { + isNgAttr = ngPrefixMatch[1] === 'Attr'; + isNgProp = ngPrefixMatch[1] === 'Prop'; + isNgEvent = ngPrefixMatch[1] === 'On'; + + // Normalize the non-prefixed name name = name.replace(PREFIX_REGEXP, '') - .substr(8).replace(/_(.)/g, function(match, letter) { + .toLowerCase() + .substr(4 + ngPrefixMatch[1].length).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); }); - } - var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE); - if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) { + // Support *-start / *-end multi element directives + } else if ((multiElementMatch = nName.match(MULTI_ELEMENT_DIR_RE)) && directiveIsMultiElement(multiElementMatch[1])) { attrStartName = name; attrEndName = name.substr(0, name.length - 5) + 'end'; name = name.substr(0, name.length - 6); } - nName = directiveNormalize(name.toLowerCase()); - attrsMap[nName] = name; - if (isNgAttr || !attrs.hasOwnProperty(nName)) { + if (isNgProp || isNgEvent) { + attrs[nName] = value; + attrsMap[nName] = attr.name; + + if (isNgProp) { + addPropertyDirective(node, directives, nName, name); + } else { + addEventDirective(directives, nName, name); + } + } else { + // Update nName for cases where a prefix was removed + // NOTE: the .toLowerCase() is unnecessary and causes https://github.com/angular/angular.js/issues/16624 for ng-attr-* + 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); } - addAttrInterpolateDirective(node, directives, value, nName, isNgAttr); - addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, - attrEndName); + } + + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); } // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; if (isObject(className)) { // Maybe SVGAnimatedString className = className.animVal; } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -7834,29 +10116,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } break; case NODE_TYPE_TEXT: /* Text Node */ - if (msie === 11) { - // Workaround for #11781 - while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) { - node.nodeValue = node.nodeValue + node.nextSibling.nodeValue; - node.parentNode.removeChild(node.nextSibling); - } - } addTextInterpolateDirective(directives, node.nodeValue); break; case NODE_TYPE_COMMENT: /* Comment */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); break; } @@ -7864,8 +10128,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return directives; } + function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + // function created because of performance, try/catch disables + // the optimization of the whole function #14848 + try { + var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + var nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + } + /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -7879,10 +10161,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', attrStart, attrEnd); } - if (node.nodeType == NODE_TYPE_ELEMENT) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -7905,12 +10187,41 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { + return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; } + /** + * A function generator that is used to support both eager and lazy compilation + * linking function. + * @param eager + * @param $compileNodes + * @param transcludeFn + * @param maxPriority + * @param ignoreDirective + * @param previousCompileContext + * @returns {Function} + */ + function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { + var compiled; + + if (eager) { + return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + } + return /** @this */ function lazyCompilation() { + if (!compiled) { + compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + + // Null out all of these references in order to make them eligible for garbage collection + // since this is a potentially long lived closure + $compileNodes = transcludeFn = previousCompileContext = null; + } + return compiled.apply(this, arguments); + }; + } + /** * Once the directives have been collected, their compile functions are executed. This method * is responsible for inlining directive templates as well as terminating the application @@ -7955,6 +10266,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = originalReplaceDirective, childTranscludeFn = transcludeFn, linkFn, + didScanForMultipleTransclusion = false, + mightHaveMultipleTransclusionError = false, directiveValue; // executes all directives on the current element @@ -7973,7 +10286,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { break; // prevent further processing of directives } - if (directiveValue = directive.scope) { + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives @@ -7997,15 +10312,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directiveName = directive.name; + // If we encounter a condition that can result in transclusion on the directive, + // then scan ahead in the remaining directives for others that may cause a multiple + // transclusion error to be thrown during the compilation process. If a matching directive + // is found, then we know that when we encounter a transcluded directive, we need to eagerly + // compile the `transclude` function rather than doing it lazily in order to throw + // exceptions at the correct time + if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template)) + || (directive.transclude && !directive.$$tlb))) { + var candidateDirective; + + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { + if ((candidateDirective.transclude && !candidateDirective.$$tlb) + || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { + mightHaveMultipleTransclusionError = true; + break; + } + } + + didScanForMultipleTransclusion = true; + } + if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); - assertNoDuplicate("'" + directiveName + "' controller", + assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. @@ -8016,17 +10353,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); - childTranscludeFn = compile($template, transcludeFn, terminalPriority, + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: // - controllerDirectives - otherwise we'll create duplicates controllers @@ -8038,10 +10374,72 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective: nonTlbTranscludeDirective }); } else { - $template = jqLite(jqLiteClone(compileNode)).contents(); + + var slots = createMap(); + + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { + + // We have transclusion slots, + // collect them up, compile them and store their transclusion functions + $template = window.document.createDocumentFragment(); + + var slotMap = createMap(); + var filledSlots = createMap(); + + // Parse the element selectors + forEach(directiveValue, function(elementSelector, slotName) { + // If an element selector starts with a ? then it is optional + var optional = (elementSelector.charAt(0) === '?'); + elementSelector = optional ? elementSelector.substring(1) : elementSelector; + + slotMap[elementSelector] = slotName; + + // We explicitly assign `null` since this implies that a slot was defined but not filled. + // Later when calling boundTransclusion functions with a slot name we only error if the + // slot is `undefined` + slots[slotName] = null; + + // filledSlots contains `true` for all slots that are either optional or have been + // filled. This is used to check that we have not missed any required slots + filledSlots[slotName] = optional; + }); + + // Add the matching elements into their slot + forEach($compileNode.contents(), function(node) { + var slotName = slotMap[directiveNormalize(nodeName_(node))]; + if (slotName) { + filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || window.document.createDocumentFragment(); + slots[slotName].appendChild(node); + } else { + $template.appendChild(node); + } + }); + + // Check for required slots that were not filled + forEach(filledSlots, function(filled, slotName) { + if (!filled) { + throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName); + } + }); + + for (var slotName in slots) { + if (slots[slotName]) { + // Only define a transclusion function if the slot was filled + var slotCompileNodes = jqLite(slots[slotName].childNodes); + slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slotCompileNodes, transcludeFn); + } + } + + $template = jqLite($template.childNodes); + } + $compileNode.empty(); // clear contents - childTranscludeFn = compile($template, transcludeFn, undefined, + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined, undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope}); + childTranscludeFn.$$slots = slots; } } @@ -8065,9 +10463,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', directiveName, ''); } @@ -8107,6 +10505,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = directive; } + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, @@ -8119,10 +10518,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + var context = directive.$$originalDirective || directive; if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); + addLinkFns(null, bind(context, linkFn), attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -8169,80 +10569,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - - function getControllers(directiveName, require, $element, elementControllers) { - var value; - - if (isString(require)) { - var match = require.match(REQUIRE_PREFIX_REGEXP); - var name = require.substring(match[0].length); - var inheritType = match[1] || match[3]; - var optional = match[2] === '?'; - - //If only parents then start at the parent element - if (inheritType === '^^') { - $element = $element.parent(); - //Otherwise attempt getting the controller from elementControllers in case - //the element is transcluded (and has no data) and to avoid .data if possible - } else { - value = elementControllers && elementControllers[name]; - value = value && value.instance; - } - - if (!value) { - var dataName = '$' + name + 'Controller'; - value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); - } - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - name, directiveName); - } - } else if (isArray(require)) { - value = []; - for (var i = 0, ii = require.length; i < ii; i++) { - value[i] = getControllers(directiveName, require[i], $element, elementControllers); - } - } - - return value || null; - } - - function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { - var elementControllers = createMap(); - for (var controllerKey in controllerDirectives) { - var directive = controllerDirectives[controllerKey]; - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }; - - var controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - var controllerInstance = $controller(controller, locals, true, directive.controllerAs); - - // For directives with element transclusion the element is a comment, - // 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); - } - } - return elementControllers; - } - - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, - attrs, removeScopeBindingWatches, removeControllerBindingWatches; + function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; if (compileNode === linkNode) { attrs = templateAttrs; @@ -8264,10 +10593,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` transcludeFn = controllersBoundTransclude; transcludeFn.$$boundTransclude = boundTranscludeFn; + // expose the slots on the `$transclude` function + transcludeFn.isSlotFilled = function(slotName) { + return !!boundTranscludeFn.$$slots[slotName]; + }; } if (controllerDirectives) { - elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); } if (newIsolateScopeDirective) { @@ -8277,11 +10610,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile.$$addScopeClass($element, true); isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings; - removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope, + scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope, isolateScope.$$isolateBindings, newIsolateScopeDirective); - if (removeScopeBindingWatches) { - isolateScope.$on('$destroy', removeScopeBindingWatches); + if (scopeBindingInfo.removeWatches) { + isolateScope.$on('$destroy', scopeBindingInfo.removeWatches); } } @@ -8291,22 +10624,47 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } - var controllerResult = controller(); - if (controllerResult !== controller.instance) { - // If the controller constructor has a return value, overwrite the instance - // from setupControllers - controller.instance = controllerResult; - $element.data('$' + controllerDirective.name + 'Controller', controllerResult); - removeControllerBindingWatches && removeControllerBindingWatches(); - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); } - } + }); + + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { @@ -8327,7 +10685,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } // POSTLINKING for (i = postLinkFns.length - 1; i >= 0; i--) { @@ -8341,13 +10701,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { ); } + // Trigger $postLink lifecycle hooks + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$postLink)) { + controllerInstance.$postLink(); + } + }); + // This is the function that is injected as `$transclude`. // Note: all arguments are optional! - function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) { + function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) { var transcludeControllers; - // No scope passed in: if (!isScope(scope)) { + slotName = futureParentElement; futureParentElement = cloneAttachFn; cloneAttachFn = scope; scope = undefined; @@ -8359,9 +10727,104 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!futureParentElement) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + if (slotName) { + // slotTranscludeFn can be one of three things: + // * a transclude function - a filled slot + // * `null` - an optional slot that was not filled + // * `undefined` - a slot that was not declared (i.e. invalid) + var slotTranscludeFn = boundTranscludeFn.$$slots[slotName]; + if (slotTranscludeFn) { + return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else if (isUndefined(slotTranscludeFn)) { + throw $compileMinErr('noslot', + 'No parent directive that requires a transclusion with slot name "{0}". ' + + 'Element: {1}', + slotName, startingTag($element)); + } + } else { + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } + } + } + } + + function getControllers(directiveName, require, $element, elementControllers) { + var value; + + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; + } + + if (!value) { + var dataName = '$' + name + 'Controller'; + + if (inheritType === '^^' && $element[0] && $element[0].nodeType === NODE_TYPE_DOCUMENT) { + // inheritedData() uses the documentElement when it finds the document, so we would + // require from the element itself. + value = null; + } else { + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); + } + } + + if (!value && !optional) { + throw $compileMinErr('ctreq', + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', + name, directiveName); + } + } else if (isArray(require)) { + value = []; + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); } + return elementControllers; } // Depending upon the context in which a directive finds itself it might need to have a new isolated @@ -8397,17 +10860,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { - try { - directive = directives[i]; - if ((isUndefined(maxPriority) || maxPriority > directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + directive = directives[i]; + if ((isUndefined(maxPriority) || maxPriority > directive.priority) && + directive.restrict.indexOf(location) !== -1) { + if (startAttrName) { + directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + } + if (!directive.$$bindings) { + var bindings = directive.$$bindings = + parseDirectiveBindings(directive, directive.name); + if (isObject(bindings.isolateScope)) { + directive.$$isolateBindings = bindings.isolateScope; } - tDirectives.push(directive); - match = directive; } - } catch (e) { $exceptionHandler(e); } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -8445,14 +10913,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { + if (key.charAt(0) !== '$') { if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -8460,18 +10931,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // copy the new attributes on the old attrs object forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + // Check if we already set this attribute in the loop above. + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') { dst[key] = value; - dstAttr[key] = srcAttr[key]; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } } }); } @@ -8508,9 +10977,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', origAsyncDirective.name, templateUrl); } @@ -8536,7 +11005,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { - if (node == compileNode) { + if (node === compileNode) { $rootElement[i] = $compileNode[0]; } }); @@ -8573,6 +11042,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; + }).catch(function(error) { + if (isError(error)) { + $exceptionHandler(error); + } }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { @@ -8651,7 +11124,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { switch (type) { case 'svg': case 'math': - var wrapper = document.createElement('div'); + var wrapper = window.document.createElement('div'); wrapper.innerHTML = '<' + type + '>' + template + ''; return wrapper.childNodes[0].childNodes; default: @@ -8660,37 +11133,113 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { + function getTrustedAttrContext(nodeName, attrNormalizedName) { + if (attrNormalizedName === 'srcdoc') { return $sce.HTML; } - var tag = nodeName_(node); - // maction[xlink:href] can source SVG. It's not limited to . - if (attrNormalizedName == "xlinkHref" || - (tag == "form" && attrNormalizedName == "action") || - (tag != "img" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { + // All nodes with src attributes require a RESOURCE_URL value, except for + // img and various html5 media nodes, which require the MEDIA_URL context. + if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') { + if (['img', 'video', 'audio', 'source', 'track'].indexOf(nodeName) === -1) { + return $sce.RESOURCE_URL; + } + return $sce.MEDIA_URL; + } else if (attrNormalizedName === 'xlinkHref') { + // Some xlink:href are okay, most aren't + if (nodeName === 'image') return $sce.MEDIA_URL; + if (nodeName === 'a') return $sce.URL; return $sce.RESOURCE_URL; + } else if ( + // Formaction + (nodeName === 'form' && attrNormalizedName === 'action') || + // If relative URLs can go where they are not expected to, then + // all sorts of trust issues can arise. + (nodeName === 'base' && attrNormalizedName === 'href') || + // links can be stylesheets or imports, which can run script in the current origin + (nodeName === 'link' && attrNormalizedName === 'href') + ) { + return $sce.RESOURCE_URL; + } else if (nodeName === 'a' && (attrNormalizedName === 'href' || + attrNormalizedName === 'ngHref')) { + return $sce.URL; + } + } + + function getTrustedPropContext(nodeName, propNormalizedName) { + var prop = propNormalizedName.toLowerCase(); + return PROP_CONTEXTS[nodeName + '|' + prop] || PROP_CONTEXTS['*|' + prop]; + } + + function sanitizeSrcsetPropertyValue(value) { + return sanitizeSrcset($sce.valueOf(value), 'ng-prop-srcset'); + } + function addPropertyDirective(node, directives, attrName, propName) { + if (EVENT_HANDLER_ATTR_REGEXP.test(propName)) { + throw $compileMinErr('nodomevents', 'Property bindings for HTML DOM event properties are disallowed'); + } + + var nodeName = nodeName_(node); + var trustedContext = getTrustedPropContext(nodeName, propName); + + var sanitizer = identity; + // Sanitize img[srcset] + source[srcset] values. + if (propName === 'srcset' && (nodeName === 'img' || nodeName === 'source')) { + sanitizer = sanitizeSrcsetPropertyValue; + } else if (trustedContext) { + sanitizer = $sce.getTrusted.bind($sce, trustedContext); } + + directives.push({ + priority: 100, + compile: function ngPropCompileFn(_, attr) { + var ngPropGetter = $parse(attr[attrName]); + var ngPropWatch = $parse(attr[attrName], function sceValueOf(val) { + // Unwrap the value to compare the actual inner safe value, not the wrapper object. + return $sce.valueOf(val); + }); + + return { + pre: function ngPropPreLinkFn(scope, $element) { + function applyPropValue() { + var propValue = ngPropGetter(scope); + $element[0][propName] = sanitizer(propValue); + } + + applyPropValue(); + scope.$watch(ngPropWatch, applyPropValue); + } + }; + } + }); } + function addEventDirective(directives, attrName, eventName) { + directives.push( + createEventDirective($parse, $rootScope, $exceptionHandler, attrName, eventName, /*forceAsync=*/false) + ); + } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { - var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { + var nodeName = nodeName_(node); + var trustedContext = getTrustedAttrContext(nodeName, name); + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - - if (name === "multiple" && nodeName_(node) === "select") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", + if (name === 'multiple' && nodeName === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', 'Interpolations for HTML DOM event attributes are disallowed'); + } + directives.push({ priority: 100, compile: function() { @@ -8698,12 +11247,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { pre: function attrInterpolatePreLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = createMap())); - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); - } - // If the attribute has changed since last $interpolate()ed var newValue = attr[name]; if (newValue !== value) { @@ -8732,7 +11275,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); @@ -8763,7 +11306,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if ($rootElement) { for (i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; @@ -8791,41 +11334,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); + // Append all the `elementsToRemove` to a fragment. This will... + // - remove them from the DOM + // - allow them to still be traversed with .nextSibling + // - allow a single fragment.qSA to fetch all elements being removed + var fragment = window.document.createDocumentFragment(); + for (i = 0; i < removeCount; i++) { + fragment.appendChild(elementsToRemove[i]); + } if (jqLite.hasData(firstElementToRemove)) { - // Copy over user data (that includes Angular's $scope etc.). Don't copy private + // Copy over user data (that includes AngularJS's $scope etc.). Don't copy private // data here because there's no public interface in jQuery to do that and copying over // event listeners (which is the main use of private data) wouldn't work anyway. jqLite.data(newNode, jqLite.data(firstElementToRemove)); - // Remove data of the replaced element. We cannot just call .remove() - // on the element it since that would deallocate scope that is needed - // for the new node. Instead, remove the data "manually". - if (!jQuery) { - delete jqLite.cache[firstElementToRemove[jqLite.expando]]; - } else { - // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after - // the replaced element. The cleanData version monkey-patched by Angular would cause - // the scope to be trashed and we do need the very same scope to work with the new - // element. However, we cannot just cache the non-patched version and use it here as - // that would break if another library patches the method after Angular does (one - // example is jQuery UI). Instead, set a flag indicating scope destroying should be - // skipped this one time. - skipDestroyOnNextJQueryCleanData = true; - jQuery.cleanData([firstElementToRemove]); - } + // Remove $destroy event listeners from `firstElementToRemove` + jqLite(firstElementToRemove).off('$destroy'); } - 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 - fragment.appendChild(element); - delete elementsToRemove[k]; - } + // Cleanup any data/listeners on the elements and children. + // This includes invoking the $destroy event on any elements with listeners. + jqLite.cleanData(fragment.querySelectorAll('*')); + // Update the jqLite collection to only contain the `newNode` + for (i = 1; i < removeCount; i++) { + delete elementsToRemove[i]; + } elementsToRemove[0] = newNode; elementsToRemove.length = 1; } @@ -8844,41 +11379,62 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } + function strictBindingsCheck(attrName, directiveName) { + if (strictComponentBindingsEnabled) { + throw $compileMinErr('missingattr', + 'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!', + attrName, directiveName); + } + } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; - forEach(bindings, function(definition, scopeName) { + var initialChanges = {}; + var changes; + + forEach(bindings, function initializeBinding(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, - mode = definition.mode, // @, =, or & + mode = definition.mode, // @, =, <, or & lastValue, - parentGet, parentSet, compare; + parentGet, parentSet, compare, removeWatch; switch (mode) { case '@': if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; + strictBindingsCheck(attrName, directive.name); + destination[scopeName] = attrs[attrName] = undefined; + } - attrs.$observe(attrName, function(value) { - if (isString(value)) { + removeWatch = attrs.$observe(attrName, function(value) { + if (isString(value) || isBoolean(value)) { + var oldValue = destination[scopeName]; + recordChanges(scopeName, value, oldValue); destination[scopeName] = value; } }); attrs.$$observers[attrName].$$scope = scope; - if (isString(attrs[attrName])) { + lastValue = attrs[attrName]; + if (isString(lastValue)) { // If the attribute has been provided then we trigger an interpolation to ensure // the value is there for use in the link fn - destination[scopeName] = $interpolate(attrs[attrName])(scope); + destination[scopeName] = $interpolate(lastValue)(scope); + } else if (isBoolean(lastValue)) { + // If the attributes is one of the BOOLEAN_ATTR then AngularJS will have converted + // the value to boolean rather than a string, so we special case this situation + destination[scopeName] = lastValue; } + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': if (!hasOwnProperty.call(attrs, attrName)) { if (optional) break; - attrs[attrName] = void 0; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; } if (optional && !attrs[attrName]) break; @@ -8886,14 +11442,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a, b) { return a === b || (a !== a && b !== b); }; + compare = simpleCompare; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = destination[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], directive.name); + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', + attrs[attrName], attrName, directive.name); }; lastValue = destination[scopeName] = parentGet(scope); var parentValueWatch = function parentValueWatch(parentValue) { @@ -8907,10 +11463,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { parentSet(scope, parentValue = destination[scopeName]); } } - return lastValue = parentValue; + lastValue = parentValue; + return lastValue; }; parentValueWatch.$stateful = true; - var removeWatch; if (definition.collection) { removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { @@ -8919,7 +11475,38 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeWatchCollection.push(removeWatch); break; + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + strictBindingsCheck(attrName, directive.name); + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + var isLiteral = parentGet.literal; + + var initialValue = destination[scopeName] = parentGet(scope); + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + + removeWatch = scope[definition.collection ? '$watchCollection' : '$watch'](parentGet, function parentValueWatchAction(newValue, oldValue) { + if (oldValue === newValue) { + if (oldValue === initialValue || (isLiteral && equals(oldValue, initialValue))) { + return; + } + oldValue = initialValue; + } + recordChanges(scopeName, newValue, oldValue); + destination[scopeName] = newValue; + }); + + removeWatchCollection.push(removeWatch); + break; + case '&': + if (!optional && !hasOwnProperty.call(attrs, attrName)) { + strictBindingsCheck(attrName, directive.name); + } // Don't assign Object.prototype method to scope parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; @@ -8933,22 +11520,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - return removeWatchCollection.length && function removeWatches() { - for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { - removeWatchCollection[i](); + function recordChanges(key, currentValue, previousValue) { + if (isFunction(destination.$onChanges) && !simpleCompare(currentValue, previousValue)) { + // If we have not already scheduled the top level onChangesQueue handler then do so now + if (!onChangesQueue) { + scope.$$postDigest(flushOnChangesQueue); + onChangesQueue = []; + } + // If we have not already queued a trigger of onChanges for this controller then do so now + if (!changes) { + changes = {}; + onChangesQueue.push(triggerOnChangesHook); + } + // If the has been a change on this property already then we need to reuse the previous value + if (changes[key]) { + previousValue = changes[key].previousValue; + } + // Store this change + changes[key] = new SimpleChange(previousValue, currentValue); + } + } + + function triggerOnChangesHook() { + destination.$onChanges(changes); + // Now clear the changes so that we schedule onChanges when more changes arrive + changes = undefined; + } + + return { + initialChanges: initialChanges, + removeWatches: removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } } }; } }]; } -var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; +function SimpleChange(previous, current) { + this.previousValue = previous; + this.currentValue = current; +} +SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; + + +var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, function(_, letter, offset) { + return offset ? letter.toUpperCase() : letter; + }); } /** @@ -8958,7 +11588,7 @@ function directiveNormalize(name) { * @description * A shared object between directive compile / linking functions which contains normalized DOM * element attributes. The values reflect current binding state `{{ }}`. The normalization is - * needed since all of these are treated as equivalent in Angular: + * needed since all of these are treated as equivalent in AngularJS: * * ``` * @@ -9020,7 +11650,7 @@ function tokenDifference(str1, str2) { 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; + if (token === tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } @@ -9037,8 +11667,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; @@ -9047,7 +11678,7 @@ function removeComments(jqNodes) { var $controllerMinErr = minErr('$controller'); -var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; +var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; function identifierForController(controller, ident) { if (ident && isString(ident)) return ident; if (isString(controller)) { @@ -9060,16 +11691,26 @@ function identifierForController(controller, ident) { /** * @ngdoc provider * @name $controllerProvider + * @this + * * @description - * The {@link ng.$controller $controller service} is used by Angular to create new + * The {@link ng.$controller $controller service} is used by AngularJS to create new * controllers. * * This provider allows controller registration via the * {@link ng.$controllerProvider#register register} method. */ function $ControllerProvider() { - var controllers = {}, - globals = false; + var controllers = {}; + + /** + * @ngdoc method + * @name $controllerProvider#has + * @param {string} name Controller name to check. + */ + this.has = function(name) { + return controllers.hasOwnProperty(name); + }; /** * @ngdoc method @@ -9088,17 +11729,7 @@ 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) { + this.$get = ['$injector', function($injector) { /** * @ngdoc service @@ -9111,8 +11742,6 @@ 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 - * * 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 @@ -9127,7 +11756,7 @@ 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, later, ident) { + return function $controller(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 @@ -9145,15 +11774,19 @@ function $ControllerProvider() { match = expression.match(CNTRL_REG); if (!match) { throw $controllerMinErr('ctrlfmt', - "Badly formed controller string '{0}'. " + - "Must match `__name__ as __id__` or `__name__`.", expression); + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); } - constructor = match[1], + constructor = match[1]; identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] - : getter(locals.$scope, constructor, true) || - (globals ? getter($window, constructor, true) : undefined); + : getter(locals.$scope, constructor, true); + + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } assertArgFn(expression, constructor, true); } @@ -9177,8 +11810,7 @@ function $ControllerProvider() { addIdentifier(locals, identifier, instance, constructor || expression.name); } - var instantiate; - return instantiate = extend(function() { + return extend(function $controllerInit() { var result = $injector.invoke(expression, instance, locals, constructor); if (result !== instance && (isObject(result) || isFunction(result))) { instance = result; @@ -9206,7 +11838,7 @@ function $ControllerProvider() { 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`.", + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', name, identifier); } @@ -9219,12 +11851,13 @@ function $ControllerProvider() { * @ngdoc service * @name $document * @requires $window + * @this * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - +

$document title:

@@ -9246,13 +11879,41 @@ function $DocumentProvider() { }]; } + +/** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ +function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; +} + /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log + * @this * * @description - * Any uncaught exception in angular expressions is delegated to this service. + * Any uncaught exception in AngularJS expressions is delegated to this service. * The default implementation simply delegates to `$log.error` which logs it into * the browser console. * @@ -9261,18 +11922,21 @@ function $DocumentProvider() { * * ## Example: * + * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught + * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead + * of `$log.error()`. + * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { - * return function(exception, cause) { - * exception.message += ' (caused by "' + cause + '")'; - * throw exception; - * }; - * }); + * angular. + * module('exceptionOverwrite', []). + * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) { + * return function myExceptionHandler(exception, cause) { + * logErrorsToBackend(exception, cause); + * $log.warn(exception, cause); + * }; + * }]); * ``` * - * 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. - * *
* 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} @@ -9282,7 +11946,7 @@ function $DocumentProvider() { * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which + * @param {string=} cause Optional information about the context in which * the error was thrown. * */ @@ -9294,7 +11958,7 @@ function $ExceptionHandlerProvider() { }]; } -var $$ForceReflowProvider = function() { +var $$ForceReflowProvider = /** @this */ function() { this.$get = ['$document', function($document) { return function(domNode) { //the line below will force the browser to perform a repaint so @@ -9324,13 +11988,8 @@ var JSON_ENDS = { '[': /]$/, '{': /}$/ }; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/; var $httpMinErr = minErr('$http'); -var $httpMinErrLegacyFn = function(method) { - return function() { - throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method); - }; -}; function serializeValue(v) { if (isObject(v)) { @@ -9340,6 +11999,7 @@ function serializeValue(v) { } +/** @this */ function $HttpParamSerializerProvider() { /** * @ngdoc service @@ -9352,19 +12012,19 @@ function $HttpParamSerializerProvider() { * * `{'foo': 'bar'}` results in `foo=bar` * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) - * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object) * * Note that serializer will sort the request parameters alphabetically. - * */ + */ this.$get = function() { return function ngParamSerializer(params) { if (!params) return ''; var parts = []; forEachSorted(params, function(value, key) { - if (value === null || isUndefined(value)) return; + if (value === null || isUndefined(value) || isFunction(value)) return; if (isArray(value)) { - forEach(value, function(v, k) { + forEach(value, function(v) { parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); }); } else { @@ -9377,10 +12037,12 @@ function $HttpParamSerializerProvider() { }; } +/** @this */ function $HttpParamSerializerJQLikeProvider() { /** * @ngdoc service * @name $httpParamSerializerJQLike + * * @description * * Alternative {@link $http `$http`} params serializer that follows @@ -9420,7 +12082,7 @@ function $HttpParamSerializerJQLikeProvider() { * }); * ``` * - * */ + */ this.$get = function() { return function jQueryLikeParamSerializer(params) { if (!params) return ''; @@ -9429,7 +12091,6 @@ function $HttpParamSerializerJQLikeProvider() { return parts.join('&'); function serialize(toSerialize, prefix, topLevel) { - if (toSerialize === null || isUndefined(toSerialize)) return; if (isArray(toSerialize)) { forEach(toSerialize, function(value, index) { serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']'); @@ -9442,7 +12103,11 @@ function $HttpParamSerializerJQLikeProvider() { (topLevel ? '' : ']')); }); } else { - parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize))); + if (isFunction(toSerialize)) { + toSerialize = toSerialize(); + } + parts.push(encodeUriQuery(prefix) + '=' + + (toSerialize == null ? '' : encodeUriQuery(serializeValue(toSerialize)))); } } }; @@ -9456,8 +12121,18 @@ function defaultHttpResponseTransform(data, headers) { if (tempData) { var contentType = headers('Content-Type'); - if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) { - data = fromJson(tempData); + var hasJsonContentType = contentType && (contentType.indexOf(APPLICATION_JSON) === 0); + + if (hasJsonContentType || isJsonLike(tempData)) { + try { + data = fromJson(tempData); + } catch (e) { + if (!hasJsonContentType) { + return data; + } + throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' + + 'Parse error: "{1}"', data, e); + } } } } @@ -9509,7 +12184,7 @@ function parseHeaders(headers) { * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { @@ -9520,7 +12195,7 @@ function headersGetter(headers) { if (name) { var value = headersObj[lowercase(name)]; - if (value === void 0) { + if (value === undefined) { value = null; } return value; @@ -9563,9 +12238,11 @@ function isSuccess(status) { /** * @ngdoc provider * @name $httpProvider + * @this + * * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. - * */ + */ function $HttpProvider() { /** * @ngdoc property @@ -9574,16 +12251,9 @@ function $HttpProvider() { * * Object containing default values for all {@link ng.$http $http} requests. * - * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} - * that will provide the cache for all requests who set their `cache` property to `true`. - * If you set the `defaults.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.cache`** - {boolean|Object} - A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses + * by default. See {@link $http#caching $http Caching} for more information. * * - **`defaults.headers`** - {Object} - Default headers for all $http requests. * Refer to {@link ng.$http#setting-http-headers $http} for documentation on @@ -9593,13 +12263,40 @@ function $HttpProvider() { * - **`defaults.headers.put`** * - **`defaults.headers.patch`** * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. * * - **`defaults.paramSerializer`** - `{string|function(Object):string}` - A function * used to the prepare string representation of request parameters (specified as an object). * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * - **/ + * - **`defaults.transformRequest`** - + * `{Array|function(data, headersGetter)}` - + * An array of functions (or a single function) which are applied to the request data. + * By default, this is an array with one request transformation function: + * + * - If the `data` property of the request configuration object contains an object, serialize it + * into JSON format. + * + * - **`defaults.transformResponse`** - + * `{Array|function(data, headersGetter, status)}` - + * An array of functions (or a single function) which are applied to the response data. By default, + * this is an array which applies one response transformation function that does two things: + * + * - If XSRF prefix is detected, strip it + * (see {@link ng.$http#security-considerations Security Considerations in the $http docs}). + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. + * + * - **`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'`. + * + */ var defaults = this.defaults = { // transform incoming response data transformResponse: [defaultHttpResponseTransform], @@ -9622,7 +12319,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -9644,7 +12343,7 @@ function $HttpProvider() { * * @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; @@ -9653,30 +12352,6 @@ function $HttpProvider() { return useApplyAsync; }; - var useLegacyPromise = true; - /** - * @ngdoc method - * @name $httpProvider#useLegacyPromiseExtensions - * @description - * - * Configure `$http` service to return promises without the shorthand methods `success` and `error`. - * This should be used to make sure that applications work without these methods. - * - * Defaults to true. If no value is specified, returns the current configured value. - * - * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods. - * - * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining. - * otherwise, returns the current configured value. - **/ - this.useLegacyPromiseExtensions = function(value) { - if (isDefined(value)) { - useLegacyPromise = !!value; - return this; - } - return useLegacyPromise; - }; - /** * @ngdoc property * @name $httpProvider#interceptors @@ -9689,20 +12364,82 @@ function $HttpProvider() { * array, on request, but reverse order, on response. * * {@link ng.$http#interceptors Interceptors detailed info} - **/ + */ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { - - var defaultCache = $cacheFactory('$http'); - - /** - * Make sure that default param serializer is exposed as a function - */ - defaults.paramSerializer = isString(defaults.paramSerializer) ? - $injector.get(defaults.paramSerializer) : defaults.paramSerializer; - + /** + * @ngdoc property + * @name $httpProvider#xsrfTrustedOrigins + * @description + * + * Array containing URLs whose origins are trusted to receive the XSRF token. See the + * {@link ng.$http#security-considerations Security Considerations} sections for more details on + * XSRF. + * + * **Note:** An "origin" consists of the [URI scheme](https://en.wikipedia.org/wiki/URI_scheme), + * the [hostname](https://en.wikipedia.org/wiki/Hostname) and the + * [port number](https://en.wikipedia.org/wiki/Port_(computer_networking). For `http:` and + * `https:`, the port number can be omitted if using th default ports (80 and 443 respectively). + * Examples: `http://example.com`, `https://api.example.com:9876` + * + *
+ * It is not possible to trust specific URLs/paths. The `path`, `query` and `fragment` parts + * of a URL will be ignored. For example, `https://foo.com/path/bar?query=baz#fragment` will be + * treated as `https://foo.com`, meaning that **all** requests to URLs starting with + * `https://foo.com/` will include the XSRF token. + *
+ * + * @example + * + * ```js + * // App served from `https://example.com/`. + * angular. + * module('xsrfTrustedOriginsExample', []). + * config(['$httpProvider', function($httpProvider) { + * $httpProvider.xsrfTrustedOrigins.push('https://api.example.com'); + * }]). + * run(['$http', function($http) { + * // The XSRF token will be sent. + * $http.get('https://api.example.com/preferences').then(...); + * + * // The XSRF token will NOT be sent. + * $http.get('https://stats.example.com/activity').then(...); + * }]); + * ``` + */ + var xsrfTrustedOrigins = this.xsrfTrustedOrigins = []; + + /** + * @ngdoc property + * @name $httpProvider#xsrfWhitelistedOrigins + * @description + * + * @deprecated + * sinceVersion="1.8.1" + * + * This property is deprecated. Use {@link $httpProvider#xsrfTrustedOrigins xsrfTrustedOrigins} + * instead. + */ + Object.defineProperty(this, 'xsrfWhitelistedOrigins', { + get: function() { + return this.xsrfTrustedOrigins; + }, + set: function(origins) { + this.xsrfTrustedOrigins = origins; + } + }); + + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { + + var defaultCache = $cacheFactory('$http'); + + /** + * Make sure that default param serializer is exposed as a function + */ + defaults.paramSerializer = isString(defaults.paramSerializer) ? + $injector.get(defaults.paramSerializer) : defaults.paramSerializer; + /** * Interceptors stored in reverse order. Inner interceptors before outer interceptors. * The reversal is needed so that we can build up the interception chain around the @@ -9715,6 +12452,11 @@ function $HttpProvider() { ? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); }); + /** + * A function to check request URLs against a list of allowed origins. + */ + var urlIsAllowedOrigin = urlIsAllowedOriginFactory(xsrfTrustedOrigins); + /** * @ngdoc service * @kind function @@ -9726,7 +12468,7 @@ function $HttpProvider() { * @requires $injector * * @description - * The `$http` service is a core Angular service that facilitates communication with the remote + * The `$http` service is a core AngularJS service that facilitates communication with the remote * HTTP servers via the browser's [XMLHttpRequest](https://developer.mozilla.org/en/xmlhttprequest) * object or via [JSONP](http://en.wikipedia.org/wiki/JSONP). * @@ -9743,7 +12485,9 @@ function $HttpProvider() { * * ## General usage * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} — - * that is used to generate an HTTP request and returns a {@link ng.$q promise}. + * that is used to generate an HTTP request and returns a {@link ng.$q promise} that is + * resolved (request success) or rejected (request failure) with a + * {@link ng.$http#$http-returns response} object. * * ```js * // Simple GET request example: @@ -9759,20 +12503,6 @@ function $HttpProvider() { * }); * ``` * - * The response object has these properties: - * - * - **data** – `{string|Object}` – The response body transformed with the transform - * functions. - * - **status** – `{number}` – HTTP status code of the response. - * - **headers** – `{function([headerName])}` – Header getter function. - * - **config** – `{Object}` – The configuration object that was used to generate the request. - * - **statusText** – `{string}` – HTTP status text of the response. - * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. - * * * ## Shortcut methods * @@ -9807,14 +12537,6 @@ function $HttpProvider() { * $httpBackend.flush(); * ``` * - * ## Deprecation Notice - *
- * The `$http` legacy promise methods `success` and `error` have been deprecated. - * Use the standard `then` method instead. - * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to - * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error. - *
- * * ## Setting HTTP Headers * * The $http service will automatically add certain HTTP headers to all requests. These defaults @@ -9822,7 +12544,7 @@ function $HttpProvider() { * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - Accept: application/json, text/plain, \*/\* * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -9838,7 +12560,7 @@ function $HttpProvider() { * * ``` * module.run(function($http) { - * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w' + * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'; * }); * ``` * @@ -9868,6 +12590,15 @@ function $HttpProvider() { * the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions, * which allows you to `push` or `unshift` a new transformation function into the transformation chain. * + *
+ * **Note:** AngularJS does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline. + * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference). + * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest + * function will be reflected on the scope and in any templates where the object is data-bound. + * To prevent this, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + *
+ * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and @@ -9877,22 +12608,25 @@ function $HttpProvider() { * 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: + * AngularJS provides the following default transformations: * - * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`): + * Request transformations (`$httpProvider.defaults.transformRequest` and `$http.defaults.transformRequest`) is + * an array with one function that does the following: * * - If the `data` property of the request configuration object contains an object, serialize it * into JSON format. * - * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`): + * Response transformations (`$httpProvider.defaults.transformResponse` and `$http.defaults.transformResponse`) is + * an array with one function that does the following: * * - If XSRF prefix is detected, strip it (see Security Considerations section below). - * - If JSON response is detected, deserialize it using a JSON parser. + * - If the `Content-Type` is `application/json` or the response looks like JSON, + * deserialize it using a JSON parser. * * * ### Overriding the Default Transformations Per Request * - * If you wish override the request/response transformations only for a single request then provide + * If you wish to override the request/response transformations only for a single request then provide * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * @@ -9925,26 +12659,35 @@ function $HttpProvider() { * * ## Caching * - * To enable caching, set the request configuration `cache` property to `true` (to use default - * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}). - * When the cache is enabled, `$http` stores the response from the server in the specified - * cache. The next time the same request is made, the response is served from the cache without - * sending a request to the server. + * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must + * set the config.cache value or the default cache value to TRUE or to a cache object (created + * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes + * precedence over the default cache value. * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * In order to: + * * cache all responses - set the default cache value to TRUE or to a cache object + * * cache a specific response - set config.cache value to TRUE or to a cache object * - * If there are multiple GET requests for the same URL that should be cached using the same - * cache, but the cache is not populated yet, only one request to the server will be made and - * the remaining requests will be fulfilled using the response from the first request. + * If caching is enabled, but neither the default cache nor config.cache are set to a cache object, + * then the default `$cacheFactory("$http")` object is used. * - * You can change the default cache to a new object (built with - * {@link ng.$cacheFactory `$cacheFactory`}) by updating the - * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set - * their `cache` property to `true` will now use this cache object. + * The default cache value can be set by updating the + * {@link ng.$http#defaults `$http.defaults.cache`} property or the + * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property. + * + * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using + * the relevant cache object. The next time the same request is made, the response is returned + * from the cache without sending a request to the server. + * + * Take note that: + * + * * Only GET and JSONP requests are cached. + * * The cache key is the request URL including search parameters; headers are not considered. + * * Cached responses are returned asynchronously, in the same way as responses from the server. + * * If multiple identical requests are made using the same cache, which is not yet populated, + * one request will be made to the server and remaining requests will return the same response. + * * A cache-control header on the response does not affect if or how responses are cached. * - * If you set the default cache to `false` then only requests that specify their own custom - * cache object will be cached. * * ## Interceptors * @@ -10038,7 +12781,7 @@ function $HttpProvider() { * - [JSON vulnerability](http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx) * - [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) * - * Both server and the client must cooperate in order to eliminate these threats. Angular comes + * Both server and the client must cooperate in order to eliminate these threats. AngularJS comes * pre-configured with strategies that address these issues, but for this to work backend server * cooperation is required. * @@ -10048,7 +12791,7 @@ function $HttpProvider() { * allows third party website to turn your JSON resource URL into * [JSONP](http://en.wikipedia.org/wiki/JSONP) request under some conditions. To * counter this your server can prefix all JSON requests with following string `")]}',\n"`. - * Angular will automatically strip the prefix before processing it as JSON. + * AngularJS will automatically strip the prefix before processing it as JSON. * * For example if your server needs to return: * ```js @@ -10061,46 +12804,70 @@ function $HttpProvider() { * ['one','two'] * ``` * - * Angular will strip the prefix, before processing the JSON. + * AngularJS will strip the prefix, before processing the JSON. * * * ### Cross Site Request Forgery (XSRF) Protection * - * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which - * an unauthorized site can gain your user's private data. Angular provides a mechanism - * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie - * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only - * JavaScript that runs on your domain could read the cookie, your server can be assured that - * the XHR came from JavaScript running on your domain. The header will not be set for - * cross-domain requests. + * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by + * which the attacker can trick an authenticated user into unknowingly executing actions on your + * website. AngularJS provides a mechanism to counter XSRF. When performing XHR requests, the + * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP + * header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read + * the cookie, your server can be assured that the XHR came from JavaScript running on your + * domain. * * To take advantage of this, your server needs to set a token in a JavaScript readable session * cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the - * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure - * 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 + * server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be + * sure 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)) * for added security. * - * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName - * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time, - * or the per-request config object. + * The header will — by default — **not** be set for cross-domain requests. This + * prevents unauthorized servers (e.g. malicious or compromised 3rd-party APIs) from gaining + * access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you + * want to, you can trust additional origins to also receive the XSRF token, by adding them + * to {@link ng.$httpProvider#xsrfTrustedOrigins xsrfTrustedOrigins}. This might be + * useful, for example, if your application, served from `example.com`, needs to access your API + * at `api.example.com`. + * See {@link ng.$httpProvider#xsrfTrustedOrigins $httpProvider.xsrfTrustedOrigins} for + * more details. + * + *
+ * **Warning**
+ * Only trusted origins that you have control over and make sure you understand the + * implications of doing so. + *
+ * + * The name of the cookie and the header can be specified using the `xsrfCookieName` and + * `xsrfHeaderName` properties of either `$httpProvider.defaults` at config-time, + * `$http.defaults` at run-time, or the per-request config object. + * + * In order to prevent collisions in environments where multiple AngularJS apps share the + * same domain or subdomain, we recommend that each application uses a unique cookie name. * - * In order to prevent collisions in environments where multiple Angular apps share the - * same domain or subdomain, we recommend that each application uses unique cookie name. * * @param {object} config Object describing the request to be made and how it should be * processed. The object has following properties: * * - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) - * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * - **params** – `{Object.}` – Map of strings or objects which will be serialized * with the `paramSerializer` and appended as GET parameters. * - **data** – `{string|Object}` – Data to be sent as the request message data. * - **headers** – `{Object}` – Map of strings or functions which return strings representing * HTTP headers to send to the server. If the return value of a function is null, the * header will not be sent. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – @@ -10114,7 +12881,7 @@ function $HttpProvider() { * transform function or an array of such functions. The transform function takes the http * 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 TransformationjqLiks} + * Overriding the Default Transformations} * - **paramSerializer** - `{string|function(Object):string}` - A function used to * prepare the string representation of request parameters (specified as an object). * If specified as string, it is interpreted as function registered with the @@ -10122,20 +12889,49 @@ function $HttpProvider() { * by registering it as a {@link auto.$provide#service service}. * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} - * - **cache** – `{boolean|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. + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. + * + * A numerical timeout or a promise returned from {@link ng.$timeout $timeout}, will set + * the `xhrStatus` in the {@link $http#$http-returns response} to "timeout", and any other + * resolved promise will set it to "abort", following standard XMLHttpRequest behavior. + * * - **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 * [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype). * - * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object - * when the request succeeds or fails. + * @returns {HttpPromise} A {@link ng.$q `Promise}` that will be resolved (request success) + * or rejected (request failure) with a response object. + * + * The response object has these properties: + * + * - **data** – `{string|Object}` – The response body transformed with + * the transform functions. + * - **status** – `{number}` – HTTP status code of the response. + * - **headers** – `{function([headerName])}` – Header getter function. + * - **config** – `{Object}` – The configuration object that was used + * to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. + * - **xhrStatus** – `{string}` – Status of the XMLHttpRequest + * (`complete`, `error`, `timeout` or `abort`). + * + * + * A response status code between 200 and 299 is considered a success status + * and will result in the success callback being called. Any response status + * code outside of that range is considered an error status and will result + * in the error callback being called. + * Also, status codes less than -1 are normalized to zero. -1 usually means + * the request was aborted, e.g. using a `config.timeout`. More information + * about the status might be available in the `xhrStatus` property. + * + * Note that if the response is a redirect, XMLHttpRequest will transparently + * follow it, meaning that the outcome (success or error) will be determined + * by the final response status code. * * * @property {Array.} pendingRequests Array of config objects for currently pending @@ -10143,7 +12939,7 @@ function $HttpProvider() { * * * @example - +

+ * Current time is: + *
+ * Blood 1 : {{blood_1}} + * Blood 2 : {{blood_2}} + * + * + * + *
+ * + * + *
+ *
+ */ + var interval = $$intervalFactory(setIntervalFn, clearIntervalFn); + + /** + * @ngdoc method + * @name $interval#cancel + * + * @description + * Cancels a task associated with the `promise`. + * + * @param {Promise=} promise returned by the `$interval` function. + * @returns {boolean} Returns `true` if the task was successfully canceled. + */ + interval.cancel = function(promise) { + if (!promise) return false; - /** - * @ngdoc service - * @name $interval - * - * @description - * Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` - * milliseconds. - * - * The return value of registering an interval function is a promise. This promise will be - * notified upon each tick of the interval, and will be resolved after `count` iterations, or - * run indefinitely if `count` is not defined. The value of the notification will be the - * number of iterations that have run. - * To cancel an interval, call `$interval.cancel(promise)`. - * - * In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to - * move forward by `millis` milliseconds and trigger any functions scheduled to run in that - * time. - * - *
- * **Note**: Intervals created by this service must be explicitly destroyed when you are finished - * with them. In particular they are not automatically destroyed when a controller's scope or a - * directive's element are destroyed. - * You should take this into consideration and make sure to always cancel the interval at the - * appropriate moment. See the example below for more details on how and when to do this. - *
- * - * @param {function()} fn A function that should be called repeatedly. - * @param {number} delay Number of milliseconds between each function call. - * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat - * indefinitely. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {promise} A promise which will be notified on each iteration. - * - * @example - * - * - * - * - *
- *
- *
- * Current time is: - *
- * Blood 1 : {{blood_1}} - * Blood 2 : {{blood_2}} - * - * - * - *
- *
- * - *
- *
- */ - function interval(fn, delay, count, invokeApply) { - var hasParams = arguments.length > 4, - args = hasParams ? sliceArgs(arguments, 4) : [], - setInterval = $window.setInterval, - clearInterval = $window.clearInterval, - iteration = 0, - skipApply = (isDefined(invokeApply) && !invokeApply), - deferred = (skipApply ? $$q : $q).defer(), - promise = deferred.promise; - - count = isDefined(count) ? count : 0; + if (!promise.hasOwnProperty('$$intervalId')) { + throw $intervalMinErr('badprom', + '`$interval.cancel()` called with a promise that was not generated by `$interval()`.'); + } - promise.then(null, null, (!hasParams) ? fn : function() { - fn.apply(null, args); - }); + if (!intervals.hasOwnProperty(promise.$$intervalId)) return false; - promise.$$intervalId = setInterval(function tick() { - deferred.notify(iteration++); + var id = promise.$$intervalId; + var deferred = intervals[id]; - if (count > 0 && iteration >= count) { - deferred.resolve(iteration); - clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - } + // Interval cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + clearIntervalFn(id); - if (!skipApply) $rootScope.$apply(); + return true; + }; - }, delay); + return interval; + }]; +} - intervals[promise.$$intervalId] = deferred; +/** @this */ +function $$IntervalFactoryProvider() { + this.$get = ['$browser', '$q', '$$q', '$rootScope', + function($browser, $q, $$q, $rootScope) { + return function intervalFactory(setIntervalFn, clearIntervalFn) { + return function intervalFn(fn, delay, count, invokeApply) { + var hasParams = arguments.length > 4, + args = hasParams ? sliceArgs(arguments, 4) : [], + iteration = 0, + skipApply = isDefined(invokeApply) && !invokeApply, + deferred = (skipApply ? $$q : $q).defer(), + promise = deferred.promise; + + count = isDefined(count) ? count : 0; + + function callback() { + if (!hasParams) { + fn(iteration); + } else { + fn.apply(null, args); + } + } - return promise; - } + function tick() { + if (skipApply) { + $browser.defer(callback); + } else { + $rootScope.$evalAsync(callback); + } + deferred.notify(iteration++); + if (count > 0 && iteration >= count) { + deferred.resolve(iteration); + clearIntervalFn(promise.$$intervalId); + } - /** - * @ngdoc method - * @name $interval#cancel - * - * @description - * Cancels a task associated with the `promise`. - * - * @param {Promise=} promise returned by the `$interval` function. - * @returns {boolean} Returns `true` if the task was successfully canceled. - */ - interval.cancel = function(promise) { - if (promise && promise.$$intervalId in intervals) { - intervals[promise.$$intervalId].reject('canceled'); - $window.clearInterval(promise.$$intervalId); - delete intervals[promise.$$intervalId]; - return true; - } - return false; - }; + if (!skipApply) $rootScope.$apply(); + } - return interval; + promise.$$intervalId = setIntervalFn(tick, delay, deferred, skipApply); + + return promise; + }; + }; }]; } +/** + * @ngdoc service + * @name $jsonpCallbacks + * @requires $window + * @description + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. + */ +var $jsonpCallbacksProvider = /** @this */ function() { + this.$get = function() { + var callbacks = angular.callbacks; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + + return { + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (callbacks.$$counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = callbacks[callbackId] = callback; + return callbackPath; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete callbacks[callback.id]; + delete callbackMap[callbackPath]; + } + }; + }; +}; + /** * @ngdoc service * @name $locale * * @description - * $locale service provides localization rules for various Angular components. As of right now the + * $locale service provides localization rules for various AngularJS components. As of right now the * only public api is: * * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +/* global stripHash: true */ + +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -11393,12 +14515,36 @@ function encodePath(path) { i = segments.length; while (i--) { - segments[i] = encodeUriSegment(segments[i]); + // decode forward slashes to prevent them from being double encoded + segments[i] = encodeUriSegment(segments[i].replace(/%2F/g, '/')); + } + + return segments.join('/'); +} + +function decodePath(path, html5Mode) { + var segments = path.split('/'), + i = segments.length; + + while (i--) { + segments[i] = decodeURIComponent(segments[i]); + if (html5Mode) { + // encode forward slashes to prevent them from being mistaken for path separators + segments[i] = segments[i].replace(/\//g, '%2F'); + } } return segments.join('/'); } +function normalizePath(pathValue, searchValue, hashValue) { + var search = toKeyValue(searchValue), + hash = hashValue ? '#' + encodeUriSegment(hashValue) : '', + path = encodePath(pathValue); + + return path + (search ? '?' + search : '') + hash; +} + function parseAbsoluteUrl(absoluteUrl, locationObj) { var parsedUrl = urlResolve(absoluteUrl); @@ -11407,49 +14553,51 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } +var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; +function parseAppUrl(url, locationObj, html5Mode) { -function parseAppUrl(relativeUrl, locationObj) { - var prefixed = (relativeUrl.charAt(0) !== '/'); + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } + + var prefixed = (url.charAt(0) !== '/'); if (prefixed) { - relativeUrl = '/' + relativeUrl; + url = '/' + url; } - var match = urlResolve(relativeUrl); - locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? - match.pathname.substring(1) : match.pathname); + var match = urlResolve(url); + var path = prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname; + locationObj.$$path = decodePath(path, html5Mode); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } +function startsWith(str, search) { + return str.slice(0, search.length) === search; +} /** * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. + * @param {string} base + * @param {string} url + * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with + * the expected string. */ -function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); +function stripBaseUrl(base, url) { + if (startsWith(url, base)) { + return url.substr(base.length); } } - function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); + 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); } @@ -11461,13 +14609,13 @@ function serverBase(url) { /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix + * @param {string} basePrefix URL path prefix */ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; @@ -11476,18 +14624,18 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); + var pathUrl = stripBaseUrl(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); } - parseAppUrl(pathUrl, this); + parseAppUrl(pathUrl, this, true); if (!this.$$path) { this.$$path = '/'; @@ -11496,16 +14644,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$compose(); }; - /** - * Compose url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + this.$$normalizeUrl = function(url) { + return appBaseNoFile + url.substr(1); // first char is always '/' }; this.$$parseLinkUrl = function(url, relHref) { @@ -11518,16 +14658,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; - if (isDefined(appUrl = beginsWith(appBase, url))) { + + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { prevAppUrl = appUrl; - if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) { - rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) { + rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } - } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) { + } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { + } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { @@ -11539,7 +14680,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * @@ -11554,19 +14695,19 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); var withoutHashUrl; if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - // The rest of the url starts with a hash so we have + // 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); + withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { // There was no hashbang prefix so we just have a hash fragment withoutHashUrl = withoutBaseUrl; @@ -11582,12 +14723,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { withoutHashUrl = ''; if (isUndefined(withoutBaseUrl)) { appBase = url; - this.replace(); + /** @type {?} */ (this).replace(); } } } - parseAppUrl(withoutHashUrl, this); + parseAppUrl(withoutHashUrl, this, false); this.$$path = removeWindowsDriveName(this.$$path, withoutHashUrl, appBase); @@ -11601,7 +14742,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { * * a.setAttribute('href', '/foo') * * a.pathname === '/C:/foo' //true * - * Inside of Angular, we're always using pathnames that + * Inside of AngularJS, we're always using pathnames that * do not include drive names for routing. */ function removeWindowsDriveName(path, url, base) { @@ -11614,7 +14755,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { var firstPathSegmentMatch; //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { + if (startsWith(url, base)) { url = url.replace(base, ''); } @@ -11628,20 +14769,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { } }; - /** - * Compose hashbang url and update `absUrl` property - * @private - */ - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; - this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + this.$$normalizeUrl = function(url) { + return appBase + (url ? hashPrefix + url : ''); }; this.$$parseLinkUrl = function(url, relHref) { - if (stripHash(appBase) == stripHash(url)) { + if (stripHash(appBase) === stripHash(url)) { this.$$parse(url); return true; } @@ -11651,7 +14784,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * @@ -11675,9 +14808,9 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var rewrittenUrl; var appUrl; - if (appBase == stripHash(url)) { + if (appBase === stripHash(url)) { rewrittenUrl = url; - } else if ((appUrl = beginsWith(appBaseNoFile, url))) { + } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBase + hashPrefix + appUrl; } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; @@ -11688,20 +14821,21 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { return !!rewrittenUrl; }; - this.$$compose = function() { - var search = toKeyValue(this.$$search), - hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; - - this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; + this.$$normalizeUrl = function(url) { // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' - this.$$absUrl = appBase + hashPrefix + this.$$url; + return appBase + hashPrefix + url; }; - } var locationPrototype = { + /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', + /** * Are we in html5 mode? * @private @@ -11714,6 +14848,16 @@ var locationPrototype = { */ $$replace: false, + /** + * Compose url and update `url` and `absUrl` property + * @private + */ + $$compose: function() { + this.$$url = normalizePath(this.$$path, this.$$search, this.$$hash); + this.$$absUrl = this.$$normalizeUrl(this.$$url); + this.$$urlUpdatedByLocation = true; + }, + /** * @ngdoc method * @name $location#absUrl @@ -11721,17 +14865,17 @@ var locationPrototype = { * @description * This method is getter only. * - * Return full url representation with all segments encoded according to rules specified in + * 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 + * // 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 + * @return {string} full URL */ absUrl: locationGetter('$$absUrl'), @@ -11742,18 +14886,18 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. * * Change path, search and hash, when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // 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=} url New URL without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { @@ -11776,16 +14920,16 @@ var locationPrototype = { * @description * This method is getter only. * - * Return protocol of current url. + * Return protocol of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * - * @return {string} protocol of current url + * @return {string} protocol of current URL */ protocol: locationGetter('$$protocol'), @@ -11796,24 +14940,24 @@ var locationPrototype = { * @description * This method is getter only. * - * Return host of current url. + * Return host of current URL. * - * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. + * Note: compared to the non-AngularJS version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var host = $location.host(); * // => "example.com" * - * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo * host = $location.host(); * // => "example.com" * host = location.host; * // => "example.com:8080" * ``` * - * @return {string} host of current url. + * @return {string} host of current URL. */ host: locationGetter('$$host'), @@ -11824,11 +14968,11 @@ var locationPrototype = { * @description * This method is getter only. * - * Return port of current url. + * Return port of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` @@ -11844,7 +14988,7 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return path of current url when called without any parameter. + * Return path of current URL when called without any parameter. * * Change path when called with parameter and return `$location`. * @@ -11853,17 +14997,17 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // 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 + * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter */ path: locationGetterSetter('$$path', function(path) { path = path !== null ? path.toString() : ''; - return path.charAt(0) == '/' ? path : '/' + path; + return path.charAt(0) === '/' ? path : '/' + path; }), /** @@ -11873,13 +15017,13 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return search part (as object) of current url when called without any parameter. + * Return search part (as object) of current URL when called without any parameter. * * Change search part when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * @@ -11895,7 +15039,7 @@ var locationPrototype = { * 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. + * as duplicate search parameters in the URL. * * @param {(string|Number|Array|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. @@ -11957,7 +15101,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` @@ -12018,6 +15162,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun // 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; + this.$$urlUpdatedByLocation = true; return this; }; @@ -12025,14 +15170,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { + return /** @this */ function(value) { if (isUndefined(value)) { return this[property]; } @@ -12074,11 +15219,13 @@ function locationGetterSetter(property, preprocess) { /** * @ngdoc provider * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -12089,6 +15236,7 @@ function $LocationProvider() { * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -12115,8 +15263,12 @@ function $LocationProvider() { * whether or not a 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. + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ @@ -12134,7 +15286,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -12194,7 +15346,7 @@ function $LocationProvider() { if (html5Mode.enabled) { if (!baseHref && html5Mode.requireBase) { throw $locationMinErr('nobase', - "$location in HTML5 mode requires a tag to be present!"); + '$location in HTML5 mode requires a tag to be present!'); } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; @@ -12211,6 +15363,13 @@ function $LocationProvider() { var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; + // Determine if two URLs are equal despite potentially having different encoding/normalizing + // such as $location.absUrl() vs $browser.url() + // See https://github.com/angular/angular.js/issues/16592 + function urlsEqual(a, b) { + return a === b || urlResolve(a).href === urlResolve(b).href; + } + function setBrowserUrlWithFallback(url, replace, state) { var oldUrl = $location.url(); var oldState = $location.$$state; @@ -12231,10 +15390,11 @@ function $LocationProvider() { } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); @@ -12244,6 +15404,8 @@ function $LocationProvider() { if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) 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 @@ -12260,15 +15422,13 @@ function $LocationProvider() { 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, + // We do a preventDefault for all urls that are part of the AngularJS 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 - if ($location.absUrl() != $browser.url()) { + 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; } } } @@ -12276,7 +15436,7 @@ function $LocationProvider() { // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { + if ($location.absUrl() !== initialUrl) { $browser.url($location.absUrl(), true); } @@ -12285,7 +15445,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -12295,7 +15455,6 @@ function $LocationProvider() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; - newUrl = trimEmptyHash(newUrl); $location.$$parse(newUrl); $location.$$state = newState; @@ -12320,36 +15479,40 @@ function $LocationProvider() { // update browser $rootScope.$watch(function $locationWatch() { - 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 || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; - if (initializing || urlOrStateChanged) { - initializing = false; + var oldUrl = $browser.url(); + var newUrl = $location.absUrl(); + var oldState = $browser.state(); + var currentReplace = $location.$$replace; + var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); - $rootScope.$evalAsync(function() { - var newUrl = $location.absUrl(); - var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - $location.$$state, oldState).defaultPrevented; + if (initializing || urlOrStateChanged) { + initializing = false; - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - } else { - if (urlOrStateChanged) { - setBrowserUrlWithFallback(newUrl, currentReplace, - oldState === $location.$$state ? null : $location.$$state); + // 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 { + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); } - afterLocationChange(oldUrl, oldState); - } - }); + }); + } } $location.$$replace = false; @@ -12378,11 +15541,19 @@ function $LocationProvider() { * * The main purpose of this service is to simplify debugging and troubleshooting. * + * To reveal the location of the calls to `$log` in the JavaScript console, + * you can "blackbox" the AngularJS source in your browser: + * + * [Mozilla description of blackboxing](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Black_box_a_source). + * [Chrome description of blackboxing](https://developer.chrome.com/devtools/docs/blackboxing). + * + * Note: Not all browsers support blackboxing. + * * The default is to log `debug` messages. You can use * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - + angular.module('logExample', []) .controller('LogController', ['$scope', '$log', function($scope, $log) { @@ -12408,6 +15579,8 @@ function $LocationProvider() { /** * @ngdoc provider * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ @@ -12425,13 +15598,22 @@ function $LogProvider() { this.debugEnabled = function(flag) { if (isDefined(flag)) { debug = flag; - return this; + return this; } else { return debug; } }; this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -12484,12 +15666,12 @@ function $LogProvider() { fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { - if (arg instanceof Error) { - if (arg.stack) { + if (isError(arg)) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; @@ -12502,29 +15684,17 @@ function $LogProvider() { function consoleLog(type) { var console = $window.console || {}, - logFn = console[type] || console.log || noop, - hasApply = false; - - // Note: reading logFn.apply throws an error in IE11 in IE8 document mode. - // The reason behind this is that console.log has type "object" in IE8... - try { - hasApply = !!logFn.apply; - } catch (e) {} - - if (hasApply) { - return function() { - var args = []; - forEach(arguments, function(arg) { - args.push(formatError(arg)); - }); - return logFn.apply(console, args); - }; - } + logFn = console[type] || console.log || noop; - // we are IE which either doesn't have window.console => this is noop and we do nothing, - // or we are IE where console.log doesn't have apply so we log at least first 2 args - return function(arg1, arg2) { - logFn(arg1, arg2 == null ? '' : arg2); + return function() { + var args = []; + forEach(arguments, function(arg) { + args.push(formatError(arg)); + }); + // Support: IE 9 only + // console methods don't inherit from Function.prototype in IE 9 so we can't + // call `logFn.apply(console, args)` directly. + return Function.prototype.apply.call(logFn, console, args); }; } }]; @@ -12543,118 +15713,45 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); -// Sandboxing Angular Expressions +var objectValueOf = {}.constructor.prototype.valueOf; + +// Sandboxing AngularJS 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 -// obtaining a reference to native JS functions such as the Function constructor. +// AngularJS expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // -// As an example, consider the following Angular expression: +// As an example, consider the following AngularJS expression: // // {}.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 -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// 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. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === "__defineGetter__" || name === "__defineSetter__" - || name === "__lookupGetter__" || name === "__lookupSetter__" - || name === "__proto__") { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function getStringValue(name, fullExpression) { - // From the JavaScript docs: +function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted // into a string via the toString method. + // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names // - // So, to ensure that we are checking the same `name` that JavaScript would use, - // we cast it to a string, if possible. - // Doing `name + ''` can cause a repl error if the result to `toString` is not a string, - // this is, this will handle objects that misbehave. - name = name + ''; - if (!isString(name)) { - throw $parseMinErr('iseccst', - 'Cannot convert object to primitive value! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - 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 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); - } - } + // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it + // to a string. It's not always possible. If `name` is an object and its `toString` method is + // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: + // + // TypeError: Cannot convert object to primitive value + // + // For performance reasons, we don't catch this error here and allow it to propagate up the call + // stack. Note that you'll get the same error in JavaScript if you try to access a property using + // such a 'broken' object as a key. + return name + ''; } -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor || - obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -12663,7 +15760,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function(options) { +var Lexer = function Lexer(options) { this.options = options; }; @@ -12677,11 +15774,11 @@ Lexer.prototype = { while (this.index < this.text.length) { var ch = this.text.charAt(this.index); - if (ch === '"' || ch === "'") { + if (ch === '"' || ch === '\'') { this.readString(ch); } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(ch)) { + } else if (this.isIdentifierStart(this.peekMultichar())) { this.readIdent(); } else if (this.is(ch, '(){}[].,;:?')) { this.tokens.push({index: this.index, text: ch}); @@ -12716,7 +15813,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9') && typeof ch === "string"; + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { @@ -12725,12 +15822,48 @@ Lexer.prototype = { ch === '\n' || ch === '\v' || ch === '\u00A0'); }, - isIdent: function(ch) { + isIdentifierStart: function(ch) { + return this.options.isIdentifierStart ? + this.options.isIdentifierStart(ch, this.codePointAt(ch)) : + this.isValidIdentifierStart(ch); + }, + + isValidIdentifierStart: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' === ch || ch === '$'); }, + isIdentifierContinue: function(ch) { + return this.options.isIdentifierContinue ? + this.options.isIdentifierContinue(ch, this.codePointAt(ch)) : + this.isValidIdentifierContinue(ch); + }, + + isValidIdentifierContinue: function(ch, cp) { + return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); + }, + + codePointAt: function(ch) { + if (ch.length === 1) return ch.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; + }, + + peekMultichar: function() { + var ch = this.text.charAt(this.index); + var peek = this.peek(); + if (!peek) { + return ch; + } + var cp1 = ch.charCodeAt(0); + var cp2 = peek.charCodeAt(0); + if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) { + return ch + peek; + } + return ch; + }, + isExpOperator: function(ch) { return (ch === '-' || ch === '+' || this.isNumber(ch)); }, @@ -12749,19 +15882,19 @@ Lexer.prototype = { var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -12779,12 +15912,13 @@ Lexer.prototype = { readIdent: function() { var start = this.index; + this.index += this.peekMultichar().length; while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - if (!(this.isIdent(ch) || this.isNumber(ch))) { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { break; } - this.index++; + this.index += ch.length; } this.tokens.push({ index: start, @@ -12835,7 +15969,7 @@ Lexer.prototype = { } }; -var AST = function(lexer, options) { +var AST = function AST(lexer, options) { this.lexer = lexer; this.options = options; }; @@ -12855,6 +15989,7 @@ AST.ArrayExpression = 'ArrayExpression'; AST.Property = 'Property'; AST.ObjectExpression = 'ObjectExpression'; AST.ThisExpression = 'ThisExpression'; +AST.LocalsExpression = 'LocalsExpression'; // Internal use only AST.NGValueParameter = 'NGValueParameter'; @@ -12890,8 +16025,7 @@ AST.prototype = { filterChain: function() { var left = this.expression(); - var token; - while ((token = this.expect('|'))) { + while (this.expect('|')) { left = this.filter(left); } return left; @@ -12904,6 +16038,10 @@ AST.prototype = { assignment: function() { var result = this.ternary(); if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } return result; @@ -12993,8 +16131,10 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -13035,7 +16175,7 @@ AST.prototype = { var args = []; if (this.peekToken().text !== ')') { do { - args.push(this.expression()); + args.push(this.filterChain()); } while (this.expect(',')); } return args; @@ -13081,13 +16221,28 @@ AST.prototype = { property = {type: AST.Property, kind: 'init'}; if (this.peek().constant) { property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); } else if (this.peek().identifier) { property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); } else { - this.throwError("invalid key", this.peek()); + this.throwError('invalid key', this.peek()); } - this.consume(':'); - property.value = this.expression(); properties.push(property); } while (this.expect(',')); } @@ -13146,16 +16301,9 @@ AST.prototype = { return false; }, - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, - 'this': {type: AST.ThisExpression } + selfReferential: { + 'this': {type: AST.ThisExpression }, + '$locals': {type: AST.LocalsExpression } } }; @@ -13174,14 +16322,47 @@ function isStateless($filter, filterName) { return !fn.$stateful; } -function findConstantAndWatchExpressions(ast, $filter) { +var PURITY_ABSOLUTE = 1; +var PURITY_RELATIVE = 2; + +// Detect nodes which could depend on non-shallow state of objects +function isPure(node, parentIsPure) { + switch (node.type) { + // Computed members might invoke a stateful toString() + case AST.MemberExpression: + if (node.computed) { + return false; + } + break; + + // Unary always convert to primative + case AST.UnaryExpression: + return PURITY_ABSOLUTE; + + // The binary + operator can invoke a stateful toString(). + case AST.BinaryExpression: + return node.operator !== '+' ? PURITY_ABSOLUTE : false; + + // Functions / filters probably read state from within objects + case AST.CallExpression: + return false; + } + + return (undefined === parentIsPure) ? PURITY_RELATIVE : parentIsPure; +} + +function findConstantAndWatchExpressions(ast, $filter, parentIsPure) { var allConstants; var argsToWatch; + var isStatelessFilter; + + var astIsPure = ast.isPure = isPure(ast, parentIsPure); + switch (ast.type) { case AST.Program: allConstants = true; forEach(ast.body, function(expr) { - findConstantAndWatchExpressions(expr.expression, $filter); + findConstantAndWatchExpressions(expr.expression, $filter, astIsPure); allConstants = allConstants && expr.expression.constant; }); ast.constant = allConstants; @@ -13191,26 +16372,26 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = []; break; case AST.UnaryExpression: - findConstantAndWatchExpressions(ast.argument, $filter); + findConstantAndWatchExpressions(ast.argument, $filter, astIsPure); ast.constant = ast.argument.constant; ast.toWatch = ast.argument.toWatch; break; case AST.BinaryExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch); break; case AST.LogicalExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = ast.constant ? [] : [ast]; break; case AST.ConditionalExpression: - findConstantAndWatchExpressions(ast.test, $filter); - findConstantAndWatchExpressions(ast.alternate, $filter); - findConstantAndWatchExpressions(ast.consequent, $filter); + findConstantAndWatchExpressions(ast.test, $filter, astIsPure); + findConstantAndWatchExpressions(ast.alternate, $filter, astIsPure); + findConstantAndWatchExpressions(ast.consequent, $filter, astIsPure); ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant; ast.toWatch = ast.constant ? [] : [ast]; break; @@ -13219,29 +16400,28 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = [ast]; break; case AST.MemberExpression: - findConstantAndWatchExpressions(ast.object, $filter); + findConstantAndWatchExpressions(ast.object, $filter, astIsPure); if (ast.computed) { - findConstantAndWatchExpressions(ast.property, $filter); + findConstantAndWatchExpressions(ast.property, $filter, astIsPure); } ast.constant = ast.object.constant && (!ast.computed || ast.property.constant); - ast.toWatch = [ast]; + ast.toWatch = ast.constant ? [] : [ast]; break; case AST.CallExpression: - allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; argsToWatch = []; forEach(ast.arguments, function(expr) { - findConstantAndWatchExpressions(expr, $filter); + findConstantAndWatchExpressions(expr, $filter, astIsPure); allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } + argsToWatch.push.apply(argsToWatch, expr.toWatch); }); ast.constant = allConstants; - ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; break; case AST.AssignmentExpression: - findConstantAndWatchExpressions(ast.left, $filter); - findConstantAndWatchExpressions(ast.right, $filter); + findConstantAndWatchExpressions(ast.left, $filter, astIsPure); + findConstantAndWatchExpressions(ast.right, $filter, astIsPure); ast.constant = ast.left.constant && ast.right.constant; ast.toWatch = [ast]; break; @@ -13249,11 +16429,9 @@ function findConstantAndWatchExpressions(ast, $filter) { allConstants = true; argsToWatch = []; forEach(ast.elements, function(expr) { - findConstantAndWatchExpressions(expr, $filter); + findConstantAndWatchExpressions(expr, $filter, astIsPure); allConstants = allConstants && expr.constant; - if (!expr.constant) { - argsToWatch.push.apply(argsToWatch, expr.toWatch); - } + argsToWatch.push.apply(argsToWatch, expr.toWatch); }); ast.constant = allConstants; ast.toWatch = argsToWatch; @@ -13262,10 +16440,14 @@ function findConstantAndWatchExpressions(ast, $filter) { allConstants = true; argsToWatch = []; forEach(ast.properties, function(property) { - findConstantAndWatchExpressions(property.value, $filter); + findConstantAndWatchExpressions(property.value, $filter, astIsPure); allConstants = allConstants && property.value.constant; - if (!property.value.constant) { - argsToWatch.push.apply(argsToWatch, property.value.toWatch); + argsToWatch.push.apply(argsToWatch, property.value.toWatch); + if (property.computed) { + //`{[key]: value}` implicitly does `key.toString()` which may be non-pure + findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false); + allConstants = allConstants && property.key.constant; + argsToWatch.push.apply(argsToWatch, property.key.toWatch); } }); ast.constant = allConstants; @@ -13275,11 +16457,15 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.constant = false; ast.toWatch = []; break; + case AST.LocalsExpression: + ast.constant = false; + ast.toWatch = []; + break; } } function getInputs(body) { - if (body.length != 1) return; + if (body.length !== 1) return; var lastExpression = body[0].expression; var candidate = lastExpression.toWatch; if (candidate.length !== 1) return candidate; @@ -13308,19 +16494,16 @@ function isConstant(ast) { return ast.constant; } -function ASTCompiler(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTCompiler($filter) { this.$filter = $filter; } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); this.state = { nextId: 0, filters: {}, - expensiveChecks: expensiveChecks, fn: {vars: [], body: [], own: {}}, assign: {vars: [], body: [], own: {}}, inputs: [] @@ -13345,7 +16528,7 @@ ASTCompiler.prototype = { var intoId = self.nextId(); self.recurse(watch, intoId); self.return_(intoId); - self.state.inputs.push(fnKey); + self.state.inputs.push({name: fnKey, isPure: watch.isPure}); watch.watchId = key; }); this.state.computing = 'fn'; @@ -13361,30 +16544,17 @@ ASTCompiler.prototype = { this.watchFns() + 'return fn;'; - /* jshint -W054 */ + // eslint-disable-next-line no-new-func var fn = (new Function('$filter', - 'ensureSafeMemberName', - 'ensureSafeObject', - 'ensureSafeFunction', 'getStringValue', - 'ensureSafeAssignContext', 'ifDefined', 'plus', - 'text', fnString))( this.$filter, - ensureSafeMemberName, - ensureSafeObject, - ensureSafeFunction, getStringValue, - ensureSafeAssignContext, ifDefined, - plusFn, - expression); - /* jshint +W054 */ + plusFn); this.state = this.stage = undefined; - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, @@ -13394,13 +16564,16 @@ ASTCompiler.prototype = { watchFns: function() { var result = []; - var fns = this.state.inputs; + var inputs = this.state.inputs; var self = this; - forEach(fns, function(name) { - result.push('var ' + name + '=' + self.generateFunction(name, 's')); + forEach(inputs, function(input) { + result.push('var ' + input.name + '=' + self.generateFunction(input.name, 's')); + if (input.isPure) { + result.push(input.name, '.isPure=' + JSON.stringify(input.isPure) + ';'); + } }); - if (fns.length) { - result.push('fn.inputs=[' + fns.join(',') + '];'); + if (inputs.length) { + result.push('fn.inputs=[' + inputs.map(function(i) { return i.name; }).join(',') + '];'); } return result.join(''); }, @@ -13431,7 +16604,7 @@ ASTCompiler.prototype = { }, recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var left, right, self = this, args, expression; + var left, right, self = this, args, expression, computed; recursionFn = recursionFn || noop; if (!skipWatchIdCheck && isDefined(ast.watchId)) { intoId = intoId || this.nextId(); @@ -13455,7 +16628,7 @@ ASTCompiler.prototype = { case AST.Literal: expression = this.escape(ast.value); this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); @@ -13495,22 +16668,18 @@ ASTCompiler.prototype = { nameId.computed = false; nameId.name = ast.name; } - ensureSafeMemberName(ast.name); self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)), function() { self.if_(self.stage === 'inputs' || 's', function() { if (create && create !== 1) { self.if_( - self.not(self.nonComputedMember('s', ast.name)), + self.isNull(self.nonComputedMember('s', ast.name)), self.lazyAssign(self.nonComputedMember('s', ast.name), '{}')); } self.assign(intoId, self.nonComputedMember('s', ast.name)); }); }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name)) ); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) { - self.addEnsureSafeObject(intoId); - } recursionFn(intoId); break; case AST.MemberExpression: @@ -13522,25 +16691,20 @@ ASTCompiler.prototype = { right = self.nextId(); self.recurse(ast.property, right); self.getStringValue(right); - self.addEnsureSafeMemberName(right); if (create && create !== 1) { self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}')); } - expression = self.ensureSafeObject(self.computedMember(left, right)); + expression = self.computedMember(left, right); self.assign(intoId, expression); if (nameId) { nameId.computed = true; nameId.name = right; } } else { - ensureSafeMemberName(ast.property.name); if (create && create !== 1) { - self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); + self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}')); } expression = self.nonComputedMember(left, ast.property.name); - if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) { - expression = self.ensureSafeObject(expression); - } self.assign(intoId, expression); if (nameId) { nameId.computed = false; @@ -13572,21 +16736,16 @@ ASTCompiler.prototype = { args = []; self.recurse(ast.callee, right, left, function() { self.if_(self.notNull(right), function() { - self.addEnsureSafeFunction(right); forEach(ast.arguments, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { - args.push(self.ensureSafeObject(argument)); + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(argument); }); }); if (left.name) { - if (!self.state.expensiveChecks) { - self.addEnsureSafeObject(left.context); - } expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')'; } else { expression = right + '(' + args.join(',') + ')'; } - expression = self.ensureSafeObject(expression); self.assign(intoId, expression); }, function() { self.assign(intoId, 'undefined'); @@ -13598,14 +16757,9 @@ ASTCompiler.prototype = { case AST.AssignmentExpression: right = this.nextId(); left = {}; - if (!isAssignable(ast.left)) { - throw $parseMinErr('lval', 'Trying to assing a value to a non l-value'); - } this.recurse(ast.left, undefined, left, function() { self.if_(self.notNull(left.context), function() { self.recurse(ast.right, right); - self.addEnsureSafeObject(self.member(left.context, left.name, left.computed)); - self.addEnsureSafeAssignContext(left.context); expression = self.member(left.context, left.name, left.computed) + ast.operator + right; self.assign(intoId, expression); recursionFn(intoId || expression); @@ -13615,35 +16769,63 @@ ASTCompiler.prototype = { case AST.ArrayExpression: args = []; forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { args.push(argument); }); }); expression = '[' + args.join(',') + ']'; this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.ObjectExpression: args = []; + computed = false; forEach(ast.properties, function(property) { - self.recurse(property.value, self.nextId(), undefined, function(expr) { - args.push(self.escape( - property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + - ':' + expr); - }); + if (property.computed) { + computed = true; + } }); - expression = '{' + args.join(',') + '}'; - this.assign(intoId, expression); - recursionFn(expression); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } + recursionFn(intoId || expression); break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -13702,12 +16884,22 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, nonComputedMember: function(left, right) { - return left + '.' + right; + var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; + var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; + if (SAFE_IDENTIFIER.test(right)) { + return left + '.' + right; + } else { + return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; + } }, computedMember: function(left, right) { @@ -13719,40 +16911,8 @@ ASTCompiler.prototype = { return this.nonComputedMember(left, right); }, - addEnsureSafeObject: function(item) { - this.current().body.push(this.ensureSafeObject(item), ';'); - }, - - addEnsureSafeMemberName: function(item) { - this.current().body.push(this.ensureSafeMemberName(item), ';'); - }, - - addEnsureSafeFunction: function(item) { - this.current().body.push(this.ensureSafeFunction(item), ';'); - }, - - addEnsureSafeAssignContext: function(item) { - this.current().body.push(this.ensureSafeAssignContext(item), ';'); - }, - - ensureSafeObject: function(item) { - return 'ensureSafeObject(' + item + ',text)'; - }, - - ensureSafeMemberName: function(item) { - return 'ensureSafeMemberName(' + item + ',text)'; - }, - - ensureSafeFunction: function(item) { - return 'ensureSafeFunction(' + item + ',text)'; - }, - getStringValue: function(item) { - this.assign(item, 'getStringValue(' + item + ',text)'); - }, - - ensureSafeAssignContext: function(item) { - return 'ensureSafeAssignContext(' + item + ',text)'; + this.assign(item, 'getStringValue(' + item + ')'); }, lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { @@ -13776,7 +16936,7 @@ ASTCompiler.prototype = { }, escape: function(value) { - if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\''; if (isNumber(value)) return value.toString(); if (value === true) return 'true'; if (value === false) return 'false'; @@ -13800,17 +16960,13 @@ ASTCompiler.prototype = { }; -function ASTInterpreter(astBuilder, $filter) { - this.astBuilder = astBuilder; +function ASTInterpreter($filter) { this.$filter = $filter; } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(ast) { var self = this; - var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -13823,6 +16979,7 @@ ASTInterpreter.prototype = { inputs = []; forEach(toWatch, function(watch, key) { var input = self.recurse(watch); + input.isPure = watch.isPure; watch.input = input; inputs.push(input); watch.watchId = key; @@ -13832,7 +16989,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? function() {} : + var fn = ast.body.length === 0 ? noop : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -13849,13 +17006,11 @@ ASTInterpreter.prototype = { if (inputs) { fn.inputs = inputs; } - fn.literal = isLiteral(ast); - fn.constant = isConstant(ast); return fn; }, recurse: function(ast, context, create) { - var left, right, self = this, args, expression; + var left, right, self = this, args; if (ast.input) { return this.inputs(ast.input, ast.watchId); } @@ -13881,20 +17036,16 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); + return self.identifier(ast.name, context, create); case AST.MemberExpression: left = this.recurse(ast.object, false, !!create); if (!ast.computed) { - ensureSafeMemberName(ast.property.name, self.expression); right = ast.property.name; } if (ast.computed) right = this.recurse(ast.property); return ast.computed ? - this.computedMember(left, right, context, create, self.expression) : - this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression); + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -13915,13 +17066,11 @@ ASTInterpreter.prototype = { var rhs = right(scope, locals, assign, inputs); var value; if (rhs.value != null) { - ensureSafeObject(rhs.context, self.expression); - ensureSafeFunction(rhs.value, self.expression); var values = []; for (var i = 0; i < args.length; ++i) { - values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -13931,8 +17080,6 @@ ASTInterpreter.prototype = { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs = right(scope, locals, assign, inputs); - ensureSafeObject(lhs.value, self.expression); - ensureSafeAssignContext(lhs.context); lhs.context[lhs.name] = rhs; return context ? {value: rhs} : rhs; }; @@ -13951,16 +17098,28 @@ ASTInterpreter.prototype = { case AST.ObjectExpression: args = []; forEach(ast.properties, function(property) { - args.push({key: property.key.type === AST.Identifier ? - property.key.name : - ('' + property.key.value), - value: self.recurse(property.value) - }); + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } }); return function(scope, locals, assign, inputs) { var value = {}; for (var i = 0; i < args.length; ++i) { - value[args[i].key] = args[i].value(scope, locals, assign, inputs); + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } } return context ? {value: value} : value; }; @@ -13968,8 +17127,12 @@ ASTInterpreter.prototype = { return function(scope) { return context ? {value: scope} : scope; }; + case AST.LocalsExpression: + return function(scope, locals) { + return context ? {value: locals} : locals; + }; case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { + return function(scope, locals, assign) { return context ? {value: assign} : assign; }; } @@ -13992,7 +17155,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -14051,12 +17214,14 @@ ASTInterpreter.prototype = { }, 'binary==': function(left, right, context) { return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs); return context ? {value: arg} : arg; }; }, 'binary!=': function(left, right, context) { return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); return context ? {value: arg} : arg; }; @@ -14106,16 +17271,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -14123,7 +17285,7 @@ ASTInterpreter.prototype = { } }; }, - computedMember: function(left, right, context, create, expression) { + computedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs; @@ -14131,12 +17293,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); - if (create && create !== 1 && lhs && !(lhs[rhs])) { - lhs[rhs] = {}; + if (create && create !== 1) { + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -14145,16 +17307,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); - if (create && create !== 1 && lhs && !(lhs[right])) { - lhs[right] = {}; + if (create && create !== 1) { + if (lhs && lhs[right] == null) { + lhs[right] = {}; + } } var value = lhs != null ? lhs[right] : undefined; - if (expensiveChecks || isPossiblyDangerousMemberName(right)) { - ensureSafeObject(value, expression); - } if (context) { return {context: lhs, name: right, value: value}; } else { @@ -14173,31 +17334,38 @@ ASTInterpreter.prototype = { /** * @constructor */ -var Parser = function(lexer, $filter, options) { - this.lexer = lexer; - this.$filter = $filter; - this.options = options; - this.ast = new AST(this.lexer); - this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : - new ASTCompiler(this.ast, $filter); -}; +function Parser(lexer, $filter, options) { + this.ast = new AST(lexer, options); + this.astCompiler = options.csp ? new ASTInterpreter($filter) : + new ASTCompiler($filter); +} Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); - } -}; - -var getterFnCacheDefault = createMap(); -var getterFnCacheExpensive = createMap(); + var ast = this.getAst(text); + var fn = this.astCompiler.compile(ast.ast); + fn.literal = isLiteral(ast.ast); + fn.constant = isConstant(ast.ast); + fn.oneTime = ast.oneTime; + return fn; + }, -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; -} + getAst: function(exp) { + var oneTime = false; + exp = exp.trim(); -var objectValueOf = Object.prototype.valueOf; + if (exp.charAt(0) === ':' && exp.charAt(1) === ':') { + oneTime = true; + exp = exp.substring(2); + } + return { + ast: this.ast.ast(exp), + oneTime: oneTime + }; + } +}; function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); @@ -14212,15 +17380,15 @@ function getValueOf(value) { * * @description * - * Converts Angular {@link guide/expression expression} into a function. + * Converts AngularJS {@link guide/expression expression} into a function. * * ```js * var getter = $parse('user.name'); * var setter = getter.assign; - * var context = {user:{name:'angular'}}; + * var context = {user:{name:'AngularJS'}}; * var locals = {user:{name:'local'}}; * - * expect(getter(context)).toEqual('angular'); + * expect(getter(context)).toEqual('AngularJS'); * setter(context, 'newValue'); * expect(context.user.name).toEqual('newValue'); * expect(getter(context, locals)).toEqual('local'); @@ -14249,55 +17417,94 @@ function getValueOf(value) { /** * @ngdoc provider * @name $parseProvider + * @this * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + var identStart, identContinue; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; + + /** + * @ngdoc method + * @name $parseProvider#setIdentifierFns + * + * @description + * + * Allows defining the set of characters that are allowed in AngularJS expressions. The function + * `identifierStart` will get called to know if a given character is a valid character to be the + * first character for an identifier. The function `identifierContinue` will get called to know if + * a given character is a valid character to be a follow-up identifier character. The functions + * `identifierStart` and `identifierContinue` will receive as arguments the single character to be + * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in + * mind that the `string` parameter can be two characters long depending on the character + * representation. It is expected for the function to return `true` or `false`, whether that + * character is allowed or not. + * + * Since this function will be called extensively, keep the implementation of these functions fast, + * as the performance of these functions have a direct impact on the expressions parsing speed. + * + * @param {function=} identifierStart The function that will decide whether the given character is + * a valid identifier start character. + * @param {function=} identifierContinue The function that will decide whether the given character is + * a valid identifier continue character. + */ + this.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; + }; this.$get = ['$filter', function($filter) { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue }; + $parse.$$getAst = $$getAst; + return $parse; - return function $parse(exp, interceptorFn, expensiveChecks) { - var parsedExpression, oneTime, cacheKey; + function $parse(exp, interceptorFn) { + var parsedExpression, cacheKey; switch (typeof exp) { case 'string': exp = exp.trim(); cacheKey = exp; - 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); + 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) { - parsedExpression.$$watchDelegate = parsedExpression.literal ? - oneTimeLiteralWatchDelegate : oneTimeWatchDelegate; - } else if (parsedExpression.inputs) { - parsedExpression.$$watchDelegate = inputsWatchDelegate; - } - cache[cacheKey] = parsedExpression; + + cache[cacheKey] = addWatchDelegate(parsedExpression); } return addInterceptor(parsedExpression, interceptorFn); @@ -14305,11 +17512,17 @@ function $ParseProvider() { return addInterceptor(exp, interceptorFn); default: - return noop; + return addInterceptor(noop, interceptorFn); } - }; + } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { + function $$getAst(exp) { + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); + return parser.getAst(exp).ast; + } + + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { if (newValue == null || oldValueOfValue == null) { // null/undefined return newValue === oldValueOfValue; @@ -14322,7 +17535,7 @@ function $ParseProvider() { // be cheaply dirty-checked newValue = getValueOf(newValue); - if (typeof newValue === 'object') { + if (typeof newValue === 'object' && !compareObjectIdentity) { // objects/arrays are not supported - deep-watching them would be too expensive return false; } @@ -14331,6 +17544,7 @@ function $ParseProvider() { } //Primitive or NaN + // eslint-disable-next-line no-self-compare return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } @@ -14343,7 +17557,7 @@ function $ParseProvider() { inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, inputExpressions.isPure)) { lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); oldInputValueOf = newInputValue && getValueOf(newInputValue); } @@ -14363,7 +17577,7 @@ function $ParseProvider() { for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); - if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], inputExpressions[i].isPure))) { oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } @@ -14377,95 +17591,127 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { + var isDone = parsedExpression.literal ? isAllDefined : isDefined; 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); + + var exp = parsedExpression.$$intercepted || parsedExpression; + var post = parsedExpression.$$interceptor || identity; + + var useInputs = parsedExpression.inputs && !exp.inputs; + + // Propogate the literal/inputs/constant attributes + // ... but not oneTime since we are handling it + oneTimeWatch.literal = parsedExpression.literal; + oneTimeWatch.constant = parsedExpression.constant; + oneTimeWatch.inputs = parsedExpression.inputs; + + // Allow other delegates to run on this wrapped expression + addWatchDelegate(oneTimeWatch); + + unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression); + + return unwatch; + + function unwatchIfDone() { + if (isDone(lastValue)) { + unwatch(); } - if (isDefined(value)) { - scope.$$postDigest(function() { - if (isDefined(lastValue)) { - unwatch(); - } - }); + } + + function oneTimeWatch(scope, locals, assign, inputs) { + lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs); + if (isDone(lastValue)) { + scope.$$postDigest(unwatchIfDone); } - }, objectEquality); + return post(lastValue); + } } - function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { + 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 = scope.$watch(function constantWatch(scope) { + unwatch(); 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); + }, listener, objectEquality); + return unwatch; + } - function isAllDefined(value) { - var allDefined = true; - forEach(value, function(val) { - if (!isDefined(val)) allDefined = false; - }); - return allDefined; + function addWatchDelegate(parsedExpression) { + if (parsedExpression.constant) { + parsedExpression.$$watchDelegate = constantWatchDelegate; + } else if (parsedExpression.oneTime) { + parsedExpression.$$watchDelegate = oneTimeWatchDelegate; + } else if (parsedExpression.inputs) { + parsedExpression.$$watchDelegate = inputsWatchDelegate; } + + return parsedExpression; } - 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 chainInterceptors(first, second) { + function chainedInterceptor(value) { + return second(first(value)); + } + chainedInterceptor.$stateful = first.$stateful || second.$stateful; + chainedInterceptor.$$pure = first.$$pure && second.$$pure; + + return chainedInterceptor; } function addInterceptor(parsedExpression, interceptorFn) { if (!interceptorFn) return parsedExpression; - var watchDelegate = parsedExpression.$$watchDelegate; - var useInputs = false; - var regularWatch = - watchDelegate !== oneTimeLiteralWatchDelegate && - watchDelegate !== oneTimeWatchDelegate; + // Extract any existing interceptors out of the parsedExpression + // to ensure the original parsedExpression is always the $$intercepted + if (parsedExpression.$$interceptor) { + interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn); + parsedExpression = parsedExpression.$$intercepted; + } + + var useInputs = false; - var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) { + var fn = function interceptedExpression(scope, locals, assign, inputs) { var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs); - return interceptorFn(value, scope, locals); - } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) { - var value = parsedExpression(scope, locals, assign, inputs); - var result = interceptorFn(value, scope, locals); - // we only return the interceptor's result if the - // initial value is defined (for bind-once) - return isDefined(value) ? result : value; + return interceptorFn(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; + // Maintain references to the interceptor/intercepted + fn.$$intercepted = parsedExpression; + fn.$$interceptor = interceptorFn; + + // Propogate the literal/oneTime/constant attributes + fn.literal = parsedExpression.literal; + fn.oneTime = parsedExpression.oneTime; + fn.constant = parsedExpression.constant; + + // Treat the interceptor like filters. + // If it is not $stateful then only watch its inputs. + // If the expression itself has no inputs then use the full expression as an input. + if (!interceptorFn.$stateful) { useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; + + if (!interceptorFn.$$pure) { + fn.inputs = fn.inputs.map(function(e) { + // Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a + // non-pure interceptor function. + if (e.isPure === PURITY_RELATIVE) { + return function depurifier(s) { return e(s); }; + } + return e; + }); + } } - return fn; + return addWatchDelegate(fn); } }]; } @@ -14479,19 +17725,19 @@ function $ParseProvider() { * 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). + * This is a [Promises/A+](https://promisesaplus.com/)-compliant 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. + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. * - * # $q constructor + * ## $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, + * function as the first argument. This is similar to the native Promise implementation from ES6, * 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 + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are * available yet. * * It can be used like so: @@ -14523,7 +17769,7 @@ function $ParseProvider() { * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * - * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise. + * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. * * However, the more traditional CommonJS-style usage is still available, and documented below. * @@ -14573,7 +17819,7 @@ 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 + * ## The Deferred API * * A new instance of deferred is constructed by calling `$q.defer()`. * @@ -14595,7 +17841,7 @@ function $ParseProvider() { * - promise – `{Promise}` – promise object associated with this deferred. * * - * # The Promise API + * ## The Promise API * * A new promise instance is created when a deferred instance is created and can be retrieved by * calling `deferred.promise`. @@ -14605,7 +17851,7 @@ function $ParseProvider() { * * **Methods** * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously * as soon as the result is available. The callbacks are called with a single argument: the result * or rejection reason. Additionally, the notify callback may be called zero or more times to @@ -14616,7 +17862,8 @@ function $ParseProvider() { * with the value which is resolved in that promise using * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). * It also notifies via the return value of the `notifyCallback` method. The promise cannot be - * resolved or rejected from the notifyCallback method. + * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback + * arguments are optional. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * @@ -14626,7 +17873,7 @@ function $ParseProvider() { * specification](https://github.com/kriskowal/q/wiki/API-Reference#promisefinallycallback) for * more information. * - * # Chaining promises + * ## Chaining promises * * Because calling the `then` method of a promise returns a new derived promise, it is easily * possible to create a chain of promises: @@ -14646,17 +17893,17 @@ function $ParseProvider() { * $http's response interceptors. * * - * # Differences between Kris Kowal's Q and $q + * ## Differences between Kris Kowal's Q and $q * * There are two main differences: * * - $q is integrated with the {@link ng.$rootScope.Scope} Scope model observation - * mechanism in angular, which means faster propagation of resolution or rejection into your + * mechanism in AngularJS, which means faster propagation of resolution or rejection into your * models and avoiding unnecessary browser repaints, which would result in flickering UI. * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. * - * # Testing + * ## Testing * * ```js * it('should simulate promise', inject(function($q, $rootScope) { @@ -14686,21 +17933,61 @@ function $ParseProvider() { * * @returns {Promise} The newly created promise. */ +/** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } +/** @this */ function $$QProvider() { + var errorOnUnhandledRejections = true; this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @@ -14709,22 +17996,14 @@ function $$QProvider() { * @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. + * @param {boolean=} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ -function qFactory(nextTick, exceptionHandler) { +function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { 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)]; - } + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -14736,9 +18015,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { + function defer() { return new Deferred(); - }; + } + + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } + function Promise() { this.$$state = { status: 0 }; @@ -14749,137 +18037,166 @@ function qFactory(nextTick, exceptionHandler) { if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { return this; } - var result = new Deferred(); + var result = new Promise(); 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; + return result; }, - "catch": function(callback) { + 'catch': function(callback) { return this.then(null, callback); }, - "finally": function(callback, progressBack) { + 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + return handleCallback(error, reject, callback); }, progressBack); } }); - //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); - }; - } - function processQueue(state) { - var fn, deferred, pending; + var fn, promise, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; - for (var i = 0, ii = pending.length; i < ii; ++i) { - deferred = pending[i][0]; - fn = pending[i][state.status]; - try { - if (isFunction(fn)) { - deferred.resolve(fn(state.value)); - } else if (state.status === 1) { - deferred.resolve(state.value); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + markQStateExceptionHandled(state); + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); + } + } catch (e) { + rejectPromise(promise, e); + // This error is explicitly marked for being passed to the $exceptionHandler + if (e && e.$$passToExceptionHandler === true) { + exceptionHandler(e); + } + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } + + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!isStateExceptionHandled(toCheck)) { + markQStateExceptionHandled(toCheck); + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (isError(toCheck.value)) { + exceptionHandler(toCheck.value, errorMessage); } else { - deferred.reject(state.value); + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !isStateExceptionHandled(state)) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } if (state.processScheduled || !state.pending) return; state.processScheduled = true; + ++queueSize; nextTick(function() { processQueue(state); }); } - 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); + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + } - extend(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)); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); } else { - this.$$resolve(val); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(e); + } - }, - - $$resolve: function(val) { - var then, fns; - - 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); - } - }, + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } - notify: function(progress) { - var callbacks = this.promise.$$state.pending; + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; - if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { - nextTick(function() { - 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); - } + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); } - }); - } + } + }); } - }); + } /** * @ngdoc method @@ -14917,39 +18234,27 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - 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; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -14969,11 +18274,11 @@ function qFactory(nextTick, exceptionHandler) { */ - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } /** * @ngdoc method @@ -15008,63 +18313,102 @@ function qFactory(nextTick, exceptionHandler) { */ function all(promises) { - var deferred = new Deferred(), + var result = new Promise(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } + return result; + } + + /** + * @ngdoc method + * @name $q#race + * @kind function + * + * @description + * Returns a promise that resolves or rejects as soon as one of those promises + * resolves or rejects, with the value or reason from that promise. + * + * @param {Array.|Object.} promises An array or hash of promises. + * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises` + * resolves or rejects, with the value or reason from that promise. + */ + + function race(promises) { + var deferred = defer(); + + forEach(promises, function(promise) { + when(promise).then(deferred.resolve, deferred.reject); + }); + return deferred.promise; } - var $Q = function Q(resolver) { + 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); + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } - var deferred = new Deferred(); + var promise = new Promise(); function resolveFn(value) { - deferred.resolve(value); + resolvePromise(promise, value); } function rejectFn(reason) { - deferred.reject(reason); + rejectPromise(promise, reason); } resolver(resolveFn, rejectFn); - return deferred.promise; - }; + return promise; + } + + // Let's make the instanceof operator work for promises, so that + // `new $q(fn) instanceof $q` would evaluate to true. + $Q.prototype = Promise.prototype; $Q.defer = defer; $Q.reject = reject; $Q.when = when; $Q.resolve = resolve; $Q.all = all; + $Q.race = race; return $Q; } +function isStateExceptionHandled(state) { + return !!state.pur; +} +function markQStateExceptionHandled(state) { + state.pur = true; +} +function markQExceptionHandled(q) { + // Built-in `$q` promises will always have a `$$state` property. This check is to allow + // overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises). + // (Currently, this is the only method that might be called with a promise, even if it is not + // created by the built-in `$q`.) + if (q.$$state) { + markQStateExceptionHandled(q.$$state); + } +} + +/** @this */ function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || @@ -15154,6 +18498,8 @@ function $$RAFProvider() { //rAF /** * @ngdoc service * @name $rootScope + * @this + * * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. @@ -15184,13 +18530,14 @@ function $RootScopeProvider() { this.$$watchersCount = 0; this.$id = nextUid(); this.$$ChildScope = null; + this.$$suspended = false; } ChildScope.prototype = parent; return ChildScope; } - this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser', - function($injector, $exceptionHandler, $parse, $browser) { + this.$get = ['$exceptionHandler', '$parse', '$browser', + function($exceptionHandler, $parse, $browser) { function destroyChildScope($event) { $event.currentScope.$$destroyed = true; @@ -15198,15 +18545,20 @@ function $RootScopeProvider() { function cleanUpScope($scope) { + // Support: IE 9 only if (msie === 9) { // There is a memory leak in IE9 if all child scopes are not disconnected // completely when a scope is destroyed. So this code will recurse up through // all this scopes children // // See issue https://github.com/angular/angular.js/issues/10706 - $scope.$$childHead && cleanUpScope($scope.$$childHead); - $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling); - } + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } + } // The code below works around IE9 and V8's memory leaks // @@ -15231,7 +18583,7 @@ function $RootScopeProvider() { * an in-depth introduction and usage examples. * * - * # Inheritance + * ## Inheritance * A scope can inherit from a parent scope, as in this example: * ```js var parent = $rootScope; @@ -15266,6 +18618,7 @@ function $RootScopeProvider() { this.$$childHead = this.$$childTail = null; this.$root = this; this.$$destroyed = false; + this.$$suspended = false; this.$$listeners = {}; this.$$listenerCount = {}; this.$$watchersCount = 0; @@ -15357,7 +18710,7 @@ function $RootScopeProvider() { // 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', destroyChildScope); + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); return child; }, @@ -15374,7 +18727,7 @@ function $RootScopeProvider() { * $digest()} and should return the value that will be watched. (`watchExpression` should not change * its value when executed multiple times with the same input because it may be executed multiple * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be - * [idempotent](http://en.wikipedia.org/wiki/Idempotence). + * [idempotent](http://en.wikipedia.org/wiki/Idempotence).) * - The `listener` is called only when the value from the current `watchExpression` and the * previous call to `watchExpression` are not equal (with the exception of the initial run, * see below). Inequality is determined according to reference inequality, @@ -15385,6 +18738,8 @@ function $RootScopeProvider() { * 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. + * - This should not be used to watch for changes in objects that are (or contain) + * [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - 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. @@ -15404,7 +18759,7 @@ function $RootScopeProvider() { * * * - * # Example + * @example * ```js // let's assume that scope was dependency injected as the $rootScope var scope = $rootScope; @@ -15474,20 +18829,21 @@ function $RootScopeProvider() { * - `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 + * @param {boolean=} [objectEquality=false] 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, prettyPrintExpression) { var get = $parse(watchExp); + var fn = isFunction(listener) ? listener : noop; if (get.$$watchDelegate) { - return get.$$watchDelegate(this, listener, objectEquality, get, watchExp); + return get.$$watchDelegate(this, fn, objectEquality, get, watchExp); } var scope = this, array = scope.$$watchers, watcher = { - fn: listener, + fn: fn, last: initWatchVal, get: get, exp: prettyPrintExpression || watchExp, @@ -15496,21 +18852,23 @@ function $RootScopeProvider() { lastDirtyWatch = null; - if (!isFunction(listener)) { - watcher.fn = noop; - } - if (!array) { array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + array.$$digestWatchIndex++; incrementWatchersCount(this, 1); return function deregisterWatch() { - if (arrayRemove(array, watcher) >= 0) { + var index = arrayRemove(array, watcher); + if (index >= 0) { incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } } lastDirtyWatch = null; }; @@ -15525,8 +18883,8 @@ function $RootScopeProvider() { * 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 items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.} watchExpressions Array of expressions that will be individually @@ -15570,9 +18928,8 @@ function $RootScopeProvider() { } forEach(watchExpressions, function(expr, i) { - var unwatchFn = self.$watch(expr, function watchGroupSubAction(value, oldValue) { + var unwatchFn = self.$watch(expr, function watchGroupSubAction(value) { newValues[i] = value; - oldValues[i] = oldValue; if (!changeReactionScheduled) { changeReactionScheduled = true; self.$evalAsync(watchGroupAction); @@ -15584,11 +18941,17 @@ function $RootScopeProvider() { function watchGroupAction() { changeReactionScheduled = false; - if (firstRun) { - firstRun = false; - listener(newValues, newValues, self); - } else { - listener(newValues, oldValues, self); + try { + if (firstRun) { + firstRun = false; + listener(newValues, newValues, self); + } else { + listener(newValues, oldValues, self); + } + } finally { + for (var i = 0; i < watchExpressions.length; i++) { + oldValues[i] = newValues[i]; + } } } @@ -15616,7 +18979,7 @@ function $RootScopeProvider() { * adding, removing, and moving items belonging to an object or array. * * - * # Example + * @example * ```js $scope.names = ['igor', 'matias', 'misko', 'james']; $scope.dataCount = 4; @@ -15656,7 +19019,11 @@ function $RootScopeProvider() { * de-registration function is executed, the internal watch operation is terminated. */ $watchCollection: function(obj, listener) { - $watchCollectionInterceptor.$stateful = true; + // Mark the interceptor as + // ... $$pure when literal since the instance will change when any input changes + $watchCollectionInterceptor.$$pure = $parse(obj).literal; + // ... $stateful when non-literal since we must read the state of the collection + $watchCollectionInterceptor.$stateful = !$watchCollectionInterceptor.$$pure; var self = this; // the current value, updated on each dirty-check run @@ -15707,6 +19074,7 @@ function $RootScopeProvider() { oldItem = oldValue[i]; newItem = newValue[i]; + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -15729,6 +19097,7 @@ function $RootScopeProvider() { oldItem = oldValue[key]; if (key in oldValue) { + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -15812,7 +19181,7 @@ function $RootScopeProvider() { * * In unit tests, you may need to call `$digest()` to simulate the scope life cycle. * - * # Example + * @example * ```js var scope = ...; scope.name = 'misko'; @@ -15839,13 +19208,12 @@ function $RootScopeProvider() { * */ $digest: function() { - var watch, value, last, + var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, - next, current, target = this, + next, current, target = asyncQueue.length ? $rootScope : this, watchLog = [], - logIdx, logMsg, asyncTask; + logIdx, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -15864,36 +19232,42 @@ function $RootScopeProvider() { dirty = false; current = target; - while (asyncQueue.length) { + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $evalAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); + asyncTask = asyncQueue[asyncQueuePosition]; + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } + asyncQueue.length = 0; traverseScopesLoop: do { // "traverse the scopes" loop - if ((watchers = current.$$watchers)) { + if ((watchers = !current.$$suspended && current.$$watchers)) { // process our watches - length = watchers.length; - while (length--) { + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { try { - watch = watchers[length]; + watch = watchers[watchers.$$digestWatchIndex]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { - if ((value = watch.get(current)) !== (last = watch.last) && + get = watch.get; + if ((value = get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) - : (typeof value === 'number' && typeof last === 'number' - && isNaN(value) && isNaN(last)))) { + : (isNumberNaN(value) && isNumberNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; - watch.fn(value, ((last === initWatchVal) ? value : last), current); + fn = watch.fn; + fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; @@ -15919,7 +19293,9 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast - if (!(next = ((current.$$watchersCount && current.$$childHead) || + // (though it differs due to having the extra check for $$suspended and does not + // check $$listenerCount) + if (!(next = ((!current.$$suspended && current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; @@ -15941,15 +19317,110 @@ function $RootScopeProvider() { clearPhase(); - while (postDigestQueue.length) { + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { try { - postDigestQueue.shift()(); + postDigestQueue[postDigestQueuePosition++](); } catch (e) { $exceptionHandler(e); } } + postDigestQueue.length = postDigestQueuePosition = 0; + + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$suspend + * @kind function + * + * @description + * Suspend watchers of this scope subtree so that they will not be invoked during digest. + * + * This can be used to optimize your application when you know that running those watchers + * is redundant. + * + * **Warning** + * + * Suspending scopes from the digest cycle can have unwanted and difficult to debug results. + * Only use this approach if you are confident that you know what you are doing and have + * ample tests to ensure that bindings get updated as you expect. + * + * Some of the things to consider are: + * + * * Any external event on a directive/component will not trigger a digest while the hosting + * scope is suspended - even if the event handler calls `$apply()` or `$rootScope.$digest()`. + * * Transcluded content exists on a scope that inherits from outside a directive but exists + * as a child of the directive's containing scope. If the containing scope is suspended the + * transcluded scope will also be suspended, even if the scope from which the transcluded + * scope inherits is not suspended. + * * Multiple directives trying to manage the suspended status of a scope can confuse each other: + * * A call to `$suspend()` on an already suspended scope is a no-op. + * * A call to `$resume()` on a non-suspended scope is a no-op. + * * If two directives suspend a scope, then one of them resumes the scope, the scope will no + * longer be suspended. This could result in the other directive believing a scope to be + * suspended when it is not. + * * If a parent scope is suspended then all its descendants will be also excluded from future + * digests whether or not they have been suspended themselves. Note that this also applies to + * isolate child scopes. + * * Calling `$digest()` directly on a descendant of a suspended scope will still run the watchers + * for that scope and its descendants. When digesting we only check whether the current scope is + * locally suspended, rather than checking whether it has a suspended ancestor. + * * Calling `$resume()` on a scope that has a suspended ancestor will not cause the scope to be + * included in future digests until all its ancestors have been resumed. + * * Resolved promises, e.g. from explicit `$q` deferreds and `$http` calls, trigger `$apply()` + * against the `$rootScope` and so will still trigger a global digest even if the promise was + * initiated by a component that lives on a suspended scope. + */ + $suspend: function() { + this.$$suspended = true; + }, + + /** + * @ngdoc method + * @name $rootScope.Scope#$isSuspended + * @kind function + * + * @description + * Call this method to determine if this scope has been explicitly suspended. It will not + * tell you whether an ancestor has been suspended. + * To determine if this scope will be excluded from a digest triggered at the $rootScope, + * for example, you must check all its ancestors: + * + * ``` + * function isExcludedFromDigest(scope) { + * while(scope) { + * if (scope.$isSuspended()) return true; + * scope = scope.$parent; + * } + * return false; + * ``` + * + * Be aware that a scope may not be included in digests if it has a suspended ancestor, + * even if `$isSuspended()` returns false. + * + * @returns true if the current scope has been suspended. + */ + $isSuspended: function() { + return this.$$suspended; }, + /** + * @ngdoc method + * @name $rootScope.Scope#$resume + * @kind function + * + * @description + * Resume watchers of this scope subtree in case it was suspended. + * + * See {@link $rootScope.Scope#$suspend} for information about the dangers of using this approach. + */ + $resume: function() { + this.$$suspended = false; + }, /** * @ngdoc event @@ -16005,8 +19476,8 @@ function $RootScopeProvider() { // sever all the references to parent scopes (after this cleanup, the current scope should // not be retained by any of our references and should be eligible for garbage collection) - if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling; + if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling; + if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling; if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling; if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling; @@ -16027,10 +19498,10 @@ function $RootScopeProvider() { * * @description * Executes the `expression` on the current scope and returns the result. Any exceptions in - * the expression are propagated (uncaught). This is useful when evaluating Angular + * the expression are propagated (uncaught). This is useful when evaluating AngularJS * expressions. * - * # Example + * @example * ```js var scope = ng.$rootScope.Scope(); scope.a = 1; @@ -16040,7 +19511,7 @@ function $RootScopeProvider() { expect(scope.$eval(function(scope){ return scope.a + scope.b; })).toEqual(3); * ``` * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {(string|function())=} expression An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. @@ -16075,7 +19546,7 @@ function $RootScopeProvider() { * will be scheduled. However, it is encouraged to always call code that changes the model * from within an `$apply` call. That includes code evaluated via `$evalAsync`. * - * @param {(string|function())=} expression An angular expression to be executed. + * @param {(string|function())=} expression An AngularJS expression to be executed. * * - `string`: execute using the rules as defined in {@link guide/expression expression}. * - `function(scope)`: execute the function with the current `scope` parameter. @@ -16090,10 +19561,10 @@ function $RootScopeProvider() { if (asyncQueue.length) { $rootScope.$digest(); } - }); + }, null, '$evalAsync'); } - asyncQueue.push({scope: this, expression: expr, locals: locals}); + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); }, $$postDigest: function(fn) { @@ -16106,15 +19577,14 @@ function $RootScopeProvider() { * @kind function * * @description - * `$apply()` is used to execute an expression in angular from outside of the angular + * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). - * Because we are calling into the angular framework we need to perform proper scope life + * Because we are calling into the AngularJS framework we need to perform proper scope life * cycle of {@link ng.$exceptionHandler exception handling}, * {@link ng.$rootScope.Scope#$digest executing watches}. * - * ## Life cycle + * **Life cycle: Pseudo-Code of `$apply()`** * - * # Pseudo-Code of `$apply()` * ```js function $apply(expr) { try { @@ -16138,7 +19608,7 @@ function $RootScopeProvider() { * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method. * * - * @param {(string|function())=} exp An angular expression to be executed. + * @param {(string|function())=} exp An AngularJS 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. @@ -16160,6 +19630,7 @@ function $RootScopeProvider() { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally throw e; } } @@ -16177,14 +19648,17 @@ function $RootScopeProvider() { * 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. + * @param {(string|function())=} exp An AngularJS 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); + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } + expr = $parse(expr); scheduleApplyAsync(); function $applyAsyncExpression() { @@ -16238,7 +19712,10 @@ function $RootScopeProvider() { return function() { var indexOfListener = namedListeners.indexOf(listener); if (indexOfListener !== -1) { - namedListeners[indexOfListener] = null; + // Use delete in the hope of the browser deallocating the memory for the array entry, + // while not shifting the array indexes of other listeners. + // See issue https://github.com/angular/angular.js/issues/16135 + delete namedListeners[indexOfListener]; decrementListenerCount(self, 1, name); } }; @@ -16305,8 +19782,7 @@ function $RootScopeProvider() { } //if any listener on the current scope stops propagation, prevent bubbling if (stopPropagation) { - event.currentScope = null; - return event; + break; } //traverse upwards scope = scope.$parent; @@ -16380,7 +19856,8 @@ function $RootScopeProvider() { // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $digest - // (though it differs due to having the extra check for $$listenerCount) + // (though it differs due to having the extra check for $$listenerCount and + // does not check $$suspended) if (!(next = ((current.$$listenerCount[name] && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { @@ -16401,6 +19878,8 @@ function $RootScopeProvider() { var postDigestQueue = $rootScope.$$postDigestQueue = []; var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + var postDigestQueuePosition = 0; + return $rootScope; @@ -16453,74 +19932,101 @@ function $RootScopeProvider() { if (applyAsyncId === null) { applyAsyncId = $browser.defer(function() { $rootScope.$apply(flushApplyAsync); - }); + }, null, '$applyAsync'); } } }]; } /** + * @ngdoc service + * @name $rootElement + * + * @description + * The root element of AngularJS application. This is either the element where {@link + * ng.directive:ngApp ngApp} was declared or the element passed into + * {@link angular.bootstrap}. The element represents the root element of application. It is also the + * location where the application's {@link auto.$injector $injector} service gets + * published, and can be retrieved using `$rootElement.injector()`. + */ + + +// the implementation is in angular.bootstrap + +/** + * @this * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ function $$SanitizeUriProvider() { - var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/; + + var aHrefSanitizationTrustedUrlList = /^\s*(https?|s?ftp|mailto|tel|file):/, + imgSrcSanitizationTrustedUrlList = /^\s*((https?|ftp|file|blob):|data:image\/)/; /** * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted 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 prevent XSS attacks via HTML anchor 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` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring + * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)` + * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL. + * + * If the URL matches the `aHrefSanitizationTrustedUrlList` regular expression, it is returned unchanged. + * + * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written + * to the DOM it is inactive and potentially malicious code will not be executed. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.aHrefSanitizationWhitelist = function(regexp) { + this.aHrefSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - aHrefSanitizationWhitelist = regexp; + aHrefSanitizationTrustedUrlList = regexp; return this; } - return aHrefSanitizationWhitelist; + return aHrefSanitizationTrustedUrlList; }; /** * @description - * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * Retrieves or overrides the default regular expression that is used for determining trusted safe * urls during img[src] sanitization. * - * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links. * - * Any url about to be assigned to img[src] via data-binding is first normalized and turned into - * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` - * regular expression. If a match is found, the original url is written into the dom. Otherwise, - * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring + * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to + * `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize + * the potentially malicious URL. * - * @param {RegExp=} regexp New regexp to whitelist urls with. + * If the URL matches the `imgSrcSanitizationTrustedUrlList` regular expression, it is returned + * unchanged. + * + * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written + * to the DOM it is inactive and potentially malicious code will not be executed. + * + * @param {RegExp=} regexp New regexp to trust urls with. * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for * chaining otherwise. */ - this.imgSrcSanitizationWhitelist = function(regexp) { + this.imgSrcSanitizationTrustedUrlList = function(regexp) { if (isDefined(regexp)) { - imgSrcSanitizationWhitelist = regexp; + imgSrcSanitizationTrustedUrlList = regexp; return this; } - return imgSrcSanitizationWhitelist; + return imgSrcSanitizationTrustedUrlList; }; this.$get = function() { - return function sanitizeUri(uri, isImage) { - var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; - var normalizedVal; - normalizedVal = urlResolve(uri).href; + return function sanitizeUri(uri, isMediaUrl) { + // if (!uri) return uri; + var regex = isMediaUrl ? imgSrcSanitizationTrustedUrlList : aHrefSanitizationTrustedUrlList; + var normalizedVal = urlResolve(uri && uri.trim()).href; if (normalizedVal !== '' && !normalizedVal.match(regex)) { return 'unsafe:' + normalizedVal; } @@ -16540,20 +20046,43 @@ function $$SanitizeUriProvider() { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* exported $SceProvider, $SceDelegateProvider */ + var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { + // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding). HTML: 'html', + + // Style statements or stylesheets. Currently unused in AngularJS. CSS: 'css', + + // An URL used in a context where it refers to the source of media, which are not expected to be run + // as scripts, such as an image, audio, video, etc. + MEDIA_URL: 'mediaUrl', + + // An URL used in a context where it does not refer to a resource that loads code. + // A value that can be trusted as a URL can also trusted as a MEDIA_URL. URL: 'url', - // RESOURCE_URL is a subtype of URL used in contexts where a privileged resource is sourced from a - // url. (e.g. ng-include, script src, templateUrl) + + // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as + // code. (e.g. ng-include, script src binding, templateUrl) + // A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL. RESOURCE_URL: 'resourceUrl', + + // Script. Currently unused in AngularJS. JS: 'js' }; // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -16567,8 +20096,8 @@ function adjustMatcher(matcher) { 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -16603,6 +20132,16 @@ function adjustMatchers(matchers) { * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict * Contextual Escaping (SCE)} services to AngularJS. * + * For an overview of this service and the functionnality it provides in AngularJS, see the main + * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how + * SCE works in their application, which shouldn't be needed in most cases. + * + *
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or + * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners, + * changes to this service will also influence users, so be extra careful and document your changes. + *
+ * * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to @@ -16614,123 +20153,177 @@ function adjustMatchers(matchers) { * The default instance of `$sceDelegate` should work out of the box with little pain. While you * can override it completely to change the behavior of `$sce`, the common case would * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting - * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as - * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist - * $sceDelegateProvider.resourceUrlWhitelist} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * your own trusted and banned resource lists for trusting URLs used for loading AngularJS resources + * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList + * $sceDelegateProvider.trustedResourceUrlList} and {@link + * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList} */ /** * @ngdoc provider * @name $sceDelegateProvider + * @this + * * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate - * $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure - * that the URLs used for sourcing Angular templates are safe. Refer {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and - * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} + * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}. * - * For the general details about this service in Angular, read the main page for {@link ng.$sce + * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and + * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularJS templates and + * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See + * {@link ng.$sceDelegateProvider#trustedResourceUrlList + * $sceDelegateProvider.trustedResourceUrlList} and + * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}, + * + * For the general details about this service in AngularJS, read the main page for {@link ng.$sce * Strict Contextual Escaping (SCE)}. * * **Example**: Consider the following case.
* * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc. + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: * * ``` * angular.module('myApp', []).config(function($sceDelegateProvider) { - * $sceDelegateProvider.resourceUrlWhitelist([ + * $sceDelegateProvider.trustedResourceUrlList([ * // 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([ + * // The banned resource URL list overrides the trusted resource URL list so the open redirect + * // here is blocked. + * $sceDelegateProvider.bannedResourceUrlList([ * 'http://myapp.example.com/clickThru**' * ]); * }); * ``` + * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require + * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates + * requested by {@link ng.$templateRequest $templateRequest} that are present in + * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism + * to populate your templates in that cache at config time, then it is a good idea to remove 'self' + * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types + * of issues, like for instance attacker-controlled `ng-includes`. */ function $SceDelegateProvider() { this.SCE_CONTEXTS = SCE_CONTEXTS; // Resource URLs can also be trusted by policy. - var resourceUrlWhitelist = ['self'], - resourceUrlBlacklist = []; + var trustedResourceUrlList = ['self'], + bannedResourceUrlList = []; /** * @ngdoc method - * @name $sceDelegateProvider#resourceUrlWhitelist + * @name $sceDelegateProvider#trustedResourceUrlList * @kind function * - * @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. - * + * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with + * the value provided. This must be an array or null. A snapshot of this array is used so + * further changes to the array are ignored. * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items * allowed in this array. * - * Note: **an empty whitelist array will block all URLs**! + * @return {Array} The currently set trusted resource URL array. * - * @return {Array} the currently set whitelist array. + * @description + * Sets/Gets the list trusted of resource URLs. * - * The **default value** when no whitelist has been explicitly set is `['self']` allowing only - * same origin resource requests. + * The **default value** when no `trustedResourceUrlList` has been explicitly set is `['self']` + * allowing only same origin resource requests. * - * @description - * Sets/Gets the whitelist of trusted resource URLs. + *
+ * **Note:** the default `trustedResourceUrlList` of 'self' is not recommended if your app shares + * its origin with other apps! It is a good idea to limit it to only your application's directory. + *
*/ - this.resourceUrlWhitelist = function(value) { + this.trustedResourceUrlList = function(value) { if (arguments.length) { - resourceUrlWhitelist = adjustMatchers(value); + trustedResourceUrlList = adjustMatchers(value); } - return resourceUrlWhitelist; + return trustedResourceUrlList; }; /** * @ngdoc method - * @name $sceDelegateProvider#resourceUrlBlacklist + * @name $sceDelegateProvider#resourceUrlWhitelist * @kind function * - * @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value - * provided. This must be an array or null. A snapshot of this array is used so further - * changes to the array are ignored. + * @deprecated + * sinceVersion="1.8.1" * - * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items - * allowed in this array. + * This method is deprecated. Use {@link $sceDelegateProvider#trustedResourceUrlList + * trustedResourceUrlList} instead. + */ + Object.defineProperty(this, 'resourceUrlWhitelist', { + get: function() { + return this.trustedResourceUrlList; + }, + set: function(value) { + this.trustedResourceUrlList = value; + } + }); + + /** + * @ngdoc method + * @name $sceDelegateProvider#bannedResourceUrlList + * @kind function * - * The typical usage for the blacklist is to **block + * @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with + * the value provided. This must be an array or null. A snapshot of this array is used so + * further changes to the array are ignored.

+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items + * allowed in this array.

+ * The typical usage for the `bannedResourceUrlList` is to **block * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as * these would otherwise be trusted but actually return content from the redirected domain. + *

+ * Finally, **the banned resource URL list overrides the trusted resource URL list** and has + * the final say. * - * Finally, **the blacklist overrides the whitelist** and has the final say. - * - * @return {Array} the currently set blacklist array. - * - * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there - * is no blacklist.) + * @return {Array} The currently set `bannedResourceUrlList` array. * * @description - * Sets/Gets the blacklist of trusted resource URLs. + * Sets/Gets the `bannedResourceUrlList` of trusted resource URLs. + * + * The **default value** when no trusted resource URL list has been explicitly set is the empty + * array (i.e. there is no `bannedResourceUrlList`.) */ - - this.resourceUrlBlacklist = function(value) { + this.bannedResourceUrlList = function(value) { if (arguments.length) { - resourceUrlBlacklist = adjustMatchers(value); + bannedResourceUrlList = adjustMatchers(value); } - return resourceUrlBlacklist; + return bannedResourceUrlList; }; - this.$get = ['$injector', function($injector) { + /** + * @ngdoc method + * @name $sceDelegateProvider#resourceUrlBlacklist + * @kind function + * + * @deprecated + * sinceVersion="1.8.1" + * + * This method is deprecated. Use {@link $sceDelegateProvider#bannedResourceUrlList + * bannedResourceUrlList} instead. + */ + Object.defineProperty(this, 'resourceUrlBlacklist', { + get: function() { + return this.bannedResourceUrlList; + }, + set: function(value) { + this.bannedResourceUrlList = value; + } + }); + + this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) { var htmlSanitizer = function htmlSanitizer(html) { throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); @@ -16743,7 +20336,7 @@ function $SceDelegateProvider() { function matchUrl(matcher, parsedUrl) { if (matcher === 'self') { - return urlIsSameOrigin(parsedUrl); + return urlIsSameOrigin(parsedUrl) || urlIsSameOriginAsBaseUrl(parsedUrl); } else { // definitely a regex. See adjustMatchers() return !!matcher.exec(parsedUrl.href); @@ -16753,17 +20346,17 @@ function $SceDelegateProvider() { function isResourceUrlAllowedByPolicy(url) { var parsedUrl = urlResolve(url.toString()); var i, n, allowed = false; - // Ensure that at least one item from the whitelist allows this url. - for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) { - if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) { + // Ensure that at least one item from the trusted resource URL list allows this url. + for (i = 0, n = trustedResourceUrlList.length; i < n; i++) { + if (matchUrl(trustedResourceUrlList[i], parsedUrl)) { allowed = true; break; } } if (allowed) { - // Ensure that no item from the blacklist blocked this url. - for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) { - if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) { + // Ensure that no item from the banned resource URL list has blocked this url. + for (i = 0, n = bannedResourceUrlList.length; i < n; i++) { + if (matchUrl(bannedResourceUrlList[i], parsedUrl)) { allowed = false; break; } @@ -16795,7 +20388,8 @@ function $SceDelegateProvider() { byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase); - byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase); + byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]); byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase); byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]); @@ -16804,17 +20398,24 @@ function $SceDelegateProvider() { * @name $sceDelegate#trustAs * * @description - * Returns an object that is trusted by angular for use in specified strict - * contextual escaping contexts (such as ng-bind-html, ng-include, any src - * attribute interpolation, any dom event binding attribute interpolation - * such as for onclick, etc.) that uses the provided value. - * See {@link ng.$sce $sce} for enabling strict contextual escaping. - * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * Returns a trusted representation of the parameter for the specified context. This trusted + * object will later on be used as-is, without any security check, by bindings or directives + * that require this security context. + * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass + * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as + * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the + * sanitizer loaded, passing the value itself will render all the HTML that does not pose a + * security risk. + * + * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those + * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual + * escaping. + * + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that should be considered trusted. + * @return {*} A trusted representation of value, that can be used in the given context. */ function trustAs(type, trustedValue) { var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null); @@ -16846,11 +20447,11 @@ function $SceDelegateProvider() { * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. * * If the passed parameter is not a value that had been returned by {@link - * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, returns it as-is. + * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is. * * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} - * call or anything else. - * @returns {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs + * call or anything else. + * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns * `value` unchanged. */ @@ -16867,28 +20468,56 @@ function $SceDelegateProvider() { * @name $sceDelegate#getTrusted * * @description - * Takes the result of a {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call and - * returns the originally supplied value if the queried context type is a supertype of the - * created type. If this condition isn't satisfied, throws an exception. + * Given an object and a security context in which to assign it, returns a value that's safe to + * use in this context, which was represented by the parameter. To do so, this function either + * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs + * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on + * the context and sanitizer availablility. * - * @param {string} type The kind of context in which this value is to be used. + * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available + * by default, and the third one relies on the `$sanitize` service (which may be loaded through + * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be + * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#trustedResourceUrlList + * `$sceDelegateProvider.trustedResourceUrlList`} and {@link ng.$sceDelegateProvider#bannedResourceUrlList + * `$sceDelegateProvider.bannedResourceUrlList`} accepts that resource. + * + * This function will throw if the safe type isn't appropriate for this context, or if the + * value given cannot be accepted in the context (which might be caused by sanitization not + * being available, or the value not being recognized as safe). + * + *

+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting + * (XSS) vulnerability in your application. + *
+ * + * @param {string} type The context in which this value is to be used (such as `$sce.HTML`). * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} call. - * @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs - * `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception. + * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ function getTrusted(type, maybeTrusted) { if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') { return maybeTrusted; } var constructor = (byType.hasOwnProperty(type) ? byType[type] : null); + // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return + // as-is. if (constructor && maybeTrusted instanceof constructor) { return maybeTrusted.$$unwrapTrustedValue(); } - // If we get here, then we may only take one of two actions. - // 1. sanitize the value for the requested type, or - // 2. throw an exception. - if (type === SCE_CONTEXTS.RESOURCE_URL) { + + // If maybeTrusted is a trusted class instance but not of the correct trusted type + // then unwrap it and allow it to pass through to the rest of the checks + if (isFunction(maybeTrusted.$$unwrapTrustedValue)) { + maybeTrusted = maybeTrusted.$$unwrapTrustedValue(); + } + + // If we get here, then we will either sanitize the value or throw an exception. + if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) { + // we attempt to sanitize non-resource URLs + return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL); + } else if (type === SCE_CONTEXTS.RESOURCE_URL) { if (isResourceUrlAllowedByPolicy(maybeTrusted)) { return maybeTrusted; } else { @@ -16897,8 +20526,10 @@ function $SceDelegateProvider() { maybeTrusted.toString()); } } else if (type === SCE_CONTEXTS.HTML) { + // htmlSanitizer throws its own error when no sanitizer is available. return htmlSanitizer(maybeTrusted); } + // Default error when the $sce service has no way to make the input safe. throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.'); } @@ -16912,6 +20543,8 @@ function $SceDelegateProvider() { /** * @ngdoc provider * @name $sceProvider + * @this + * * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -16921,8 +20554,6 @@ function $SceDelegateProvider() { * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ -/* jshint maxlen: false*/ - /** * @ngdoc service * @name $sce @@ -16932,23 +20563,29 @@ function $SceDelegateProvider() { * * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS. * - * # Strict Contextual Escaping + * ## Strict Contextual Escaping + * + * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render + * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and + * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * + * ### Overview * - * Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain - * contexts to result in a value that is marked as safe to use for that context. One example of - * such a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer - * to these contexts as privileged or SCE contexts. + * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in + * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically + * run security checks on them (sanitizations, trusted URL resource, depending on context), or throw + * when it cannot guarantee the security of the result. That behavior depends strongly on contexts: + * HTML can be sanitized, but template URLs cannot, for instance. * - * As of version 1.2, Angular ships with SCE enabled by default. + * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML: + * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it + * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and + * render the input as-is, you will need to mark it as trusted for that context before attempting + * to bind it. * - * 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 - * to learn more about them. - * You can ensure your document is in standards mode and not quirks mode by adding `` - * to the top of your HTML document. + * As of version 1.2, AngularJS ships with SCE enabled by default. * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for - * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. + * ### In practice * * Here's an example of a binding in a privileged context: * @@ -16958,10 +20595,10 @@ function $SceDelegateProvider() { * ``` * * 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. - * In a more realistic example, one may be rendering user comments, blog articles, etc. via - * bindings. (HTML is just one example of a context where rendering user controlled input creates - * security vulnerabilities.) + * disabled, this application allows the user to render arbitrary HTML into the DIV, which would + * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog + * articles, etc. via bindings. (HTML is just one example of a context where rendering user + * controlled input creates security vulnerabilities.) * * For the case of HTML, you might use a library, either on the client side, or on the server side, * to sanitize unsafe HTML before binding to the value and rendering it in the document. @@ -16971,25 +20608,29 @@ function $SceDelegateProvider() { * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some * properties/fields and forgot to update the binding to the sanitized value? * - * To be secure by default, you want to ensure that any such bindings are disallowed unless you can - * determine that something explicitly says it's safe to use a value for binding in that - * context. You can then audit your code (a simple grep would do) to ensure that this is only done - * for those values that you can easily tell are safe - because they were received from your server, - * sanitized by your library, etc. You can organize your codebase to help with this - perhaps - * allowing only the files in a specific directory to do this. Ensuring that the internal API - * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task. + * To be secure by default, AngularJS makes sure bindings go through that sanitization, or + * any similar validation process, unless there's a good reason to trust the given value in this + * context. That trust is formalized with a function call. This means that as a developer, you + * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues, + * you just need to ensure the values you mark as trusted indeed are safe - because they were + * received from your server, sanitized by your library, etc. You can organize your codebase to + * help with this - perhaps allowing only the files in a specific directory to do this. + * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then + * becomes a more manageable task. * * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to - * obtain values that will be accepted by SCE / privileged contexts. + * build the trusted versions of your values. * - * - * ## How does it work? + * ### How does it work? * * 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#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. + * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as + * a way to enforce the required security context in your data sink. Directives use {@link + * 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. Also, + * when binding without directives, AngularJS will understand the context of your bindings + * automatically. * * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly @@ -17005,16 +20646,16 @@ function $SceDelegateProvider() { * }]; * ``` * - * ## Impact on loading templates + * ### Impact on loading templates * * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as * `templateUrl`'s specified by {@link guide/directive directives}. * - * By default, Angular only loads templates from the same domain and protocol as the application + * By default, AngularJS only loads templates from the same domain and protocol as the application * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or - * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist - * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value. + * protocols, you may either add them to the {@link ng.$sceDelegateProvider#trustedResourceUrlList + * trustedResourceUrlList} or {@link ng.$sce#trustAsResourceUrl wrap them} into trusted values. * * *Please note*: * The browser's @@ -17025,40 +20666,58 @@ 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 + * ### This feels like too much overhead * * It's important to remember that SCE only applies to interpolation expressions. * * If your expressions are constant literals, they're automatically trusted and you don't need to - * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g. - * `
`) just works. - * - * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them - * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here. + * call `$sce.trustAs` on them (e.g. + * `
`) just works (remember to include the + * `ngSanitize` module). The `$sceDelegate` will also use the `$sanitize` service if it is available + * when binding untrusted values to `$sce.HTML` context. + * AngularJS provides an implementation in `angular-sanitize.js`, and if you + * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in + * your application. * * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load * templates in `ng-include` from your application's domain without having to even know about SCE. * It blocks loading templates from other domains or loading templates over http from an https * served document. You can change these by setting your own custom {@link - * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link - * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs. + * ng.$sceDelegateProvider#trustedResourceUrlList trusted resource URL list} and {@link + * ng.$sceDelegateProvider#bannedResourceUrlList banned resource URL list} for matching such URLs. * * This significantly reduces the overhead. It is far easier to pay the small overhead and have an * application that's secure and can be audited to verify that with much more ease than bolting * security onto an application later. * * - * ## What trusted context types are supported? + * ### What trusted context types are supported? * * | 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. 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 (`
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. | + * | `$sce.MEDIA_URL` | For URLs that are safe to render as media. Is automatically converted from string by sanitizing when needed. | + * | `$sce.URL` | For URLs that are safe to follow as links. Is automatically converted from string by sanitizing when needed. Note that `$sce.URL` makes a stronger statement about the URL than `$sce.MEDIA_URL` does and therefore contexts requiring values trusted for `$sce.URL` can be used anywhere that values trusted for `$sce.MEDIA_URL` are required.| + * | `$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.)

Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` or `$sce.MEDIA_URL` do and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` or `$sce.MEDIA_URL` are required.

The {@link $sceDelegateProvider#trustedResourceUrlList $sceDelegateProvider#trustedResourceUrlList()} and {@link $sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider#bannedResourceUrlList()} can be used to restrict trusted origins for `RESOURCE_URL` | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * - * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * + *
+ * Be aware that, before AngularJS 1.7.0, `a[href]` and `img[src]` used to sanitize their + * interpolated values directly rather than rely upon {@link ng.$sce#getTrusted `$sce.getTrusted`}. + * + * **As of 1.7.0, this is no longer the case.** + * + * Now such interpolations are marked as requiring `$sce.URL` (for `a[href]`) or `$sce.MEDIA_URL` + * (for `img[src]`), so that the sanitization happens (via `$sce.getTrusted...`) when the `$interpolate` + * service evaluates the expressions. + *
+ * + * There are no CSS or JS context bindings in AngularJS currently, so their corresponding `$sce.trustAs` + * functions aren't useful yet. This might evolve. + * + * ### Format of items in {@link ng.$sceDelegateProvider#trustedResourceUrlList trustedResourceUrlList}/{@link ng.$sceDelegateProvider#bannedResourceUrlList bannedResourceUrlList} * * Each element in these arrays must be one of the following: * @@ -17072,7 +20731,7 @@ function $SceDelegateProvider() { * match themselves. * - `*`: matches zero or more occurrences of any character other than one of the following 6 * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use - * in a whitelist. + * for matching resource URL lists. * - `**`: matches zero or more occurrences of *any* character. As such, it's not * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g. * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might @@ -17105,9 +20764,9 @@ function $SceDelegateProvider() { * * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example. * - * ## Show me an example using SCE. + * ### Show me an example using SCE. * - * + * * *
*

@@ -17128,10 +20787,10 @@ function $SceDelegateProvider() { * * angular.module('mySceApp', ['ngSanitize']) * .controller('AppController', ['$http', '$templateCache', '$sce', - * function($http, $templateCache, $sce) { + * function AppController($http, $templateCache, $sce) { * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * ' * describe('SCE doc demo', function() { * it('should sanitize untrusted values', function() { - * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) * .toBe('Is anyone reading this?'); * }); * * it('should NOT sanitize explicitly trusted values', function() { - * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( * 'Hover over this text.'); * }); @@ -17175,20 +20834,20 @@ function $SceDelegateProvider() { * for little coding overhead. It will be much harder to take an SCE disabled application and * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE * for cases where you have a lot of existing code that was written before SCE was introduced and - * you're migrating them a module at a time. + * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if + * you are writing a library, you will cause security bugs applications using it. * * That said, here's how you can completely disable SCE: * * ``` * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) { * // Completely disable SCE. For demonstration purposes only! - * // Do not use in new projects. + * // Do not use in new projects or libraries. * $sceProvider.enabled(false); * }); * ``` * */ -/* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; @@ -17198,8 +20857,8 @@ function $SceProvider() { * @name $sceProvider#enabled * @kind function * - * @param {boolean=} value If provided, then enables/disables SCE. - * @return {boolean} true if SCE is enabled, false otherwise. + * @param {boolean=} value If provided, then enables/disables SCE application-wide. + * @return {boolean} True if SCE is enabled, false otherwise. * * @description * Enables/disables SCE and returns the current value. @@ -17230,7 +20889,7 @@ function $SceProvider() { * such a value. * * - getTrusted(contextEnum, value) - * This function should return the a value that is safe to use in the context specified by + * This function should return the value that is safe to use in the context specified by * contextEnum or throw and exception otherwise. * * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be @@ -17253,13 +20912,14 @@ function $SceProvider() { * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value) * will also succeed. * - * Inheritance happens to capture this in a natural way. In some future, we - * may not use inheritance anymore. That is OK because no code outside of - * sce.js and sceSpecs.js would need to be aware of this detail. + * Inheritance happens to capture this in a natural way. In some future, we may not use + * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to + * be aware of this detail. */ this.$get = ['$parse', '$sceDelegate', function( $parse, $sceDelegate) { + // Support: IE 9-11 only // 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 && msie < 8) { @@ -17276,8 +20936,8 @@ function $SceProvider() { * @name $sce#isEnabled * @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}. + * @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}. * * @description * Returns a boolean indicating if SCE is enabled. @@ -17299,19 +20959,19 @@ function $SceProvider() { * @name $sce#parseAs * * @description - * Converts Angular {@link guide/expression expression} into a function. This is like {@link + * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*, * *result*)} * - * @param {string} type The kind of SCE context in which this result will be used. + * @param {string} type The SCE context in which this result will be used. * @param {string} expression String expression to compile. - * @returns {function(context, locals)} a function which represents the compiled expression: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ sce.parseAs = function sceParseAs(type, expr) { var parsed = $parse(expr); @@ -17329,18 +20989,18 @@ function $SceProvider() { * @name $sce#trustAs * * @description - * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, - * returns an object that is trusted by angular for use in specified strict contextual - * escaping contexts (such as ng-bind-html, ng-include, any src attribute - * interpolation, any dom event binding attribute interpolation such as for onclick, etc.) - * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual - * escaping. + * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a + * wrapped object that represents your value, and the trust you have in its safety for the given + * context. AngularJS can then use that value as-is in bindings of the specified secure context. + * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute + * interpolations. See {@link ng.$sce $sce} for strict contextual escaping. * - * @param {string} type The kind of context in which this value is safe for use. e.g. url, - * resourceUrl, html, js and css. - * @param {*} value The value that that should be considered trusted/safe. - * @returns {*} A value that can be used to stand in for the provided `value` in places - * where Angular expects a $sce.trustAs() return value. + * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`, + * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`. + * + * @param {*} value The value that that should be considered trusted. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in the context you specified. */ /** @@ -17351,11 +21011,23 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsHtml(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml - * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.HTML` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.HTML` context (like `ng-bind-html`). + */ + + /** + * @ngdoc method + * @name $sce#trustAsCss + * + * @description + * Shorthand method. `$sce.trustAsCss(value)` → + * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`} + * + * @param {*} value The value to mark as trusted for `$sce.CSS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant + * of your `value` in `$sce.CSS` context. This context is currently unused, so there are + * almost no reasons to use this function so far. */ /** @@ -17366,11 +21038,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl - * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.URL` context. That context is currently unused, so there are almost no reasons + * to use this function so far. */ /** @@ -17381,11 +21052,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsResourceUrl(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl - * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the return - * value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute + * bindings, ...) */ /** @@ -17396,11 +21066,10 @@ function $SceProvider() { * Shorthand method. `$sce.trustAsJs(value)` → * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`} * - * @param {*} value The value to trustAs. - * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs - * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives - * only accept expressions that are either literal constants or are the - * return value of {@link ng.$sce#trustAs $sce.trustAs}.) + * @param {*} value The value to mark as trusted for `$sce.JS` context. + * @return {*} A wrapped version of value that can be used as a trusted variant of your `value` + * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to + * use this function so far. */ /** @@ -17409,16 +21078,17 @@ function $SceProvider() { * * @description * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, - * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the - * originally supplied value if the queried context type is a supertype of the created type. - * If this condition isn't satisfied, throws an exception. - * - * @param {string} type The kind of context in which this value is to be used. - * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} - * call. - * @returns {*} The value the was originally provided to - * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context. - * Otherwise, throws an exception. + * takes any input, and either returns a value that's safe to use in the specified context, + * or throws an exception. This function is aware of trusted values created by the `trustAs` + * function and its shorthands, and when contexts are appropriate, returns the unwrapped value + * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a + * safe value (e.g., no sanitization is available or possible.) + * + * @param {string} type The context in which this value is to be used. + * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs + * `$sce.trustAs`} call, or anything else (which will not be considered trusted.) + * @return {*} A version of the value that's safe to use in the given context, or throws an + * exception if this is impossible. */ /** @@ -17430,7 +21100,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)` + * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)` */ /** @@ -17442,7 +21112,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)` */ /** @@ -17454,7 +21124,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.URL, value)` */ /** @@ -17466,7 +21136,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`} * * @param {*} value The value to pass to `$sceDelegate.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` + * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)` */ /** @@ -17478,7 +21148,7 @@ function $SceProvider() { * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`} * * @param {*} value The value to pass to `$sce.getTrusted`. - * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)` + * @return {*} The return value of `$sce.getTrusted($sce.JS, value)` */ /** @@ -17490,12 +21160,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17507,12 +21177,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17524,12 +21194,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17541,12 +21211,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ /** @@ -17558,12 +21228,12 @@ function $SceProvider() { * {@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: + * @return {function(context, locals)} A function which represents the compiled expression: * - * * `context` – `{object}` – an object against which any expressions embedded in the strings - * are evaluated against (typically a scope object). - * * `locals` – `{object=}` – local variables context object, useful for overriding values in - * `context`. + * * `context` – `{object}` – an object against which any expressions embedded in the + * strings are evaluated against (typically a scope object). + * * `locals` – `{object=}` – local variables context object, useful for overriding values + * in `context`. */ // Shorthand delegations. @@ -17573,13 +21243,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -17588,12 +21258,15 @@ function $SceProvider() { }]; } +/* exported $SnifferProvider */ + /** * !!! This is an undocumented "private" service !!! * * @name $sniffer * @requires $window * @requires $document + * @this * * @property {boolean} history Does the browser support html5 history api ? * @property {boolean} transitions Does the browser support CSS transition events ? @@ -17605,37 +21278,32 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, - animations = false, - match; + animations = false; if (bodyStyle) { - 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) { - 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)) { - transitions = isString(bodyStyle.webkitTransition); - animations = isString(bodyStyle.webkitAnimation); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -17648,16 +21316,15 @@ function $SnifferProvider() { // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 + history: !!(hasHistoryPushState && !(android < 4) && !boxee), hasEvent: function(event) { + // Support: IE 9-11 only // 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. // 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 (event === 'input' && msie) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -17667,7 +21334,6 @@ function $SnifferProvider() { return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android @@ -17675,119 +21341,289 @@ function $SnifferProvider() { }]; } -var $compileMinErr = minErr('$compile'); - /** - * @ngdoc service - * @name $templateRequest + * ! This is a private undocumented service ! * + * @name $$taskTrackerFactory * @description - * The `$templateRequest` service runs security checks then 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, a `$compile` error will be thrown (the - * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the - * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted - * when `tpl` is of type string and `$templateCache` has the matching entry. + * A function to create `TaskTracker` instances. * - * @param {string|TrustedResourceUrl} tpl The HTTP request template URL - * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * A `TaskTracker` can keep track of pending tasks (grouped by type) and can notify interested + * parties when all pending tasks (or tasks of a specific type) have been completed. * - * @return {Promise} a promise for the HTTP response data of the given URL. + * @param {$log} log - A logger instance (such as `$log`). Used to log error during callback + * execution. * - * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + * @this */ -function $TemplateRequestProvider() { - this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { - function handleRequestFn(tpl, ignoreRequestError) { - handleRequestFn.totalPendingRequests++; +function $$TaskTrackerFactoryProvider() { + this.$get = valueFn(function(log) { return new TaskTracker(log); }); +} - // We consider the template cache holds only trusted templates, so - // there's no need to go through whitelisting again for keys that already - // are included in there. This also makes Angular accept any script - // directive, no matter its name. However, we still need to unwrap trusted - // types. - if (!isString(tpl) || !$templateCache.get(tpl)) { - tpl = $sce.getTrustedResourceUrl(tpl); - } +function TaskTracker(log) { + var self = this; + var taskCounts = {}; + var taskCallbacks = []; - var transformResponse = $http.defaults && $http.defaults.transformResponse; + var ALL_TASKS_TYPE = self.ALL_TASKS_TYPE = '$$all$$'; + var DEFAULT_TASK_TYPE = self.DEFAULT_TASK_TYPE = '$$default$$'; - if (isArray(transformResponse)) { - transformResponse = transformResponse.filter(function(transformer) { - return transformer !== defaultHttpResponseTransform; - }); - } else if (transformResponse === defaultHttpResponseTransform) { - transformResponse = null; - } + /** + * Execute the specified function and decrement the appropriate `taskCounts` counter. + * If the counter reaches 0, all corresponding `taskCallbacks` are executed. + * + * @param {Function} fn - The function to execute. + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task that is being completed. + */ + self.completeTask = completeTask; - var httpOptions = { - cache: $templateCache, - transformResponse: transformResponse - }; + /** + * Increase the task count for the specified task type (or the default task type if non is + * specified). + * + * @param {string=} [taskType=DEFAULT_TASK_TYPE] - The type of task whose count will be increased. + */ + self.incTaskCount = incTaskCount; - return $http.get(tpl, httpOptions) - ['finally'](function() { - handleRequestFn.totalPendingRequests--; - }) - .then(function(response) { - $templateCache.put(tpl, response.data); - return response.data; - }, handleError); + /** + * Execute the specified callback when all pending tasks have been completed. + * + * If there are no pending tasks, the callback is executed immediately. You can optionally limit + * the tasks that will be waited for to a specific type, by passing a `taskType`. + * + * @param {function} callback - The function to call when there are no pending tasks. + * @param {string=} [taskType=ALL_TASKS_TYPE] - The type of tasks that will be waited for. + */ + self.notifyWhenNoPendingTasks = notifyWhenNoPendingTasks; + + function completeTask(fn, taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + + try { + fn(); + } finally { + decTaskCount(taskType); + + var countForType = taskCounts[taskType]; + var countForAll = taskCounts[ALL_TASKS_TYPE]; + + // If at least one of the queues (`ALL_TASKS_TYPE` or `taskType`) is empty, run callbacks. + if (!countForAll || !countForType) { + var getNextCallback = !countForAll ? getLastCallback : getLastCallbackForType; + var nextCb; - function handleError(resp) { - if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', - tpl, resp.status, resp.statusText); + while ((nextCb = getNextCallback(taskType))) { + try { + nextCb(); + } catch (e) { + log.error(e); + } } - return $q.reject(resp); } } + } + + function decTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + if (taskCounts[taskType]) { + taskCounts[taskType]--; + taskCounts[ALL_TASKS_TYPE]--; + } + } - handleRequestFn.totalPendingRequests = 0; + function getLastCallback() { + var cbInfo = taskCallbacks.pop(); + return cbInfo && cbInfo.cb; + } - return handleRequestFn; - }]; + function getLastCallbackForType(taskType) { + for (var i = taskCallbacks.length - 1; i >= 0; --i) { + var cbInfo = taskCallbacks[i]; + if (cbInfo.type === taskType) { + taskCallbacks.splice(i, 1); + return cbInfo.cb; + } + } + } + + function incTaskCount(taskType) { + taskType = taskType || DEFAULT_TASK_TYPE; + taskCounts[taskType] = (taskCounts[taskType] || 0) + 1; + taskCounts[ALL_TASKS_TYPE] = (taskCounts[ALL_TASKS_TYPE] || 0) + 1; + } + + function notifyWhenNoPendingTasks(callback, taskType) { + taskType = taskType || ALL_TASKS_TYPE; + if (!taskCounts[taskType]) { + callback(); + } else { + taskCallbacks.push({type: taskType, cb: callback}); + } + } } -function $$TestabilityProvider() { - this.$get = ['$rootScope', '$browser', '$location', - function($rootScope, $browser, $location) { +var $templateRequestMinErr = minErr('$templateRequest'); - /** - * @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 = {}; +/** + * @ngdoc provider + * @name $templateRequestProvider + * @this + * + * @description + * Used to configure the options passed to the {@link $http} service when making a template request. + * + * For example, it can be used for specifying the "Accept" header that is sent to the server, when + * requesting a template. + */ +function $TemplateRequestProvider() { - /** - * @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); + var httpOptions; + + /** + * @ngdoc method + * @name $templateRequestProvider#httpOptions + * @description + * The options to be passed to the {@link $http} service when making the request. + * You can use this to override options such as the "Accept" header for template requests. + * + * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the + * options if not overridden here. + * + * @param {string=} value new value for the {@link $http} options. + * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter. + */ + this.httpOptions = function(val) { + if (val) { + httpOptions = val; + return this; + } + return httpOptions; + }; + + /** + * @ngdoc service + * @name $templateRequest + * + * @description + * The `$templateRequest` service runs security checks then 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, a `$compile` error will be thrown (the + * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the + * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted + * when `tpl` is of type string and `$templateCache` has the matching entry. + * + * If you want to pass custom options to the `$http` service, such as setting the Accept header you + * can configure this via {@link $templateRequestProvider#httpOptions}. + * + * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such + * as {@link ngInclude} to download and cache templates. + * + * 3rd party modules should use `$templateRequest` if their services or directives are loading + * templates. + * + * @param {string|TrustedResourceUrl} tpl The HTTP request template URL + * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty + * + * @return {Promise} a promise for the HTTP response data of the given URL. + * + * @property {number} totalPendingRequests total amount of pending template requests being downloaded. + */ + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { + + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + // We consider the template cache holds only trusted templates, so + // there's no need to go through adding the template again to the trusted + // resources for keys that already are included in there. This also makes + // AngularJS accept any script directive, no matter its name. However, we + // still need to unwrap trusted types. + if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { + tpl = $sce.getTrustedResourceUrl(tpl); + } + + var transformResponse = $http.defaults && $http.defaults.transformResponse; + + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } + + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + return $templateCache.put(tpl, response.data); + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } + + return $q.reject(resp); + } + } + + handleRequestFn.totalPendingRequests = 0; + + return handleRequestFn; + } + ]; +} + +/** @this */ +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) { + if (bindingName.indexOf(expression) !== -1) { matches.push(binding); } } @@ -17852,7 +21688,15 @@ function $$TestabilityProvider() { * @name $$testability#whenStable * * @description - * Calls the callback when $timeout and $http requests are completed. + * Calls the callback when all pending tasks are completed. + * + * Types of tasks waited for include: + * - Pending timeouts (via {@link $timeout}). + * - Pending HTTP requests (via {@link $http}). + * - In-progress route transitions (via {@link $route}). + * - Pending tasks scheduled via {@link $rootScope#$applyAsync}. + * - Pending tasks scheduled via {@link $rootScope#$evalAsync}. + * These include tasks scheduled via `$evalAsync()` indirectly (such as {@link $q} promises). * * @param {function} callback */ @@ -17864,6 +21708,9 @@ function $$TestabilityProvider() { }]; } +var $timeoutMinErr = minErr('$timeout'); + +/** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { @@ -17871,35 +21718,35 @@ function $TimeoutProvider() { var deferreds = {}; - /** - * @ngdoc service - * @name $timeout - * - * @description - * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch - * block and delegates any exceptions to - * {@link ng.$exceptionHandler $exceptionHandler} service. - * - * The return value of calling `$timeout` is a promise, which will be resolved when - * the delay has passed and the timeout function, if provided, is executed. - * - * To cancel a timeout request, call `$timeout.cancel(promise)`. - * - * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to - * synchronously flush the queue of deferred functions. - * - * If you only want a promise that will be resolved after some specified delay - * then you can call `$timeout` without the `fn` function. - * - * @param {function()=} fn A function, whose execution should be delayed. - * @param {number=} [delay=0] Delay in milliseconds. - * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise - * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. - * @param {...*=} Pass additional parameters to the executed function. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. - * - */ + /** + * @ngdoc service + * @name $timeout + * + * @description + * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch + * block and delegates any exceptions to + * {@link ng.$exceptionHandler $exceptionHandler} service. + * + * The return value of calling `$timeout` is a promise, which will be resolved when + * the delay has passed and the timeout function, if provided, is executed. + * + * To cancel a timeout request, call `$timeout.cancel(promise)`. + * + * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to + * synchronously flush the queue of deferred functions. + * + * If you only want a promise that will be resolved after some specified delay + * then you can call `$timeout` without the `fn` function. + * + * @param {function()=} fn A function, whose execution should be delayed. + * @param {number=} [delay=0] Delay in milliseconds. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. + * @param {...*=} Pass additional parameters to the executed function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. + * + */ function timeout(fn, delay, invokeApply) { if (!isFunction(fn)) { invokeApply = delay; @@ -17919,13 +21766,12 @@ function $TimeoutProvider() { } catch (e) { deferred.reject(e); $exceptionHandler(e); - } - finally { + } finally { delete deferreds[promise.$$timeoutId]; } if (!skipApply) $rootScope.$apply(); - }, delay); + }, delay, '$timeout'); promise.$$timeoutId = timeoutId; deferreds[timeoutId] = deferred; @@ -17934,25 +21780,37 @@ function $TimeoutProvider() { } - /** - * @ngdoc method - * @name $timeout#cancel - * - * @description - * Cancels a task associated with the `promise`. As a result of this, the promise will be - * resolved with a rejection. - * - * @param {Promise=} promise Promise returned by the `$timeout` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully - * canceled. - */ + /** + * @ngdoc method + * @name $timeout#cancel + * + * @description + * Cancels a task associated with the `promise`. As a result of this, the promise will be + * resolved with a rejection. + * + * @param {Promise=} promise Promise returned by the `$timeout` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully + * canceled. + */ timeout.cancel = function(promise) { - if (promise && promise.$$timeoutId in deferreds) { - deferreds[promise.$$timeoutId].reject('canceled'); - delete deferreds[promise.$$timeoutId]; - return $browser.defer.cancel(promise.$$timeoutId); + if (!promise) return false; + + if (!promise.hasOwnProperty('$$timeoutId')) { + throw $timeoutMinErr('badprom', + '`$timeout.cancel()` called with a promise that was not generated by `$timeout()`.'); } - return false; + + if (!deferreds.hasOwnProperty(promise.$$timeoutId)) return false; + + var id = promise.$$timeoutId; + var deferred = deferreds[id]; + + // Timeout cancels should not report an unhandled promise. + markQExceptionHandled(deferred.promise); + deferred.reject('canceled'); + delete deferreds[id]; + + return $browser.defer.cancel(id); }; return timeout; @@ -17966,9 +21824,16 @@ function $TimeoutProvider() { // doesn't know about mocked locations and resolves URLs to the real document - which is // exactly the behavior needed here. There is little value is mocking these out for this // service. -var urlParsingNode = document.createElement("a"); +var urlParsingNode = window.document.createElement('a'); var originUrl = urlResolve(window.location.href); +var baseUrlParsingNode; +urlParsingNode.href = 'http://[::1]'; + +// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview) +// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets +// when parsed out of an anchor element. +var ipv6InBrackets = urlParsingNode.hostname === '[::1]'; /** * @@ -17979,7 +21844,7 @@ var originUrl = urlResolve(window.location.href); * URL will be resolved into an absolute URL in the context of the application document. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * compatibility - Safari 1+, Mozilla 1+ etc. See * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE @@ -17999,42 +21864,51 @@ var originUrl = urlResolve(window.location.href); * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ * * @kind function - * @param {string} url The URL to be parsed. + * @param {string|object} url The URL to be parsed. If `url` is not a string, it will be returned + * unchanged. * @description Normalizes and parses a URL. * @returns {object} Returns the normalized URL as a dictionary. * - * | member name | Description | - * |---------------|----------------| + * | member name | Description | + * |---------------|------------------------------------------------------------------------| * | href | A normalized version of the provided URL if it was not an absolute URL | - * | protocol | The protocol including the trailing colon | + * | protocol | The protocol without the trailing colon | * | host | The host and port (if the port is non-default) of the normalizedUrl | * | search | The search params, minus the question mark | - * | hash | The hash string, minus the hash symbol - * | hostname | The hostname - * | port | The port, without ":" - * | pathname | The pathname, beginning with "/" + * | hash | The hash string, minus the hash symbol | + * | hostname | The hostname | + * | port | The port, without ":" | + * | pathname | The pathname, beginning with "/" | * */ function urlResolve(url) { + if (!isString(url)) return url; + var href = url; + // Support: IE 9-11 only if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. - urlParsingNode.setAttribute("href", href); + urlParsingNode.setAttribute('href', href); href = urlParsingNode.href; } urlParsingNode.setAttribute('href', href); - // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + var hostname = urlParsingNode.hostname; + + if (!ipv6InBrackets && hostname.indexOf(':') > -1) { + hostname = '[' + hostname + ']'; + } + return { href: urlParsingNode.href, protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '', host: urlParsingNode.host, search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '', hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '', - hostname: urlParsingNode.hostname, + hostname: hostname, port: urlParsingNode.port, pathname: (urlParsingNode.pathname.charAt(0) === '/') ? urlParsingNode.pathname @@ -18043,26 +21917,107 @@ function urlResolve(url) { } /** - * Parse a request URL and determine whether this is a same-origin request as the application document. + * Parse a request URL and determine whether this is a same-origin request as the application + * document. * * @param {string|object} requestUrl The url of the request as a string that will be resolved * or a parsed URL object. * @returns {boolean} Whether the request is for the same origin as the application document. */ function urlIsSameOrigin(requestUrl) { - var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; - return (parsed.protocol === originUrl.protocol && - parsed.host === originUrl.host); + return urlsAreSameOrigin(requestUrl, originUrl); +} + +/** + * Parse a request URL and determine whether it is same-origin as the current document base URL. + * + * Note: The base URL is usually the same as the document location (`location.href`) but can + * be overriden by using the `` tag. + * + * @param {string|object} requestUrl The url of the request as a string that will be resolved + * or a parsed URL object. + * @returns {boolean} Whether the URL is same-origin as the document base URL. + */ +function urlIsSameOriginAsBaseUrl(requestUrl) { + return urlsAreSameOrigin(requestUrl, getBaseUrl()); +} + +/** + * Create a function that can check a URL's origin against a list of allowed/trusted origins. + * The current location's origin is implicitly trusted. + * + * @param {string[]} trustedOriginUrls - A list of URLs (strings), whose origins are trusted. + * + * @returns {Function} - A function that receives a URL (string or parsed URL object) and returns + * whether it is of an allowed origin. + */ +function urlIsAllowedOriginFactory(trustedOriginUrls) { + var parsedAllowedOriginUrls = [originUrl].concat(trustedOriginUrls.map(urlResolve)); + + /** + * Check whether the specified URL (string or parsed URL object) has an origin that is allowed + * based on a list of trusted-origin URLs. The current location's origin is implicitly + * trusted. + * + * @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be + * resolved or a parsed URL object). + * + * @returns {boolean} - Whether the specified URL is of an allowed origin. + */ + return function urlIsAllowedOrigin(requestUrl) { + var parsedUrl = urlResolve(requestUrl); + return parsedAllowedOriginUrls.some(urlsAreSameOrigin.bind(null, parsedUrl)); + }; +} + +/** + * Determine if two URLs share the same origin. + * + * @param {string|Object} url1 - First URL to compare as a string or a normalized URL in the form of + * a dictionary object returned by `urlResolve()`. + * @param {string|object} url2 - Second URL to compare as a string or a normalized URL in the form + * of a dictionary object returned by `urlResolve()`. + * + * @returns {boolean} - True if both URLs have the same origin, and false otherwise. + */ +function urlsAreSameOrigin(url1, url2) { + url1 = urlResolve(url1); + url2 = urlResolve(url2); + + return (url1.protocol === url2.protocol && + url1.host === url2.host); +} + +/** + * Returns the current document base URL. + * @returns {string} + */ +function getBaseUrl() { + if (window.document.baseURI) { + return window.document.baseURI; + } + + // `document.baseURI` is available everywhere except IE + if (!baseUrlParsingNode) { + baseUrlParsingNode = window.document.createElement('a'); + baseUrlParsingNode.href = '.'; + + // Work-around for IE bug described in Implementation Notes. The fix in `urlResolve()` is not + // suitable here because we need to track changes to the base URL. + baseUrlParsingNode = baseUrlParsingNode.cloneNode(false); + } + return baseUrlParsingNode.href; } /** * @ngdoc service * @name $window + * @this * * @description * A reference to the browser's `window` object. While `window` * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the + * it is a global variable. In AngularJS we always refer to it through the * `$window` service, so it may be overridden, removed or mocked for testing. * * Expressions, like the one defined for the `ngClick` directive in the example @@ -18071,7 +22026,7 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - + +
+ +

{{title}}

+ +

{{title | uppercase}}

+
+
+
*/ var uppercaseFilter = valueFn(uppercase); @@ -19159,24 +23297,25 @@ var uppercaseFilter = valueFn(uppercase); * @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, 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|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 + * 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, string or number, as specified by + * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported + * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, + * it is converted to a string. + * + * @param {Array|ArrayLike|string|number} input - Array/array-like, 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 * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, * the input will be returned unchanged. - * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin` - * indicates an offset from the end of `input`. Defaults to `0`. - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. + * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index, + * `begin` indicates an offset from the end of `input`. Defaults to `0`. + * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had + * less than `limit` elements. * * @example - +
- +
@@ -19345,58 +23576,78 @@ function limitToFilter() {
Name Phone Number
+ + angular.module('orderByExample1', []) + .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} + ]; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var names = element.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by age in reverse order', function() { + expect(names.get(0).getText()).toBe('Adam'); + expect(names.get(1).getText()).toBe('Julie'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('John'); + }); +
+ *
* - * The predicate and reverse parameters can be controlled dynamically through scope properties, - * as shown in the next example. * @example - + * ### Changing parameters dynamically + * + * All parameters can be changed dynamically. The next example shows how you can make the columns of + * a table sortable, by binding the `expression` and `reverse` parameters to scope properties. + * + - -
-
Sorting predicate = {{predicate}}; reverse = {{reverse}}
+
Sort by = {{propertyName}}; reverse = {{reverse}}

- [ unsorted ] - + +
+
- + @@ -19404,67 +23655,351 @@ function limitToFilter() {
- Name - + + - Phone Number - + + - Age - + +
{{friend.name}} {{friend.phone}} {{friend.age}}
+ + angular.module('orderByExample2', []) + .controller('ExampleController', ['$scope', function($scope) { + var 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.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = friends; + + $scope.sortBy = function(propertyName) { + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); +
+ *
+ * + * @example + * ### Using `orderBy` inside a controller * - * 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. + * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and + * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory + * and retrieve the `orderBy` filter with `$filter('orderBy')`.) * - * Example: + + +
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+ +
+ + + + + + + + + + + +
+ + + + + + + + +
{{friend.name}}{{friend.phone}}{{friend.age}}
+
+
+ + angular.module('orderByExample3', []) + .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) { + var 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.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + + $scope.sortBy = function(propertyName) { + $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName) + ? !$scope.reverse : false; + $scope.propertyName = propertyName; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + }; + }]); + + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + + + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + +
+ *
* * @example - - -
- - - - - - - - - - - -
Name - (^)Phone NumberAge
{{friend.name}}{{friend.phone}}{{friend.age}}
-
-
+ * ### Using a custom comparator + * + * If you have very specific requirements about the way items are sorted, you can pass your own + * comparator function. For example, you might need to compare some strings in a locale-sensitive + * way. (When specifying a custom comparator, you also need to pass a value for the `reverse` + * argument - passing `false` retains the default sorting order, i.e. ascending.) + * + + +
+
+

Locale-sensitive Comparator

+ + + + + + + + + +
NameFavorite Letter
{{friend.name}}{{friend.favoriteLetter}}
+
+
+

Default Comparator

+ + + + + + + + + +
NameFavorite Letter
{{friend.name}}{{friend.favoriteLetter}}
+
+
+
+ + angular.module('orderByExample4', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', favoriteLetter: 'Ä'}, + {name: 'Mary', favoriteLetter: 'Ü'}, + {name: 'Mike', favoriteLetter: 'Ö'}, + {name: 'Adam', favoriteLetter: 'H'}, + {name: 'Julie', favoriteLetter: 'Z'} + ]; + + $scope.localeSensitiveComparator = function(v1, v2) { + // If we don't get strings, just compare by index + if (v1.type !== 'string' || v2.type !== 'string') { + return (v1.index < v2.index) ? -1 : 1; + } - - 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); - }]); - -
+ // Compare strings alphabetically, taking locale into account + return v1.value.localeCompare(v2.value); + }; + }]); +
+ + .friends-container { + display: inline-block; + margin: 0 30px; + } + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + + // Element locators + var container = element(by.css('.custom-comparator')); + var names = container.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by favorite letter (in correct alphabetical order)', function() { + expect(names.get(0).getText()).toBe('John'); + expect(names.get(1).getText()).toBe('Adam'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('Julie'); + }); + + + * */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse) { - return function(array, sortPredicate, reverseOrder) { + return function(array, sortPredicate, reverseOrder, compareFn) { - if (!(isArrayLike(array))) return array; + if (array == null) return array; + if (!isArrayLike(array)) { + throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array); + } if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } if (sortPredicate.length === 0) { sortPredicate = ['+']; } - var predicates = processPredicates(sortPredicate, reverseOrder); - // Add a predicate at the end that evaluates to the element index. This makes the - // sort stable as it works as a tie-breaker when all the input predicates cannot - // distinguish between two elements. - predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1}); + var predicates = processPredicates(sortPredicate); + + var descending = reverseOrder ? -1 : 1; + + // Define the `compare()` function. Use a default comparator if none is specified. + var compare = isFunction(compareFn) ? compareFn : defaultCompare; // The next three lines are a version of a Swartzian Transform idiom from Perl // (sometimes called the Decorate-Sort-Undecorate idiom) @@ -19476,8 +24011,12 @@ function orderByFilter($parse) { return array; function getComparisonObject(value, index) { + // NOTE: We are adding an extra `tieBreaker` value based on the element's index. + // This will be used to keep the sort stable when none of the input predicates can + // distinguish between two elements. return { value: value, + tieBreaker: {value: index, type: 'number', index: index}, predicateValues: predicates.map(function(predicate) { return getPredicateValue(predicate.get(value), index); }) @@ -19485,25 +24024,26 @@ function orderByFilter($parse) { } function doComparison(v1, v2) { - var result = 0; - for (var index=0, length = predicates.length; index < length; ++index) { - result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending; - if (result) break; + for (var i = 0, ii = predicates.length; i < ii; i++) { + var result = compare(v1.predicateValues[i], v2.predicateValues[i]); + if (result) { + return result * predicates[i].descending * descending; + } } - return result; + + return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending; } }; - function processPredicates(sortPredicate, reverseOrder) { - reverseOrder = reverseOrder ? -1 : 1; - return sortPredicate.map(function(predicate) { + function processPredicates(sortPredicates) { + return sortPredicates.map(function(predicate) { var descending = 1, get = identity; if (isFunction(predicate)) { get = predicate; } else if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-' ? -1 : 1; + if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) { + descending = predicate.charAt(0) === '-' ? -1 : 1; predicate = predicate.substring(1); } if (predicate !== '') { @@ -19514,7 +24054,7 @@ function orderByFilter($parse) { } } } - return { get: get, descending: descending * reverseOrder }; + return {get: get, descending: descending}; }); } @@ -19529,9 +24069,9 @@ function orderByFilter($parse) { } } - function objectValue(value, index) { + function objectValue(value) { // If `valueOf` is a valid function use that - if (typeof value.valueOf === 'function') { + if (isFunction(value.valueOf)) { value = value.valueOf(); if (isPrimitive(value)) return value; } @@ -19540,32 +24080,51 @@ function orderByFilter($parse) { value = value.toString(); if (isPrimitive(value)) return value; } - // We have a basic object so we use the position of the object in the collection - return index; + + return value; } function getPredicateValue(value, index) { var type = typeof value; if (value === null) { - type = 'string'; - value = 'null'; - } else if (type === 'string') { - value = value.toLowerCase(); + type = 'null'; } else if (type === 'object') { - value = objectValue(value, index); + value = objectValue(value); } - return { value: value, type: type }; + return {value: value, type: type, index: index}; } - function compare(v1, v2) { + function defaultCompare(v1, v2) { var result = 0; - if (v1.type === v2.type) { - if (v1.value !== v2.value) { - result = v1.value < v2.value ? -1 : 1; + var type1 = v1.type; + var type2 = v2.type; + + if (type1 === type2) { + var value1 = v1.value; + var value2 = v2.value; + + if (type1 === 'string') { + // Compare strings case-insensitively + value1 = value1.toLowerCase(); + value2 = value2.toLowerCase(); + } else if (type1 === 'object') { + // For basic objects, use the position of the object + // in the collection instead of the value + if (isObject(value1)) value1 = v1.index; + if (isObject(value2)) value2 = v2.index; + } + + if (value1 !== value2) { + result = value1 < value2 ? -1 : 1; } } else { - result = v1.type < v2.type ? -1 : 1; + result = (type1 === 'undefined') ? 1 : + (type2 === 'undefined') ? -1 : + (type1 === 'null') ? 1 : + (type2 === 'null') ? -1 : + (type1 < type2) ? -1 : 1; } + return result; } } @@ -19586,12 +24145,10 @@ function ngDirective(directive) { * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `Add Item` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -19622,10 +24179,10 @@ var htmlAnchorDirective = valueFn({ * @priority 99 * * @description - * Using Angular markup like `{{hash}}` in an href attribute will + * Using AngularJS markup like `{{hash}}` in an href attribute will * 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 + * AngularJS has a chance to replace the `{{hash}}` markup with its + * value. Until AngularJS replaces the markup the link will be broken * and will most likely return a 404 error. The `ngHref` directive * solves this problem. * @@ -19645,7 +24202,7 @@ var htmlAnchorDirective = valueFn({ * @example * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes * in links and their different behaviors: - +
link 1 (link, don't reload)
@@ -19673,7 +24230,7 @@ var htmlAnchorDirective = valueFn({ element(by.id('link-3')).click(); - // At this point, we navigate away from an Angular page, so we need + // At this point, we navigate away from an AngularJS page, so we need // to use browser.driver to get the base webdriver. browser.wait(function() { @@ -19702,7 +24259,7 @@ var htmlAnchorDirective = valueFn({ element(by.id('link-6')).click(); - // At this point, we navigate away from an Angular page, so we need + // At this point, we navigate away from an AngularJS page, so we need // to use browser.driver to get the base webdriver. browser.wait(function() { return browser.driver.getCurrentUrl().then(function(url) { @@ -19721,9 +24278,9 @@ var htmlAnchorDirective = valueFn({ * @priority 99 * * @description - * Using Angular markup like `{{hash}}` in a `src` attribute doesn't + * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside + * text `{{hash}}` until AngularJS replaces the expression inside * `{{hash}}`. The `ngSrc` directive solves this problem. * * The buggy way to write it: @@ -19747,9 +24304,9 @@ var htmlAnchorDirective = valueFn({ * @priority 99 * * @description - * Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't + * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't * work right: The browser will fetch from the URL with the literal - * text `{{hash}}` until Angular replaces the expression inside + * text `{{hash}}` until AngularJS replaces the expression inside * `{{hash}}`. The `ngSrcset` directive solves this problem. * * The buggy way to write it: @@ -19774,27 +24331,15 @@ var htmlAnchorDirective = valueFn({ * * @description * - * This directive sets the `disabled` attribute on the element if the + * This directive sets the `disabled` attribute on the element (typically a form control, + * e.g. `input`, `button`, `select` etc.) if the * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy. * * A special directive is necessary because we cannot use interpolation inside the `disabled` - * attribute. The following example would make the button enabled on Chrome/Firefox - * but not on older IEs: - * - * ```html - * - *
- * - *
- * ``` - * - * This is because the HTML specification does not require browsers to preserve the values of - * boolean attributes such as `disabled` (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * * @example - +
@@ -19808,7 +24353,6 @@ var htmlAnchorDirective = valueFn({
* - * @element INPUT * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy, * then the `disabled` attribute will be set on the element */ @@ -19826,26 +24370,20 @@ var htmlAnchorDirective = valueFn({ * Note that this directive should not be used together with {@link ngModel `ngModel`}, * as this can lead to unexpected behavior. * - * ### Why do we need `ngChecked`? + * A special directive is necessary because we cannot use interpolation inside the `checked` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. * @example - + -
- +
+
it('should check both checkBoxes', function() { - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy(); - element(by.model('master')).click(); - expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy(); + expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy(); + element(by.model('leader')).click(); + expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy(); });
@@ -19863,18 +24401,19 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as readonly. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngReadonly` directive solves this problem for the `readonly` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy. + * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on + * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information. + * + * A special directive is necessary because we cannot use interpolation inside the `readonly` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example - +
- +
it('should toggle readonly attr', function() { @@ -19898,16 +24437,21 @@ var htmlAnchorDirective = valueFn({ * @priority 100 * * @description - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as selected. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngSelected` directive solves this problem for the `selected` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. + * + * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy. + * + * A special directive is necessary because we cannot use interpolation inside the `selected` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * + *
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only + * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you + * should not use `ngSelected` on the options, as `ngModel` will set the select value and + * selected options. + *
* * @example - +

+
- Show/Hide me + List +
    +
  • Apple
  • +
  • Orange
  • +
  • Durian
  • +
@@ -19970,7 +24523,7 @@ var ngAttributeAliasDirectives = {}; // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported - if (propName == "multiple") return; + if (propName === 'multiple') return; function defaultLinkFn(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -20007,10 +24560,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { 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) == "/") { + 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])); + attr.$set('ngPattern', new RegExp(match[1], match[2])); return; } } @@ -20026,7 +24579,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { // ng-src, ng-srcset, ng-href are interpolated forEach(['src', 'srcset', 'href'], function(attrName) { var normalized = directiveNormalize('ng-' + attrName); - ngAttributeAliasDirectives[normalized] = function() { + ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) { return { priority: 99, // it needs to run after the attributes are interpolated link: function(scope, element, attr) { @@ -20040,6 +24593,10 @@ forEach(['src', 'srcset', 'href'], function(attrName) { propName = null; } + // We need to sanitize the url at least once, in case it is a constant + // non-interpolated attribute. + attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized])); + attr.$observe(normalized, function(value) { if (!value) { if (attrName === 'href') { @@ -20050,28 +24607,32 @@ forEach(['src', 'srcset', 'href'], function(attrName) { attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. + // We use attr[attrName] value since $set might have sanitized the url. if (msie && propName) element.prop(propName, attr[name]); }); } }; - }; + }]; }); -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, + $getControls: valueFn([]), $$renameControl: nullFormRenameControl, $removeControl: noop, $setValidity: noop, $setDirty: noop, $setPristine: noop, - $setSubmitted: noop + $setSubmitted: noop, + $$setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -20086,17 +24647,23 @@ function nullFormRenameControl(control, name) { * @property {boolean} $dirty True if user has already interacted with the form. * @property {boolean} $valid True if all of the containing forms and controls are valid. * @property {boolean} $invalid True if at least one containing control or form is invalid. - * @property {boolean} $pending True if at least one containing control or form is pending. * @property {boolean} $submitted True if user has submitted the form even if its invalid. * - * @property {Object} $error Is an object hash, containing references to controls or - * forms with failing validators, where: + * @property {Object} $pending An object hash, containing references to controls or forms with + * pending validators, where: + * + * - keys are validations tokens (error names). + * - values are arrays of controls or forms that have a pending validator for the given error name. + * + * See {@link form.FormController#$error $error} for a list of built-in validation tokens. + * + * @property {Object} $error 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 have a failing validator for given error name. + * - values are arrays of controls or forms that have a failing validator for the given error name. * * Built-in validation tokens: - * * - `email` * - `max` * - `maxlength` @@ -20122,22 +24689,28 @@ function nullFormRenameControl(control, name) { */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - 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; - form.$$parentForm = nullFormCtrl; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + + this.$$element = $element; + this.$$animate = $animate; + + setupValidity(this); +} +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -20149,11 +24722,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -20166,11 +24739,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -20188,34 +24761,58 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * However, if the method is used programmatically, for example by adding dynamically created controls, * or controls that have been previously removed without destroying their corresponding DOM element, - * it's the developers responsiblity to make sure the current state propagates to the parent form. + * it's the developers responsibility to make sure the current state propagates to the parent form. * * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ - form.$addControl = function(control) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, + + /** + * @ngdoc method + * @name form.FormController#$getControls + * @returns {Array} the controls that are currently part of this form + * + * @description + * This method returns a **shallow copy** of the controls that are currently part of this form. + * The controls can be instances of {@link form.FormController `FormController`} + * ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}. + * If you need access to the controls of child-forms, you have to call `$getControls()` + * recursively on them. + * This can be used for example to iterate over all controls to validate them. + * + * The controls can be accessed normally, but adding to, or removing controls from the array has + * no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and + * {@link form.FormController#$removeControl `$removeControl()`} for this use-case. + * Likewise, adding a control to, or removing a control from the form is not reflected + * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time + * you need access to the controls. + */ + $getControls: function() { + return shallowCopy(this.$$controls); + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -20233,60 +24830,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; - } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); - - arrayRemove(controls, control); + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; + } + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + + arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -20298,13 +24861,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - form.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -20313,22 +24876,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its pristine state. * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. + * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes + * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` + * state to false. + * + * This method will also propagate to all the controls contained in this form. * * 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.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -20343,43 +24908,110 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * 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) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method * @name form.FormController#$setSubmitted * * @description - * Sets the form to its submitted state. + * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and + * parent forms of the form. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + var rootForm = this; + while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) { + rootForm = rootForm.$$parentForm; + } + rootForm.$$setSubmitted(); + }, + + $$setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + forEach(this.$$controls, function(control) { + if (control.$$setSubmitted) { + control.$$setSubmitted(); + } + }); + } +}; /** - * @ngdoc directive - * @name ngForm - * @restrict EAC + * @ngdoc method + * @name form.FormController#$setValidity * * @description - * Nestable alias of {@link ng.directive:form `form`} directive. HTML - * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a - * sub-group of controls needs to be determined. - * - * Note: the purpose of `ngForm` is to group controls, - * but not to be a replacement for the `
` tag with all of its capabilities - * (e.g. posting to the server, ...). - * - * @param {string=} ngForm|name Name of the form. If specified, the form controller will be published into - * related scope, under this name. - * + * Change the validity state of the form, and notify the parent form (if any). + * + * Application developers will rarely need to call this method directly. It is used internally, by + * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a + * control's validity state to the parent `FormController`. + * + * @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` classes and can be bound to as `{{ someForm.$error.myError }}`. + * @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 AngularJS when validators do not run because of parse errors and when + * `$asyncValidators` do not run because any of the `$validators` failed. + * @param {NgModelController | FormController} controller - The controller whose validity state is + * triggering the change. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); + +/** + * @ngdoc directive + * @name ngForm + * @restrict EAC + * + * @description + * Helper directive that makes it possible to create control groups inside a + * {@link ng.directive:form `form`} directive. + * These "child forms" can be used, for example, to determine the validity of a sub-group of + * controls. + * + *
+ * **Note**: `ngForm` cannot be used as a replacement for ``, because it lacks its + * [built-in HTML functionality](https://html.spec.whatwg.org/#the-form-element). + * Specifically, you cannot submit `ngForm` like a `` tag. That means, + * you cannot send data to the server with `ngForm`, or integrate it with + * {@link ng.directive:ngSubmit `ngSubmit`}. + *
+ * + * @param {string=} ngForm|name Name of the form. If specified, the form controller will + * be published into the related scope, under this name. + * */ /** @@ -20394,19 +25026,15 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * If the `name` attribute is specified, the form controller is published onto the current scope under * this name. * - * # Alias: {@link ng.directive:ngForm `ngForm`} + * ## Alias: {@link ng.directive:ngForm `ngForm`} * - * In Angular, forms can be nested. This means that the outer form is valid when all of the child + * In AngularJS, forms can be nested. This means that the outer form is valid when all of the child * forms are valid as well. However, browsers do not allow nesting of `` elements, so - * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to - * `` but can be nested. This allows you to have nested forms, which is very useful when - * using Angular validation directives in forms that are dynamically generated using the - * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name` - * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an - * `ngForm` directive and nest these in an outer `form` element. - * + * AngularJS provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to + * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group + * of controls needs to be determined. * - * # CSS classes + * ## CSS classes * - `ng-valid` is set if the form is valid. * - `ng-invalid` is set if the form is invalid. * - `ng-pending` is set if the form is pending. @@ -20417,14 +25045,14 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Keep in mind that ngAnimate can detect each of these classes when added and removed. * * - * # Submitting a form and preventing the default action + * ## Submitting a form and preventing the default action * - * Since the role of forms in client-side Angular applications is different than in classical + * Since the role of forms in client-side AngularJS applications is different than in classical * roundtrip apps, it is desirable for the browser not to translate the form submission into a full * page reload that sends the data to the server. Instead some javascript logic should be triggered * to handle the form submission in an application-specific way. * - * For this reason, Angular prevents the default action (form submission to the server) unless the + * For this reason, AngularJS prevents the default action (form submission to the server) unless the * `` element has an `action` attribute specified. * * You can use one of the following two ways to specify what javascript method should be called when @@ -20450,8 +25078,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * submitted. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * - * ## Animation Hooks - * + * @animations * Animations in ngForm are triggered when any of the associated CSS classes are added and removed. * These classes are: `.ng-pristine`, `.ng-dirty`, `.ng-invalid` and `.ng-valid` as well as any * other validations that are performed within the form. Animations in ngForm are similar to how @@ -20475,7 +25102,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * * @example - + - +
@@ -21008,7 +25764,6 @@ var inputType = { var value = element(by.binding('example.value | date: "HH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21053,13 +25808,17 @@ var inputType = { * 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. + * The model must always be a Date object, otherwise AngularJS will throw an error. * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string. * + * The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week, + * due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the + * year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates + * * 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} ngModel Assignable AngularJS 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##). You can also use interpolation inside this @@ -21077,7 +25836,7 @@ var inputType = { * @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 + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21113,7 +25872,6 @@ var inputType = { var value = element(by.binding('example.value | date: "yyyy-Www"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21156,7 +25914,7 @@ var inputType = { * 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. + * The model must always be a Date object, otherwise AngularJS 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. @@ -21164,7 +25922,7 @@ var inputType = { * 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} ngModel Assignable AngularJS 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). You can also use interpolation inside this @@ -21183,7 +25941,7 @@ var inputType = { * @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 + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21217,7 +25975,6 @@ var inputType = { var value = element(by.binding('example.value | date: "yyyy-MM"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21262,12 +26019,16 @@ var inputType = { * error if not a valid number. * *
- * The model must always be of type `number` otherwise Angular will throw an error. + * The model must always be of type `number` otherwise AngularJS will throw an error. * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt} * error docs for more information and an example of how to convert your model if necessary. *
* - * ## Issues with HTML5 constraint validation + * + * + * @knownIssue + * + * ### HTML5 constraint validation and `allowInvalid` * * In browsers that follow the * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29), @@ -21276,11 +26037,32 @@ var inputType = { * which means the view / model values in `ngModel` and subsequently the scope value * will also be an empty string. * + * @knownIssue * - * @param {string} ngModel Assignable angular expression to data-bind to. + * ### Large numbers and `step` validation + * + * The `step` validation will not work correctly for very large numbers (e.g. 9999999999) due to + * Javascript's arithmetic limitations. If you need to handle large numbers, purpose-built + * libraries (e.g. https://github.com/MikeMcl/big.js/), can be included into AngularJS by + * {@link guide/forms#modifying-built-in-validators overwriting the validators} + * for `number` and / or `step`, or by {@link guide/forms#custom-validation applying custom validators} + * to an `input[text]` element. The source for `input[number]` type can be used as a starting + * point for both implementations. + * + * @param {string} ngModel Assignable AngularJS 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`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @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 @@ -21293,8 +26075,8 @@ var inputType = { * @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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21302,7 +26084,7 @@ var inputType = { * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to * start at the index of the last search's match, thus not taking the whole input value into * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21377,7 +26159,7 @@ var inputType = { * the built-in validators (see the {@link guide/forms Forms guide}) *
* - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS 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. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to @@ -21391,8 +26173,8 @@ var inputType = { * @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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21400,7 +26182,7 @@ var inputType = { * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to * start at the index of the last search's match, thus not taking the whole input value into * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21472,11 +26254,13 @@ var inputType = { * *
* **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}) + * used in Chromium, which may not fulfill your app's requirements. + * If you need stricter (e.g. requiring a top-level domain), or more relaxed validation + * (e.g. allowing IPv6 address literals) you can use `ng-pattern` or + * modify the built-in validators (see the {@link guide/forms Forms guide}). *
* - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS 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. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to @@ -21490,8 +26274,8 @@ var inputType = { * @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. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21499,7 +26283,7 @@ var inputType = { * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to * start at the index of the last search's match, thus not taking the whole input value into * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21567,14 +26351,41 @@ var inputType = { * @description * HTML radio button. * - * @param {string} ngModel Assignable angular expression to data-bind to. + * **Note:**
+ * All inputs controlled by {@link ngModel ngModel} (including those of type `radio`) will use the + * value of their `name` attribute to determine the property under which their + * {@link ngModel.NgModelController NgModelController} will be published on the parent + * {@link form.FormController FormController}. Thus, if you use the same `name` for multiple + * inputs of a form (e.g. a group of radio inputs), only _one_ `NgModelController` will be + * published on the parent `FormController` under that name. The rest of the controllers will + * continue to work as expected, but you won't be able to access them as properties on the parent + * `FormController`. + * + *
+ *

+ * In plain HTML forms, the `name` attribute is used to identify groups of radio inputs, so + * that the browser can manage their state (checked/unchecked) based on the state of other + * inputs in the same group. + *

+ *

+ * In AngularJS forms, this is not necessary. The input's state will be updated based on the + * value of the underlying model data. + *

+ *
+ * + *
+ * If you omit the `name` attribute on a radio input, `ngModel` will automatically assign it a + * unique name. + *
+ * + * @param {string} ngModel Assignable AngularJS expression to data-bind to. * @param {string} value The value to which the `ngModel` expression should be set when selected. * Note that `value` only supports `string` values, i.e. the scope model needs to be a string, * too. Use `ngValue` if you need complex models (`number`, `object`, ...). * @param {string=} name Property name of the form under which the control is published. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. - * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio + * @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio * is selected. Should be used instead of the `value` attribute if you need * a non-string `ngModel` (`boolean`, `array`, ...). * @@ -21612,19 +26423,140 @@ var inputType = {
it('should change state', function() { + var inputs = element.all(by.model('color.name')); var color = element(by.binding('color.name')); expect(color.getText()).toContain('blue'); - element.all(by.model('color.name')).get(0).click(); - + inputs.get(0).click(); expect(color.getText()).toContain('red'); + + inputs.get(1).click(); + expect(color.getText()).toContain('green'); });
*/ 'radio': radioInputType, + /** + * @ngdoc input + * @name input[range] + * + * @description + * Native range input with validation and transformation. + * + * The model for the range input must always be a `Number`. + * + * IE9 and other browsers that do not support the `range` type fall back + * to a text input without any default values for `min`, `max` and `step`. Model binding, + * validation and number parsing are nevertheless supported. + * + * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` + * in a way that never allows the input to hold an invalid value. That means: + * - any non-numerical value is set to `(max + min) / 2`. + * - any numerical value that is less than the current min val, or greater than the current max val + * is set to the min / max val respectively. + * - additionally, the current `step` is respected, so the nearest value that satisfies a step + * is used. + * + * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) + * for more info. + * + * This has the following consequences for AngularJS: + * + * Since the element value should always reflect the current model value, a range input + * will set the bound ngModel expression to the value that the browser has set for the + * input element. For example, in the following input ``, + * if the application sets `model.value = null`, the browser will set the input to `'50'`. + * AngularJS will then set the model to `50`, to prevent input and model value being out of sync. + * + * That means the model for range will immediately be set to `50` after `ngModel` has been + * initialized. It also means a range input can never have the required error. + * + * This does not only affect changes to the model value, but also to the values of the `min`, + * `max`, and `step` attributes. When these change in a way that will cause the browser to modify + * the input value, AngularJS will also update the model value. + * + * Automatic value adjustment also means that a range input element can never have the `required`, + * `min`, or `max` errors. + * + * However, `step` is currently only fully implemented by Firefox. Other browsers have problems + * when the step value changes dynamically - they do not adjust the element value correctly, but + * instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step` + * error on the input, and set the model to `undefined`. + * + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do + * not set the `min` and `max` attributes, which means that the browser won't automatically adjust + * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. + * + * @param {string} ngModel Assignable AngularJS 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 to ensure that the value entered is greater + * than `min`. Can be interpolated. + * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. + * Can be interpolated. + * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` + * Can be interpolated. + * @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due + * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. + * + * @example + + + + + + Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} + +
+
+ + * ## Range Input with ngMin & ngMax attributes + + * @example + + + +
+ Model as range: +
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}} +
+
+
+ + */ + 'range': rangeInputType, /** * @ngdoc input @@ -21633,11 +26565,11 @@ var inputType = { * @description * HTML checkbox. * - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS expression to data-bind to. * @param {string=} name Property name of the form under which the control is published. * @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 + * @param {string=} ngChange AngularJS expression to be executed when input changes due to user * interaction with the input element. * * @example @@ -21704,22 +26636,34 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); - // In composition mode, users are still inputing intermediate text buffer, + // In composition mode, users are still inputting intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); + // Support: IE9+ + element.on('compositionupdate', function(ev) { + // End composition when ev.data is empty string on 'compositionupdate' event. + // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate' + // instead of 'compositionend'. + if (isUndefined(ev.data) || ev.data === '') { + composing = false; + } + }); + element.on('compositionend', function() { composing = false; listener(); }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -21749,8 +26693,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { if ($sniffer.hasEvent('input')) { element.on('input', listener); } else { - var timeout; - var deferListener = function(ev, input, origValue) { if (!timeout) { timeout = $browser.defer(function() { @@ -21762,7 +26704,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }; - element.on('keydown', function(event) { + element.on('keydown', /** @this */ function(event) { var key = event.keyCode; // ignore @@ -21772,9 +26714,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { deferListener(event, this, this.value); }); - // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it + // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it if ($sniffer.hasEvent('paste')) { - element.on('paste cut', deferListener); + element.on('paste cut drop', deferListener); } } @@ -21782,6 +26724,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { // or form autocomplete on newer browser, we need "change" event to catch it element.on('change', listener); + // Some native input types (date-family) have the ability to change validity without + // firing any input/change events. + // For these event types, when native validators are present and the browser supports the type, + // check for validity changes on various DOM events. + if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) { + element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) { + if (!timeout) { + var validity = this[VALIDITY_STATE_PROPERTY]; + var origBadInput = validity.badInput; + var origTypeMismatch = validity.typeMismatch; + timeout = $browser.defer(function() { + timeout = null; + if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) { + listener(ev); + } + }); + } + }); + } + ctrl.$render = function() { // Workaround for Firefox validation #12102. var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; @@ -21824,7 +26786,7 @@ function weekParser(isoWeek, existingDate) { } function createDateParser(regexp, mapping) { - return function(iso, date) { + return function(iso, previousDate) { var parts, map; if (isDate(iso)) { @@ -21835,7 +26797,7 @@ function createDateParser(regexp, mapping) { // 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) == '"') { + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { iso = iso.substring(1, iso.length - 1); } if (ISO_DATE_REGEXP.test(iso)) { @@ -21846,15 +26808,15 @@ function createDateParser(regexp, mapping) { if (parts) { parts.shift(); - if (date) { + if (previousDate) { map = { - yyyy: date.getFullYear(), - MM: date.getMonth() + 1, - dd: date.getDate(), - HH: date.getHours(), - mm: date.getMinutes(), - ss: date.getSeconds(), - sss: date.getMilliseconds() / 1000 + yyyy: previousDate.getFullYear(), + MM: previousDate.getMonth() + 1, + dd: previousDate.getDate(), + HH: previousDate.getHours(), + mm: previousDate.getMinutes(), + ss: previousDate.getSeconds(), + sss: previousDate.getMilliseconds() / 1000 }; } else { map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; @@ -21865,7 +26827,15 @@ function createDateParser(regexp, mapping) { 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); + + var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); + if (map.yyyy < 100) { + // In the constructor, 2-digit years map to 1900-1999. + // Use `setFullYear()` to set the correct year. + date.setFullYear(map.yyyy); + } + + return date; } } @@ -21874,25 +26844,24 @@ function createDateParser(regexp, mapping) { } function createDateInputType(type, regexp, parseDate, format) { - return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { - badInputChecker(scope, element, attr, ctrl); + return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + + var isTimeType = type === 'time' || type === 'datetimelocal'; var previousDate; + var previousTimezone; - 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) { - parsedDate = convertTimezoneToLocal(parsedDate, timezone); - } - return parsedDate; + return parseDateAndConvertTimeZoneToLocal(value, previousDate); } + ctrl.$$parserName = type; return undefined; }); @@ -21902,35 +26871,50 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; - if (previousDate && timezone) { + var timezone = ctrl.$options.getOption('timezone'); + + if (timezone) { + previousTimezone = timezone; previousDate = convertTimezoneToLocal(previousDate, timezone, true); } - return $filter('date')(value, format, timezone); + + return formatter(value, timezone); } else { previousDate = null; + previousTimezone = null; return ''; } }); if (isDefined(attr.min) || attr.ngMin) { - var minVal; + var minVal = attr.min || $parse(attr.ngMin)(scope); + var parsedMinVal = parseObservedDateValue(minVal); + ctrl.$validators.min = function(value) { - return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal; + return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal; }; attr.$observe('min', function(val) { - minVal = parseObservedDateValue(val); - ctrl.$validate(); + if (val !== minVal) { + parsedMinVal = parseObservedDateValue(val); + minVal = val; + ctrl.$validate(); + } }); } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; + var maxVal = attr.max || $parse(attr.ngMax)(scope); + var parsedMaxVal = parseObservedDateValue(maxVal); + ctrl.$validators.max = function(value) { - return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal; + return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal; }; attr.$observe('max', function(val) { - maxVal = parseObservedDateValue(val); - ctrl.$validate(); + if (val !== maxVal) { + parsedMaxVal = parseObservedDateValue(val); + maxVal = val; + ctrl.$validate(); + } }); } @@ -21940,34 +26924,68 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseObservedDateValue(val) { - return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val; + return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val; + } + + function parseDateAndConvertTimeZoneToLocal(value, previousDate) { + var timezone = ctrl.$options.getOption('timezone'); + + if (previousTimezone && previousTimezone !== timezone) { + // If the timezone has changed, adjust the previousDate to the default timezone + // so that the new date is converted with the correct timezone offset + previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone)); + } + + var parsedDate = parseDate(value, previousDate); + + if (!isNaN(parsedDate) && timezone) { + parsedDate = convertTimezoneToLocal(parsedDate, timezone); + } + return parsedDate; + } + + function formatter(value, timezone) { + var targetFormat = format; + + if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) { + targetFormat = format + .replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat')) + .replace(/:$/, ''); + } + + var formatted = $filter('date')(value, targetFormat, timezone); + + if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) { + formatted = formatted.replace(/(?::00)?(?:\.000)?$/, ''); + } + + return formatted; } }; } -function badInputChecker(scope, element, attr, ctrl) { +function badInputChecker(scope, element, attr, ctrl, parserName) { 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; + if (validity.badInput || validity.typeMismatch) { + ctrl.$$parserName = parserName; + return undefined; + } + + return value; }); } } -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - - ctrl.$$parserName = 'number'; +function numberFormatterParser(ctrl) { ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; if (NUMBER_REGEXP.test(value)) return parseFloat(value); + + ctrl.$$parserName = 'number'; return undefined; }); @@ -21980,60 +26998,303 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } return value; }); +} + +function parseNumberAttrVal(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val); + } + return !isNumberNaN(val) ? val : undefined; +} + +function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) + + // eslint-disable-next-line no-bitwise + return (num | 0) === num; +} + +function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); + } + } + + return 0; + } + + return numString.length - decimalSymbolIndex - 1; +} + +function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); + + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; + + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) { + badInputChecker(scope, element, attr, ctrl, 'number'); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var parsedMinVal; if (isDefined(attr.min) || attr.ngMin) { - var minVal; - ctrl.$validators.min = function(value) { - return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; + var minVal = attr.min || $parse(attr.ngMin)(scope); + parsedMinVal = parseNumberAttrVal(minVal); + + ctrl.$validators.min = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal; }; attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); + if (val !== minVal) { + parsedMinVal = parseNumberAttrVal(val); + minVal = val; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; - // TODO(matsko): implement validateLater to reduce number of validations - ctrl.$validate(); }); } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; - ctrl.$validators.max = function(value) { - return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; + var maxVal = attr.max || $parse(attr.ngMax)(scope); + var parsedMaxVal = parseNumberAttrVal(maxVal); + + ctrl.$validators.max = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal; }; attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); + if (val !== maxVal) { + parsedMaxVal = parseNumberAttrVal(val); + maxVal = val; + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal = attr.step || $parse(attr.ngStep)(scope); + var parsedStepVal = parseNumberAttrVal(stepVal); + + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) || + isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal); + }; + + attr.$observe('step', function(val) { // TODO(matsko): implement validateLater to reduce number of validations - ctrl.$validate(); + if (val !== stepVal) { + parsedStepVal = parseNumberAttrVal(val); + stepVal = val; + ctrl.$validate(); + } + }); + } } -function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { - // 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! +function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl, 'range'); + numberFormatterParser(ctrl); 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); - }; -} + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + minVal = parseNumberAttrVal(attr.min); + + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; -function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { + setInitialValueAndObserver('min', minChange); + } + + if (hasMaxAttr) { + maxVal = parseNumberAttrVal(attr.max); + + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; + + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + stepVal = parseNumberAttrVal(attr.step); + + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); + } + + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + var oldVal = attr[htmlAttrName]; + attr.$observe(htmlAttrName, function wrappedObserver(val) { + if (val !== oldVal) { + oldVal = val; + changeFn(val); + } + }); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the minVal is greater than the element value + if (minVal > elVal) { + elVal = minVal; + element.val(elVal); + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the maxVal is less than the element value + if (maxVal < elVal) { + element.val(maxVal); + // IE11 and Chrome don't set the value to the minVal when max < min + elVal = maxVal < minVal ? minVal : maxVal; + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (!supportsRange) { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } else if (ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); + } + } +} + +function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { + // 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.$validators.url = function(modelValue, viewValue) { + var value = modelValue || viewValue; + return ctrl.$isEmpty(value) || URL_REGEXP.test(value); + }; +} + +function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { // 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); @@ -22041,22 +27302,31 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { + var value; if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } }; - element.on('click', listener); + element.on('change', listener); ctrl.$render = function() { var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); @@ -22083,7 +27353,7 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt ctrl.$setViewValue(element[0].checked, ev && ev.type); }; - element.on('click', listener); + element.on('change', listener); ctrl.$render = function() { element[0].checked = ctrl.$viewValue; @@ -22112,11 +27382,11 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @restrict E * * @description - * HTML textarea element control with angular data-binding. The data-binding and validation + * HTML textarea element control with AngularJS data-binding. The data-binding and validation * properties of this element are exactly the same as those of the * {@link ng.directive:input input element}. * - * @param {string} ngModel Assignable angular expression to data-bind to. + * @param {string} ngModel Assignable AngularJS 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. * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to @@ -22127,8 +27397,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -22136,9 +27406,23 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to * start at the index of the last search's match, thus not taking the whole input value into * account. - * @param {string=} ngChange Angular expression to be executed when input changes due to user + * @param {string=} ngChange AngularJS 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. + * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input. + * + * @knownIssue + * + * When specifying the `placeholder` attribute of ` @@ -24606,8 +30157,6 @@ var ngInitDirective = ngDirective({ *
*
* - * @element input - * @param {string=} ngList optional delimiter that should be used to split the value. */ var ngListDirective = function() { return { @@ -24615,9 +30164,7 @@ var ngListDirective = function() { priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - // 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 ngList = attr.ngList || ', '; var trimValues = attr.ngTrim !== 'false'; var separator = trimValues ? trim(ngList) : ngList; @@ -24659,51 +30206,81 @@ var ngListDirective = function() { DIRTY_CLASS: true, UNTOUCHED_CLASS: true, TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false */ + var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending'; + EMPTY_CLASS = 'ng-empty', + NOT_EMPTY_CLASS = 'ng-not-empty'; var ngModelMinErr = minErr('ngModel'); /** * @ngdoc type * @name ngModel.NgModelController - * * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue * is set. + * * @property {*} $modelValue The value in the model that the control is bound to. + * * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever - the control reads value from the DOM. The functions are called in array order, each passing - its return value through to the next. The last return value is forwarded to the - {@link ngModel.NgModelController#$validators `$validators`} collection. + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. -Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue -`$viewValue`}. + 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. -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`. + 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`. + + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` * * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever - 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. + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. + + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. + + 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. + + This simple example shows a formatter that would convert the model value to uppercase: + * ```js - * function formatter(value) { + * function format(value) { * if (value) { * return value.toUpperCase(); * } * } - * ngModel.$formatters.push(formatter); + * ngModel.$formatters.push(format); * ``` * * @property {Object.} $validators A collection of validators that are applied @@ -24750,8 +30327,10 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * }; * ``` * - * @property {Array.} $viewChangeListeners Array of functions to execute whenever the - * view value has changed. It is called with no arguments, and its return value is ignored. + * @property {Array.} $viewChangeListeners Array of functions to execute whenever + * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change + * to {@link ngModel.NgModelController#$modelValue `$modelValue`}. + * 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 failing validator ids as keys. @@ -24773,7 +30352,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * 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. + * AngularJS 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. * @@ -24829,7 +30408,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. var html = element.html(); // When we clear the content editable the browser leaves a
behind // If strip-br attribute is provided then we strip this out - if ( attrs.stripBr && html == '
' ) { + if (attrs.stripBr && html === '
') { html = ''; } ngModel.$setViewValue(html); @@ -24851,7 +30430,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { // SafariDriver can't handle contenteditable // and Firefox driver can't clear contenteditables very well return; @@ -24871,8 +30450,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { +NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; +function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. @@ -24892,40 +30471,61 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$pending = undefined; // keep pending keys here this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + this.$$updateEvents = ''; + // Attach the correct context to the event handler function for updateOn + this.$$updateEventHandler = this.$$updateEventHandler.bind(this); + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + this.$$parserName = 'parse'; + + this.$$currentValidationRunId = 0; + + this.$$scope = $scope; + this.$$rootScope = $scope.$root; + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); +} - var parsedNgModel = $parse($attr.ngModel), - parsedNgModelAssign = parsedNgModel.assign, - ngModelGet = parsedNgModel, - ngModelSet = parsedNgModelAssign, - pendingDebounce = null, - parserValid, - 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); +NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); + + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); if (isFunction(modelValue)) { modelValue = invokeModelGetter($scope); } return modelValue; }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, ctrl.$modelValue); + this.$$parsedNgModelAssign($scope, newValue); } }; - } else if (!parsedNgModel.assign) { - throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); } - }; + }, + /** * @ngdoc method @@ -24943,11 +30543,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous values. 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; + $render: noop, /** * @ngdoc method @@ -24967,45 +30567,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value The value of the input to check for emptiness. * @returns {boolean} True if `value` is "empty". */ - this.$isEmpty = function(value) { + $isEmpty: function(value) { + // eslint-disable-next-line no-self-compare return isUndefined(value) || value === '' || value === null || value !== value; - }; - - var currentValidationRunId = 0; + }, - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notify the form. - * - * 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 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), 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]; - }, - $animate: $animate - }); + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); + } else { + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); + } + }, /** * @ngdoc method @@ -25018,12 +30593,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, /** * @ngdoc method @@ -25036,13 +30611,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - ctrl.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -25056,11 +30631,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, /** * @ngdoc method @@ -25073,11 +30648,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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); - }; + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, /** * @ngdoc method @@ -25085,66 +30660,94 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * @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 + * which may be caused by a pending debounced event or because the input is waiting for some * future event. * - * If you have an input that uses `ng-model-options` to set up debounced events or events such - * as blur you can have a situation where there is a period when the `$viewValue` - * is out of synch with the ngModel's `$modelValue`. + * If you have an input that uses `ng-model-options` to set up debounced updates or updates that + * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of + * sync with the ngModel's `$modelValue`. + * + * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update + * and reset the input to the last committed view value. * - * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue` - * programmatically before these debounced/future events have resolved/occurred, because Angular's + * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue` + * programmatically before these debounced/future events have resolved/occurred, because AngularJS'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 * * * 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 = ''; + * $scope.model = {value1: '', value2: ''}; + * + * $scope.setEmpty = function(e, value, rollback) { + * if (e.keyCode === 27) { + * e.preventDefault(); + * if (rollback) { + * $scope.myForm[value].$rollbackViewValue(); + * } + * $scope.model[value] = ''; * } * }; * }]); * * *
- *

Try typing something in each input. See that the model only updates when you - * blur off the input. - *

- *

Now see what happens if you start typing then press the Escape key

+ *

Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:

+ *
    + *
  1. Type something in the input. You will see that the model is not yet updated
  2. + *
  3. Press the Escape key. + *
      + *
    1. In the first example, nothing happens, because the model is already '', and no + * update is detected. If you blur the input, the model will be set to the current view. + *
    2. + *
    3. In the second example, the pending update is cancelled, and the input is set back + * to the last committed view value (''). Blurring the input does nothing. + *
    4. + *
    + *
  4. + *
* *
- *

With $rollbackViewValue()

- *
- * myValue: "{{ myValue }}" - * - *

Without $rollbackViewValue()

- *
- * myValue: "{{ myValue }}" + *
+ *

Without $rollbackViewValue():

+ * + * value1: "{{ model.value1 }}" + *
+ * + *
+ *

With $rollbackViewValue():

+ * + * value2: "{{ model.value2 }}" + *
*
*
*
+ + div { + display: table-cell; + } + div:nth-child(1) { + padding-right: 30px; + } + + *
*/ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, /** * @ngdoc method @@ -25158,45 +30761,47 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * 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() { + $validate: function() { + // ignore $validate before model is initialized - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + if (isNumberNaN(this.$modelValue)) { return; } - var viewValue = ctrl.$$lastCommittedViewValue; + var viewValue = this.$$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; + var modelValue = this.$$rawModelValue; - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + var allowInvalid = this.$options.getOption('allowInvalid'); - ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + var that = this; + this.$$runValidators(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 + // Note: Don't check this.$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; + that.$modelValue = allValid ? modelValue : undefined; - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } }); + }, - }; - - this.$$runValidators = function(modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; // check parser error if (!processParseErrors()) { @@ -25210,34 +30815,36 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ processAsyncValidators(); function processParseErrors() { - var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + var errorKey = that.$$parserName; + + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { - if (!parserValid) { - forEach(ctrl.$validators, function(v, name) { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { setValidity(name, null); }); - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); } + // Set the parse error last, to prevent unsetting it, should a $validators key == parserName - setValidity(errorKey, parserValid); - return parserValid; + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; } return true; } function processSyncValidators() { var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { - var result = validator(modelValue, viewValue); + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); syncValidatorsValid = syncValidatorsValid && result; setValidity(name, result); }); if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); return false; @@ -25248,16 +30855,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processAsyncValidators() { var validatorPromises = []; var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { + forEach(that.$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); + throw ngModelMinErr('nopromise', + '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) { + }, function() { allValid = false; setValidity(name, false); })); @@ -25265,25 +30872,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (!validatorPromises.length) { validationDone(true); } else { - $q.all(validatorPromises).then(function() { + that.$$q.all(validatorPromises).then(function() { validationDone(allValid); }, noop); } } function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); } } function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { + if (localValidationRunId === that.$$currentValidationRunId) { doneCallback(allValid); } } - }; + }, /** * @ngdoc method @@ -25296,83 +30903,91 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - this.$commitViewValue = function() { - var viewValue = ctrl.$viewValue; + $commitViewValue: function() { + var viewValue = this.$viewValue; - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$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)) { + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { return; } - ctrl.$$lastCommittedViewValue = viewValue; + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; // change to dirty - if (ctrl.$pristine) { + if (this.$pristine) { this.$setDirty(); } this.$$parseAndValidate(); - }; + }, - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; var modelValue = viewValue; - parserValid = isUndefined(modelValue) ? undefined : true; + var that = this; + + this.$$parserValid = isUndefined(modelValue) ? undefined : true; - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); + // Reset any previous parse error + this.$setValidity(this.$$parserName, null); + this.$$parserName = 'parse'; + + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); if (isUndefined(modelValue)) { - parserValid = false; + this.$$parserValid = false; break; } } } - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - ctrl.$$rawModelValue = modelValue; + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; if (allowInvalid) { - ctrl.$modelValue = modelValue; + this.$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(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$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; + that.$modelValue = allValid ? modelValue : undefined; writeToModelIfNeeded(); } }); function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } - }; + }, - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch (e) { - $exceptionHandler(e); + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); } - }); - }; + }, this); + }, /** * @ngdoc method @@ -25388,9 +31003,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers` * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged - * value sent directly for processing, finally to be applied to `$modelValue` and then the - * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners, - * in the `$viewChangeListeners` list, are called. + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * 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 @@ -25405,7 +31021,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * However, custom controls might also pass objects to this method. In this case, we should make * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not * perform a deep watch of objects, it only looks for a change of identity. If you only change - * the property of the object then ngModel will not realise that the object has changed and + * the property of the object then ngModel will not realize 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. @@ -25424,89 +31040,304 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value value from the view. * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value, trigger) { - ctrl.$viewValue = value; - if (!ctrl.$options || ctrl.$options.updateOnDefault) { - ctrl.$$debounceViewValueCommit(trigger); + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); } - }; + }, - this.$$debounceViewValueCommit = function(trigger) { - var debounceDelay = 0, - options = ctrl.$options, - debounce; + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('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 (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default']) && + this.$options.getOption('updateOn').indexOf(trigger) === -1 + ) { + debounceDelay = debounceDelay['default']; + } else if (isNumber(debounceDelay['*'])) { + debounceDelay = debounceDelay['*']; } - $timeout.cancel(pendingDebounce); - if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); + } else if (this.$$rootScope.$$phase) { + this.$commitViewValue(); } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); + this.$$scope.$apply(function() { + that.$commitViewValue(); }); } - }; - - // model -> value - // 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 modelValue = ngModelGet($scope); - - // if scope model value and ngModel value are out of sync - // TODO(perf): why not move this to the action fn? - if (modelValue !== ctrl.$modelValue && - // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator - (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) - ) { - ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; - - var formatters = ctrl.$formatters, - idx = formatters.length; - - var viewValue = modelValue; - while (idx--) { - viewValue = formatters[idx](viewValue); - } - if (ctrl.$viewValue !== viewValue) { - ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; - ctrl.$render(); - - ctrl.$$runValidators(modelValue, viewValue, noop); - } - } + }, - return modelValue; - }); -}]; + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + *
+ * **Note:** this function only affects the options set on the `ngModelController`, + * and not the options on the {@link ngModelOptions} directive from which they might have been + * obtained initially. + *
+ * + *
+ * **Note:** it is not possible to override the `getterSetter` option. + *
+ * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + this.$$setUpdateOnEvents(); + }, + + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$processModelValue + + * @description + * + * Runs the model -> view pipeline on the current + * {@link ngModel.NgModelController#$modelValue $modelValue}. + * + * The following actions are performed by this method: + * + * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters} + * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue} + * - the `ng-empty` or `ng-not-empty` class is set on the element + * - if the `$viewValue` has changed: + * - {@link ngModel.NgModelController#$render $render} is called on the control + * - the {@link ngModel.NgModelController#$validators $validators} are run and + * the validation status is set. + * + * This method is called by ngModel internally when the bound scope value changes. + * Application developers usually do not have to call this function themselves. + * + * This function can be used when the `$viewValue` or the rendered DOM value are not correctly + * formatted and the `$modelValue` must be run through the `$formatters` again. + * + * @example + * Consider a text input with an autocomplete list (for fruit), where the items are + * objects with a name and an id. + * A user enters `ap` and then selects `Apricot` from the list. + * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`, + * but the rendered value will still be `ap`. + * The widget can then call `ctrl.$processModelValue()` to run the model -> view + * pipeline again, which formats the object to the string `Apricot`, + * then updates the `$viewValue`, and finally renders it in the DOM. + * + * + +
+
+ Search Fruit: + +
+
+ Model:
+
{{selectedFruit | json}}
+
+
+
+ + angular.module('inputExample', []) + .controller('inputController', function($scope) { + $scope.items = [ + {name: 'Apricot', id: 443}, + {name: 'Clementine', id: 972}, + {name: 'Durian', id: 169}, + {name: 'Jackfruit', id: 982}, + {name: 'Strawberry', id: 863} + ]; + }) + .component('basicAutocomplete', { + bindings: { + items: '<', + onSelect: '&' + }, + templateUrl: 'autocomplete.html', + controller: function($element, $scope) { + var that = this; + var ngModel; + + that.$postLink = function() { + ngModel = $element.find('input').controller('ngModel'); + + ngModel.$formatters.push(function(value) { + return (value && value.name) || value; + }); + + ngModel.$parsers.push(function(value) { + var match = value; + for (var i = 0; i < that.items.length; i++) { + if (that.items[i].name === value) { + match = that.items[i]; + break; + } + } + + return match; + }); + }; + + that.selectItem = function(item) { + ngModel.$setViewValue(item); + ngModel.$processModelValue(); + that.onSelect({item: item}); + }; + } + }); + + +
+ +
    +
  • + +
  • +
+
+
+ *
+ * + */ + $processModelValue: function() { + var viewValue = this.$$format(); + + if (this.$viewValue !== viewValue) { + this.$$updateEmptyClasses(viewValue); + this.$viewValue = this.$$lastCommittedViewValue = viewValue; + this.$render(); + // It is possible that model and view value have been updated during render + this.$$runValidators(this.$modelValue, this.$viewValue, noop); + } + }, + + /** + * This method is called internally to run the $formatters on the $modelValue + */ + $$format: function() { + var formatters = this.$formatters, + idx = formatters.length; + + var viewValue = this.$modelValue; + while (idx--) { + viewValue = formatters[idx](viewValue); + } + + return viewValue; + }, + + /** + * This method is called internally when the bound scope value changes. + */ + $$setModelValue: function(modelValue) { + this.$modelValue = this.$$rawModelValue = modelValue; + this.$$parserValid = undefined; + this.$processModelValue(); + }, + + $$setUpdateOnEvents: function() { + if (this.$$updateEvents) { + this.$$element.off(this.$$updateEvents, this.$$updateEventHandler); + } + + this.$$updateEvents = this.$options.getOption('updateOn'); + if (this.$$updateEvents) { + this.$$element.on(this.$$updateEvents, this.$$updateEventHandler); + } + }, + + $$updateEventHandler: function(ev) { + this.$$debounceViewValueCommit(ev && ev.type); + } +}; + +function setupModelWatcher(ctrl) { + // model -> value + // 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' + ctrl.$$scope.$watch(function ngModelWatch(scope) { + var modelValue = ctrl.$$ngModelGet(scope); + + // if scope model value and ngModel value are out of sync + // This cannot be moved to the action function, because it would not catch the + // case where the model is changed in the ngChange function or the model setter + if (modelValue !== ctrl.$modelValue && + // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare + (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) + ) { + ctrl.$$setModelValue(modelValue); + } + + return modelValue; + }); +} + +/** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * 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 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` + * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`. + * @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 AngularJS when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ +addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } +}); /** * @ngdoc directive * @name ngModel - * - * @element input + * @restrict A * @priority 1 + * @param {expression} ngModel assignable {@link guide/expression Expression} to bind to. * * @description * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a @@ -25519,7 +31350,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * require. * - Providing validation behavior (i.e. required, number, email, url). * - 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. + * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, + * `ng-untouched`, `ng-empty`, `ng-not-empty`) 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 @@ -25547,7 +31379,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - {@link ng.directive:select select} * - {@link ng.directive:textarea textarea} * - * # CSS classes + * ## Complex Models (objects or collections) + * + * By default, `ngModel` watches the model by reference, not value. This is important to know when + * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the + * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered. + * + * The model must be assigned an entirely new object or collection before a re-rendering will occur. + * + * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression + * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or + * if the select is given the `multiple` attribute. + * + * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the + * first level of the object (or only changing the properties of an item in the collection if it's an array) will still + * not trigger a re-rendering of the model. + * + * ## CSS classes * The following CSS classes are added and removed on the associated input/select/textarea element * depending on the validity of the model. * @@ -25560,13 +31408,15 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * - `ng-touched`: the control has been blurred * - `ng-untouched`: the control hasn't been blurred * - `ng-pending`: any `$asyncValidators` are unfulfilled + * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined + * by the {@link ngModel.NgModelController#$isEmpty} method + * - `ng-not-empty`: the view contains a non-empty value * * Keep in mind that ngAnimate can detect each of these classes when added and removed. * - * ## Animation Hooks - * + * @animations * Animations within models are triggered when any of the associated CSS classes are added and removed - * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`, + * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`, * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself. * The animations that are triggered within ngModel are similar to how they work in ngClass and * animations can be hooked into using CSS transitions, keyframes as well as JS animations. @@ -25588,7 +31438,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * * @example - * + * ### Basic Usage + * -
-
-
- {{text}} -
-
- - it('should have transcluded', function() { - var titleElement = element(by.model('title')); - titleElement.clear(); - titleElement.sendKeys('TITLE'); - var textElement = element(by.model('text')); - textElement.clear(); - textElement.sendKeys('TEXT'); - expect(element(by.binding('title')).getText()).toEqual('TITLE'); - expect(element(by.binding('text')).getText()).toEqual('TEXT'); - }); - -
+ * ### Basic transclusion + * This example demonstrates basic transclusion of content into a component directive. + * + * + * + *
+ *
+ *
+ * {{text}} + *
+ *
+ * + * it('should have transcluded', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.binding('title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * }); + * + *
+ * + * @example + * ### Transclude fallback content + * This example shows how to use `NgTransclude` with fallback content, that + * is displayed if no transcluded content is provided. + * + * + * + * + * + * + * + * + * Button2 + * + * + * + * it('should have different transclude element content', function() { + * expect(element(by.id('fallback')).getText()).toBe('Button1'); + * expect(element(by.id('modified')).getText()).toBe('Button2'); + * }); + * + * * + * @example + * ### Multi-slot transclusion + * This example demonstrates using multi-slot transclusion in a component directive. + * + * + * + *
+ *
+ *
+ * + * {{title}} + *

{{text}}

+ *
+ *
+ *
+ * + * angular.module('multiSlotTranscludeExample', []) + * .directive('pane', function() { + * return { + * restrict: 'E', + * transclude: { + * 'title': '?paneTitle', + * 'body': 'paneBody', + * 'footer': '?paneFooter' + * }, + * template: '
' + + * '
Fallback Title
' + + * '
' + + * '' + + * '
' + * }; + * }) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.title = 'Lorem Ipsum'; + * $scope.link = 'https://google.com'; + * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + * }]); + *
+ * + * it('should have transcluded the title and the body', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.css('.title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer'); + * }); + * + *
*/ -var ngTranscludeDirective = ngDirective({ - restrict: 'EAC', - link: function($scope, $element, $attrs, controller, $transclude) { - if (!$transclude) { - throw minErr('ngTransclude')('orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element: {0}', - startingTag($element)); - } - - $transclude(function(clone) { - $element.empty(); - $element.append(clone); - }); - } -}); +var ngTranscludeMinErr = minErr('ngTransclude'); +var ngTranscludeDirective = ['$compile', function($compile) { + return { + restrict: 'EAC', + compile: function ngTranscludeCompile(tElement) { + + // Remove and cache any original content to act as a fallback + var fallbackLinkFn = $compile(tElement.contents()); + tElement.empty(); + + return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) { + + if (!$transclude) { + throw ngTranscludeMinErr('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + + + // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default + if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) { + $attrs.ngTransclude = ''; + } + var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot; + + // If the slot is required and no transclusion content is provided then this call will throw an error + $transclude(ngTranscludeCloneAttachFn, null, slotName); + + // If the slot is optional and no transclusion content is provided then use the fallback content + if (slotName && !$transclude.isSlotFilled(slotName)) { + useFallbackContent(); + } + + function ngTranscludeCloneAttachFn(clone, transcludedScope) { + if (clone.length && notWhitespace(clone)) { + $element.append(clone); + } else { + useFallbackContent(); + // There is nothing linked against the transcluded scope since no content was available, + // so it should be safe to clean up the generated scope. + transcludedScope.$destroy(); + } + } + + function useFallbackContent() { + // Since this is the fallback content rather than the transcluded content, + // we link against the scope of this directive rather than the transcluded scope + fallbackLinkFn($scope, function(clone) { + $element.append(clone); + }); + } + + function notWhitespace(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i]; + if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) { + return true; + } + } + } + }; + } + }; +}]; /** * @ngdoc directive @@ -28270,7 +35067,7 @@ var ngTranscludeDirective = ngDirective({ * @param {string} id Cache name of the template. * * @example - + + *
+ *
+ * + * + *
+ * + *
+ *
+ * required error set? = {{form.input.$error.required}}
+ * model = {{model}} + *
+ *
+ *
+ * + var required = element(by.binding('form.input.$error.required')); + var model = element(by.binding('model')); + var input = element(by.id('input')); -var requiredDirective = function() { + it('should set the required error', function() { + expect(required.getText()).toContain('true'); + + input.sendKeys('123'); + expect(required.getText()).not.toContain('true'); + expect(model.getText()).toContain('123'); + }); + * + *
+ */ +var requiredDirective = ['$parse', function($parse) { 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 + // For boolean attributes like required, presence means true + var value = attr.hasOwnProperty('required') || $parse(attr.ngRequired)(scope); + + if (!attr.ngRequired) { + // force truthy in case we are on non input element + // (input elements do this automatically for boolean attributes like required) + attr.required = true; + } ctrl.$validators.required = function(modelValue, viewValue) { - return !attr.required || !ctrl.$isEmpty(viewValue); + return !value || !ctrl.$isEmpty(viewValue); }; - attr.$observe('required', function() { - ctrl.$validate(); + attr.$observe('required', function(newVal) { + + if (value !== newVal) { + value = newVal; + ctrl.$validate(); + } }); } }; -}; +}]; + +/** + * @ngdoc directive + * @name ngPattern + * @restrict A + * + * @param {expression|RegExp} ngPattern AngularJS expression that must evaluate to a `RegExp` or a `String` + * parsable into a `RegExp`, or a `RegExp` literal. See above for + * more details. + * + * @description + * + * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * does not match a RegExp which is obtained from the `ngPattern` attribute value: + * - the value is an AngularJS expression: + * - If the expression evaluates to a RegExp object, then this is used directly. + * - If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it + * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`. + * - If the value is a RegExp literal, e.g. `ngPattern="/^\d+$/"`, it is used directly. + * + *
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to + * start at the index of the last search's match, thus not taking the whole input value into + * account. + *
+ * + *
+ * **Note:** This directive is also added when the plain `pattern` attribute is used, with two + * differences: + *
    + *
  1. + * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is + * not available. + *
  2. + *
  3. + * The `ngPattern` attribute must be an expression, while the `pattern` value must be + * interpolated. + *
  4. + *
+ *
+ * + * @example + * + * + * + *
+ *
+ * + * + *
+ * + *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}} + *
+ *
+ *
+ * + var model = element(by.binding('model')); + var input = element(by.id('input')); + it('should validate the input with the default pattern', function() { + input.sendKeys('aaa'); + expect(model.getText()).not.toContain('aaa'); -var patternDirective = function() { + input.clear().then(function() { + input.sendKeys('123'); + expect(model.getText()).toContain('123'); + }); + }); + * + *
+ */ +var patternDirective = ['$parse', function($parse) { 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 + '$'); + compile: function(tElm, tAttr) { + var patternExp; + var parseFn; + + if (tAttr.ngPattern) { + patternExp = tAttr.ngPattern; + + // ngPattern might be a scope expression, or an inlined regex, which is not parsable. + // We get value of the attribute here, so we can compare the old and the new value + // in the observer to avoid unnecessary validations + if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) { + parseFn = function() { return tAttr.ngPattern; }; + } else { + parseFn = $parse(tAttr.ngPattern); } + } + + return function(scope, elm, attr, ctrl) { + if (!ctrl) return; - if (regex && !regex.test) { - throw minErr('ngPattern')('noregexp', - 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, - regex, startingTag(elm)); + var attrVal = attr.pattern; + + if (attr.ngPattern) { + attrVal = parseFn(scope); + } else { + patternExp = attr.pattern; } - regexp = regex || undefined; - ctrl.$validate(); - }); + var regexp = parsePatternAttr(attrVal, patternExp, elm); + + attr.$observe('pattern', function(newVal) { + var oldRegexp = regexp; + + regexp = parsePatternAttr(newVal, patternExp, elm); - ctrl.$validators.pattern = function(modelValue, viewValue) { - // HTML5 pattern constraint validates the input value, so we validate the viewValue - return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue); + if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) { + ctrl.$validate(); + } + }); + + ctrl.$validators.pattern = function(modelValue, viewValue) { + // HTML5 pattern constraint validates the input value, so we validate the viewValue + return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue); + }; }; } + }; -}; +}]; + +/** + * @ngdoc directive + * @name ngMaxlength + * @restrict A + * + * @param {expression} ngMaxlength AngularJS expression that must evaluate to a `Number` or `String` + * parsable into a `Number`. Used as value for the `maxlength` + * {@link ngModel.NgModelController#$validators validator}. + * + * @description + * + * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * is longer than the integer obtained by evaluating the AngularJS expression given in the + * `ngMaxlength` attribute value. + * + *
+ * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two + * differences: + *
    + *
  1. + * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint + * validation is not available. + *
  2. + *
  3. + * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be + * interpolated. + *
  4. + *
+ *
+ * + * @example + * + * + * + *
+ *
+ * + * + *
+ * + *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}} + *
+ *
+ *
+ * + var model = element(by.binding('model')); + var input = element(by.id('input')); + it('should validate the input with the default maxlength', function() { + input.sendKeys('abcdef'); + expect(model.getText()).not.toContain('abcdef'); -var maxlengthDirective = function() { + input.clear().then(function() { + input.sendKeys('abcde'); + expect(model.getText()).toContain('abcde'); + }); + }); + * + *
+ */ +var maxlengthDirective = ['$parse', function($parse) { return { restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; - var maxlength = -1; + var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope); + var maxlengthParsed = parseLength(maxlength); + attr.$observe('maxlength', function(value) { - var intVal = toInt(value); - maxlength = isNaN(intVal) ? -1 : intVal; - ctrl.$validate(); + if (maxlength !== value) { + maxlengthParsed = parseLength(value); + maxlength = value; + ctrl.$validate(); + } }); ctrl.$validators.maxlength = function(modelValue, viewValue) { - return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength); + return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed); }; } }; -}; +}]; + +/** + * @ngdoc directive + * @name ngMinlength + * @restrict A + * + * @param {expression} ngMinlength AngularJS expression that must evaluate to a `Number` or `String` + * parsable into a `Number`. Used as value for the `minlength` + * {@link ngModel.NgModelController#$validators validator}. + * + * @description + * + * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}. + * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls. + * + * The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} + * is shorter than the integer obtained by evaluating the AngularJS expression given in the + * `ngMinlength` attribute value. + * + *
+ * **Note:** This directive is also added when the plain `minlength` attribute is used, with two + * differences: + *
    + *
  1. + * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint + * validation is not available. + *
  2. + *
  3. + * The `ngMinlength` value must be an expression, while the `minlength` value must be + * interpolated. + *
  4. + *
+ *
+ * + * @example + * + * + * + *
+ *
+ * + * + *
+ * + *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}} + *
+ *
+ *
+ * + var model = element(by.binding('model')); + var input = element(by.id('input')); + + it('should validate the input with the default minlength', function() { + input.sendKeys('ab'); + expect(model.getText()).not.toContain('ab'); -var minlengthDirective = function() { + input.sendKeys('abc'); + expect(model.getText()).toContain('abc'); + }); + * + *
+ */ +var minlengthDirective = ['$parse', function($parse) { return { restrict: 'A', require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; - var minlength = 0; + var minlength = attr.minlength || $parse(attr.ngMinlength)(scope); + var minlengthParsed = parseLength(minlength) || -1; + attr.$observe('minlength', function(value) { - minlength = toInt(value) || 0; - ctrl.$validate(); + if (minlength !== value) { + minlengthParsed = parseLength(value) || -1; + minlength = value; + ctrl.$validate(); + } + }); ctrl.$validators.minlength = function(modelValue, viewValue) { - return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength; + return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed; }; } }; -}; +}]; + + +function parsePatternAttr(regex, patternExp, elm) { + if (!regex) return undefined; + + if (isString(regex)) { + regex = new RegExp('^' + regex + '$'); + } + + if (!regex.test) { + throw minErr('ngPattern')('noregexp', + 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp, + regex, startingTag(elm)); + } + + return regex; +} + +function parseLength(val) { + var intVal = toInt(val); + return isNumberNaN(intVal) ? -1 : intVal; +} if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... - console.log('WARNING: Tried to load angular more than once.'); + // AngularJS is already loaded, so we can return here... + if (window.console) { + console.log('WARNING: Tried to load AngularJS more than once.'); + } return; } -//try to bind to jquery now so that one can write jqLite(document).ready() -//but we will rebind on bootstrap again. +// try to bind to jquery now so that one can write jqLite(fn) +// but we will rebind on bootstrap again. bindJQuery(); publishExternalAPI(angular); @@ -28962,6 +36529,20 @@ $provide.value("$locale", { "Nov", "Dec" ], + "STANDALONEMONTH": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], "WEEKENDRANGE": [ 5, 6 @@ -29005,14 +36586,15 @@ $provide.value("$locale", { ] }, "id": "en-us", + "localeID": "en_US", "pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;} }); }]); - jqLite(document).ready(function() { - angularInit(document, bootstrap); + jqLite(function() { + angularInit(window.document, bootstrap); }); -})(window, document); +})(window); -!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(''); \ No newline at end of file +!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(window.angular.element(''); +(function(z){'use strict';function ve(a){if(D(a))w(a.objectMaxDepth)&&(Xb.objectMaxDepth=Yb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Xb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Xb}function Yb(a){return X(a)&&0c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f
").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Vc(a){try{return decodeURIComponent(a)}catch(b){}}function hc(a){var b={};r((a||"").split("&"), +function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Vc(e),w(e)&&(f=w(f)?Vc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Ce(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function ic(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a, +b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function De(a,b){var d,c,e=Qa.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))}function Wc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===z.document?"document":Aa(a);throw oa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider", +function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;z&&e.test(z.name)&&(d.debugInfoEnabled=!0,z.name=z.name.replace(e,""));if(z&&!f.test(z.name))return c();z.name=z.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&& +ca.resumeDeferredBootstrap()}function Ge(){z.name="NG_ENABLE_DEBUG_INFO!"+z.name;z.location.reload()}function He(a){a=ca.element(a).injector();if(!a)throw oa("test");return a.get("$$testability")}function Xc(a,b){b=b||"_";return a.replace(Ie,function(a,c){return(c?b:"")+a.toLowerCase()})}function Je(){var a;if(!Yc){var b=rb();(sb=A(b)?z.jQuery:b?z[b]:void 0)&&sb.fn.on?(x=sb,S(sb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})): +x=U;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Yc=!0}}function Ke(){U.legacyXHTMLReplacement=!0}function gb(a,b,d){if(!a)throw oa("areq",b||"?",d||"required");return a}function tb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw oa("badname", +b);}function Le(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g"):a;if(10>wa)for(c=hb[c]||hb._default,d.innerHTML=c[1]+e+c[2],k=c[0];k--;)d=d.firstChild;else{c=qa[c]||[];for(k=c.length;-1<--k;)d.appendChild(z.document.createElement(c[k])),d=d.firstChild;d.innerHTML=e}g=db(g,d.childNodes);d=f.firstChild;d.textContent=""}else g.push(b.createTextNode(a)); +f.textContent="";f.innerHTML="";r(g,function(a){f.appendChild(a)});return f}function U(a){if(a instanceof U)return a;var b;C(a)&&(a=V(a),b=!0);if(!(this instanceof U)){if(b&&"<"!==a.charAt(0))throw oc("nosel");return new U(a)}if(b){b=z.document;var d;a=(d=tg.exec(a))?[b.createElement(d[1])]:(d=gd(a,b))?d.childNodes:[];pc(this,a)}else B(a)?hd(a):pc(this,a)}function qc(a){return a.cloneNode(!0)}function zb(a,b){!b&&mc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function id(a){for(var b in a)return!1; +return!0}function jd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!id(d)||c&&!id(c)||(delete Ka[b],a.ng339=void 0)}function kd(a,b,d,c){if(w(c))throw oc("offargs");var e=(c=Ab(a))&&c.events,f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0l&&this.remove(n.key);return b}},get:function(a){if(l";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement; +h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("
").append(a).html())):c?Wa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;mu.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&& +!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Jb=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],oa(f,Ha.call(M,0),b),R=Z(Jb,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=z.document.createDocumentFragment();var Xa= +T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[xa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||z.document.createDocumentFragment(),ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Jb,R,e));M=x(M.childNodes)}else M=x(qc(b)).contents();y.empty();R=Z(Jb,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P= +!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=nc.test(O)?td(ja(u.templateNamespace,V(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",Q,"");oa(f,y,b);C={$attr:{}};O=tc(b,[],C);var Ig=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Ig);ga(d,C);C=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!== +u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),C=a.length;else if(u.compile)try{q=u.compile(y,d,R);var Y=u.$$originalDirective||u;B(q)?m(null,Va(Y,q),E,jb):q&&m(Va(Y,q.pre),Va(Y,q.post),E,jb)}catch(ca){c(ca,Aa(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function X(a,b,c,d){var e;if(C(b)){var f=b.match(l);b=b.substring(f[0].length); +var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=bc(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null}; +D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function qa(a,b){if("srcdoc"=== +b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function ya(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function za(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c, +d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=ya(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=za;b.push({priority:100,compile:function(a,b){var e=p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=qa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",Aa(a));if(m.test(e))throw $("nodomevents");c.push({priority:100, +compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function oa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Kg.call(a,b,1)}return a} +function Gg(a,b){if(b&&C(b))return b;if(C(a)){var d=wd.exec(a);if(d)return d[3]}}function Kf(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&C(g)&&(l=g);if(C(c)){g=c.match(wd);if(!g)throw xd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Le(e.$scope, +h,!0);if(!c)throw xd("ctrlreg",h);tb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Lf(){this.$get=["$window",function(a){return x(a.document)}]}function Mf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden; +a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function Nf(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function vc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Tf(){this.$get=function(){return function(a){if(!a)return"";var b=[];Qc(a,function(a,c){null===a||A(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(vc(a)))}):b.push(ba(c)+"="+ba(vc(a))))});return b.join("&")}}}function Uf(){this.$get= +function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Qc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(vc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function wc(a,b){if(C(a)){var d=a.replace(Lg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(yd),e;(e=c)||(e=(e=d.match(Mg))&&Ng[e[0]].test(d));if(e)try{a=Tc(d)}catch(f){if(!c)return a;throw Lb("baddata",a,f);}}}return a} +function zd(a){var b=T(),d;C(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(V(a.substr(0,d)));a=V(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&r(a,function(a,d){var f=K(d),g=V(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function Ad(a){var b;return function(d){b||(b=zd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function Bd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Sf(){var a=this.defaults={transformResponse:[wc],transformRequest:[function(a){return D(a)&& +"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ja(xc),put:ja(xc),patch:ja(xc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfTrustedOrigins=[];Object.defineProperty(this,"xsrfWhitelistedOrigins", +{get:function(){return this.xsrfTrustedOrigins},set:function(a){this.xsrfTrustedOrigins=a}});this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;da?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!C(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method= +vb(g.method);g.paramSerializer=C(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});b=c(b,h);b=b.then(function(b){var c=b.headers,d=Bd(b.data,Ad(c),void 0,b.transformRequest);A(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});A(b.withCredentials)&&!A(a.withCredentials)&& +(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,zd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300> +b?L.resolve:L.reject)({data:a,status:b,headers:Ad(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),O=c.url;x?O=p.getTrustedResourceUrl(O):C(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache|| +!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));A(q)&&((q=kc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Cd(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host= +d.hostname;b.$$port=fa(d.port)||Rg[d.protocol]||null}function Dd(a,b,d){if(Sg.test(a))throw kb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=hc(a.search);b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function yc(a,b){return a.slice(0, +b.length)===b}function ya(a,b){if(yc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function zc(a,b,d){this.$$html5=!0;d=d||"";Cd(a,this);this.$$parse=function(a){var d=ya(b,a);if(!C(d))throw kb("ipthprfx",a,b);Dd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=ya(a,c))?(g=f,g=d&&w(f=ya(d,f))? +b+(ya("/",f)||f):a+g):w(f=ya(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function Ac(a,b,d){Cd(a,this);this.$$parse=function(c){var e=ya(a,c)||ya(b,c),f;A(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",A(e)&&(a=c,this.replace())):(f=ya(d,e),A(f)&&(f=e));Dd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;yc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b, +d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Ed(a,b,d){this.$$html5=!0;Ac.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=ya(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Mb(a){return function(){return this[a]}}function Fd(a,b){return function(d){if(A(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Yf(){var a="!", +b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||C(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a, +b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw kb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?zc:Ed}else s=Da(n),p=Ac;var r=s.substr(0,Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i; +f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!C(e)||!A(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!== +n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){yc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!== +m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Zf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){dc(a)&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=wa||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Tg(a){return a+""}function Ug(a,b){return"undefined"!==typeof a?a: +b}function Gd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Vg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Hd:b}function Z(a,b,d){var c,e,f=a.isPure=Vg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch= +[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&& +a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left, +b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant= +!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Id(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Jd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Kd(a){if(1===a.body.length&&Jd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}function Ld(a){this.$filter=a}function Md(a){this.$filter=a}function Nb(a,b,d){this.ast= +new q(a,d);this.astCompiler=d.csp?new Md(b):new Ld(b)}function Bc(a){return B(a.valueOf)?a.valueOf():Wg.call(a)}function $f(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Ob(G),d=(new Nb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E, +c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Bc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Bc(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++, +f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1r&&(A=4-r,N[A]||(N[A]=[]),N[A].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s= +!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;Jwa)throw Ea("iequirks");var c=ja(W);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(W, +function(a,b){var d=K(b);c[("parse_as_"+d).replace(Dc,xb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Dc,xb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Dc,xb)]=function(b){return g(a,b)}});return c}]}function fg(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))|| +[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&wa)return!1;if(A(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),transitions:h,animations:l,android:e}}]}function gg(){this.$get=ia(function(a){return new Yg(a)})}function Yg(a){function b(){var a=e.pop();return a&& +a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}): +a()}}function ig(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!C(k)||A(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==wc}):l===wc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)}, +function(a){h||(a=Zg("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function jg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Od(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-", +"data-ng-","ng\\:"],k=0;kc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Fc;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Fc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Yd&&(d=d.splice(0,Yd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function ih(a, +b,d,c){var e=a.d,f=e.length-a.i;b=A(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;fk;)h.unshift(0),k++;0=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+ +h+b.posSuf}function Pb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Pb(f,b,c,e)}}function lb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Zd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function $d(a){return function(b){var d= +Zd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Pb(b,a)}}function Gc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Td(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+ +(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;C(c)&&(c=jh.test(c)?fa(c):b(c));X(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=kh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=fc(f,m),c=gc(c,f,!0));r(k,function(b){h=lh[b];g+=h?h(c,a.DATETIME_FORMATS, +m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ch(){return function(a,b){A(b)&&(b=2);return eb(a,b)}}function dh(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(Y(b))return a;X(a)&&(a=a.toString());if(!za(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Hc(a,d,d+b):0===d?Hc(a,b,a.length):Hc(a,Math.max(0,d+b),d)}}function Hc(a,b,d){return C(a)?a.slice(b,d):Ha.call(a,b,d)}function Vd(a){function b(b){return b.map(function(b){var c= +1,d=Ta;if(B(b))d=b;else if(C(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c= +hb||37<=b&&40>=b|| +m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ee[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Rb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(C(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length- +1)&&(d=d.substring(1,d.length-1));if(mh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){cf.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function ob(a,b,d,c){return function(e,f,g,k,h,l,m, +p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Uc(b,fc(v)));var e=d(a,b);!isNaN(e)&&c&&(e=gc(e,c));return e}Jc(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw pb("datefmt",a);if(n(a)){q=a;var b= +k.$options.getOption("timezone");b&&(v=b,q=gc(q,b,!0));var d=c;t&&C(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),z=s(x);k.$validators.min=function(a){return!n(a)||A(z)||d(a)>=z};g.$observe("min",function(a){a!==x&&(z=s(a),x=a,k.$validate())})}if(w(g.max)|| +g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||A(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Jc(a,b,d,c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function fe(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(nh.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!X(b))throw pb("numfmt", +b);b=b.toString()}return b})}function na(a){w(a)&&!X(a)&&(a=parseFloat(a));return Y(a)?void 0:a}function Kc(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ge(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Kc(a):0,k=e?Kc(b):0,h=f?Kc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function he(a,b,d,c,e){if(w(c)){a= +a(c);if(!a.constant)throw pb("constexpr",d,c);return a(b)}return e}function Lc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/,nc=/<|&#?\w+;/,rg=/<([\w:-]+)/,sg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,qa={thead:["table"],col:["colgroup","table"],tr:["tbody","table"],td:["tr", +"tbody","table"]};qa.tbody=qa.tfoot=qa.colgroup=qa.caption=qa.thead;qa.th=qa.td;var hb={option:[1,'"],_default:[0,"",""]},Nc;for(Nc in qa){var le=qa[Nc],me=le.slice().reverse();hb[Nc]=[me.length,"<"+me.join("><")+">",""]}hb.optgroup=hb.option;var zg=z.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=U.prototype={ready:hd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+ +"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:ph,sort:[].sort,splice:[].splice},Hb={};r("multiple selected checked disabled readOnly required open".split(" "),function(a){Hb[K(a)]=a});var od={};r("input select option textarea button form details".split(" "),function(a){od[a]=!0});var vd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:sc,removeData:rc,hasData:function(a){for(var b in Ka[a.ng339])return!0; +return!1},cleanData:function(a){for(var b=0,d=a.length;b/,Cg=/^[^(]*\(\s*([^)]*)\)/m,sh=/,/,th=/^\s*(_?)(\S+?)\1\s*$/,Ag=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ca=F("$injector");fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw C(d)&&d||(d=a.name||Dg(a)),Ca("strictdi",d);b=qd(a);r(b[1].split(sh),function(a){a.replace(th,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,tb(a[b],"fn"),c=a.slice(0,b)):tb(a,"fn",!0);return c};var ne=F("$animate"), +Ef=function(){this.$get=E},Ff=function(){var a=new Ib,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=C(b)?b.split(" "):H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=Eg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Eb(a,e);f&&Db(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g, +k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},Cf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw ne("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g, +d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,ne("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Vb[a]= +!0});var wh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Ob=function(a){this.options=a};Ob.prototype={constructor:Ob,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"=== +typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)}, +isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b= +w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:q.BinaryExpression,operator:b.text, +left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()}, +primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression", +this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression()); +return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]"); +return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"), +b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe", +this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Hd=2;Ld.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[], +body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Kd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Id(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage= +"main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Tg,Ug,Gd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+ +";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")}, +recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0, +void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right, +b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)), +h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b, +m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)): +(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context, +g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId(); +h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+ +b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a, +b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a, +b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(C(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(X(a))return a.toString();if(!0===a)return"true";if(!1=== +a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Md.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Kd(a))c=this.recurse(d);d=Id(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body, +function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left), +e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c, +e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c= +a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c= +e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d, +c,e,f){return f?f[b]:a(d,c,e)}}};Nb.prototype={constructor:Nb,parse:function(a){a=this.getAst(a);var b=this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a), +oneTime:b}}};var Ea=F("$sce"),W={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},Dc=/_([a-z])/g,Zg=F("$templateRequest"),$g=F("$timeout"),aa=z.document.createElement("a"),Qd=ga(z.location.href),Na;aa.href="http://[::1]";var ah="[::1]"===aa.hostname;Rd.$inject=["$document"];fd.$inject=["$provide"];var Yd=22,Xd=".",Fc="0";Sd.$inject=["$locale"];Ud.$inject=["$locale"];var lh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0), +MMMM:lb("Month"),MMM:lb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:lb("Month",!1,!0),dd:ea("Date",2),d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:lb("Day"),EEE:lb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Pb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},kh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,jh=/^-?\d+$/;Td.$inject=["$locale"];var eh=ia(K),fh=ia(vb);Vd.$inject=["$parse"];var Re=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href"; +b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};r(Hb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=xa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(vd,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ke))){e.$set("ngPattern",new RegExp(c[1], +c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=xa("ng-"+a);wb[b]=["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),wa&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var mb={$addControl:E,$getControls:ia([]),$$renameControl:function(a, +b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Qb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Qb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)}, +$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=mb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element, +Wb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Za,Wb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==mb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element, +"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ce({clazz:Qb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var oe=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Qb,compile:function(d,f){d.addClass(Za).addClass(nb); +var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))})); +d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,mb)})}}}}}]},Se=oe(),df=oe(!0),mh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,xh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,yh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/, +nh=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,pe=/^(\d{4,})-(\d{2})-(\d{2})$/,qe=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Oc=/^(\d{4,})-W(\d\d)$/,re=/^(\d{4,})-(\d\d)$/,se=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ee=T();r(["date","datetime-local","month","time","week"],function(a){ee[a]=!0});var te={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c)},date:ob("date",pe,Rb(pe,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",qe,Rb(qe,"yyyy MM dd HH mm ss sss".split(" ")), +"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",se,Rb(se,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Oc,function(a,b){if(ha(a))return a;if(C(a)){Oc.lastIndex=0;var d=Oc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Zd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:ob("month",re,Rb(re,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Jc(a,b,d,c,"number");fe(c);Sa(a, +b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||A(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||A(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)|| +A(s)||ge(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||xh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||yh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==V(d.ngTrim);A(d.name)&&b.attr("name",++qb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g= +V(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=V(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);Y(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);Y(c.$modelValue)||(m?(a=b.val(),n=p},g("min",k)); +e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||A(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:function(a,b){return c.$isEmpty(b)||A(s)||ge(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=he(k,a,"ngTrueValue",d.ngTrueValue,!0),l=he(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty= +function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},$c=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(te[K(g.type)]||te.text)(e,f,g,k[0],b,a,d,c)}}}}],Af=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}}; +return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&Object.defineProperty(b,"value",a)}}}}},zh=/^(true|false|\d+)$/,xf=function(){function a(a,d,c){var e=w(c)?c:9===wa?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return zh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue, +function(b){a(d,f,b)})}}}},We=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=jc(a)})}}}}],Ye=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=A(a)?"":a})}}}}], +Xe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],wf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ze=Lc("",!0),af=Lc("Odd",0),$e=Lc("Even",1),bf=Ra({compile:function(a, +b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),cf=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],ed={},Ah={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=xa("ng-"+a);ed[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return sd(d,c,e,b,a,Ah[a])}]});var ff=["$animate","$compile",function(a,b){return{multiElement:!0, +transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=ub(k.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],gf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element", +controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded", +f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],zf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?(d.empty(),a(gd(e.template,z.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],hf=Ra({priority:450,compile:function(){return{pre:function(a, +b,d){a.$eval(d.ngInit)}}}}),vf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?V(e):e;c.$parsers.push(function(a){if(!A(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?V(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",be="ng-invalid",Za="ng-pristine",Wb="ng-dirty",pb=F("ngModel");Sb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" "); +Sb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,Aa(this.$$element));},$render:E,$isEmpty:function(a){return A(a)|| +""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Wb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element, +Za);this.$$animate.addClass(this.$$element,Wb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!Y(this.$modelValue)){var a= +this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e, +g){var h=e(a,b);if(!h||!B(h.then))throw pb("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(A(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a, +b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid= +A(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;dg||e.$isEmpty(b)||b.length<=g}}}}}],cd= +["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Ub(f)||-1;c.$observe("minlength",function(a){f!==a&&(g=Ub(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];z.angular.bootstrap?z.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Je(),Oe(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1== +b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "), +WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, +c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ee(z.document,Wc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(window.angular.element("