diff --git a/bower.json b/bower.json
index 88cb0b3df4a..2e45c456274 100644
--- a/bower.json
+++ b/bower.json
@@ -14,13 +14,10 @@
],
"dependencies": {
"jquery": "~2.1.4",
- "angular": "~1.4.3",
- "angular-route": "~1.4.3",
- "angular-mocks": "~1.4.3",
- "angular-sanitize": "~1.4.3",
- "angular-native-dragdrop": "~1.1.1",
- "angular-bindonce": "~0.3.3",
- "requirejs": "~2.1.18",
- "requirejs-text": "~2.0.14"
+ "angular": "~1.5.0",
+ "angular-route": "~1.5.0",
+ "angular-mocks": "~1.5.0",
+ "angular-sanitize": "~1.5.0",
+ "angular-bindonce": "~0.3.3"
}
}
diff --git a/public/app/plugins/panels/graph/module.html b/public/app/plugins/panels/graph/module.html
index 9c49c42741d..068223a5366 100644
--- a/public/app/plugins/panels/graph/module.html
+++ b/public/app/plugins/panels/graph/module.html
@@ -10,7 +10,7 @@
Datapoints outside time range Can be caused by timezone mismatch between browser and graphite server
-
+
diff --git a/public/app/plugins/panels/graph/module.js b/public/app/plugins/panels/graph/module.js
index 817a924d965..731fb47ae6d 100644
--- a/public/app/plugins/panels/graph/module.js
+++ b/public/app/plugins/panels/graph/module.js
@@ -138,6 +138,10 @@ function (angular, _, moment, kbn, TimeSeries, PanelMeta) {
});
};
+ $scope.zoomOut = function(evt) {
+ $scope.appEvent('zoom-out', evt);
+ };
+
$scope.loadSnapshot = function(snapshotData) {
panelHelper.updateTimeRange($scope);
$scope.annotationsPromise = $q.when([]);
diff --git a/public/app/require_config.js b/public/app/require_config.js
deleted file mode 100644
index 66165c40017..00000000000
--- a/public/app/require_config.js
+++ /dev/null
@@ -1,91 +0,0 @@
-require.config({
- urlArgs: 'bust=' + (new Date().getTime()),
- baseUrl: 'public',
-
- paths: {
- 'lodash-src': 'vendor/lodash',
- lodash: 'app/core/lodash_extended',
-
- text: 'vendor/requirejs-text/text',
- moment: 'vendor/moment',
- angular: 'vendor/angular/angular',
- 'angular-route': 'vendor/angular-route/angular-route',
- 'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize',
- 'angular-dragdrop': 'vendor/angular-native-dragdrop/draganddrop',
- 'angular-strap': 'vendor/angular-other/angular-strap',
- 'angular-ui': 'vendor/angular-ui/ui-bootstrap-tpls',
- timepicker: 'vendor/angular-other/timepicker',
- datepicker: 'vendor/angular-other/datepicker',
- bindonce: 'vendor/angular-bindonce/bindonce',
- crypto: 'vendor/crypto.min',
- spectrum: 'vendor/spectrum',
- bootstrap: 'vendor/bootstrap/bootstrap',
- jquery: 'vendor/jquery/dist/jquery',
-
- 'jquery.flot': 'vendor/flot/jquery.flot',
- 'jquery.flot.pie': 'vendor/flot/jquery.flot.pie',
- 'jquery.flot.events': 'vendor/flot/jquery.flot.events',
- 'jquery.flot.selection': 'vendor/flot/jquery.flot.selection',
- 'jquery.flot.stack': 'vendor/flot/jquery.flot.stack',
- 'jquery.flot.stackpercent':'vendor/flot/jquery.flot.stackpercent',
- 'jquery.flot.time': 'vendor/flot/jquery.flot.time',
- 'jquery.flot.crosshair': 'vendor/flot/jquery.flot.crosshair',
- 'jquery.flot.fillbelow': 'vendor/flot/jquery.flot.fillbelow',
-
- modernizr: 'vendor/modernizr-2.6.1',
-
- 'bootstrap-tagsinput': 'vendor/tagsinput/bootstrap-tagsinput',
- },
-
- shim: {
-
- spectrum: {
- deps: ['jquery']
- },
-
- crypto: {
- exports: 'Crypto'
- },
-
- angular: {
- deps: ['jquery'],
- exports: 'angular'
- },
-
- bootstrap: {
- deps: ['jquery']
- },
-
- modernizr: {
- exports: 'Modernizr'
- },
-
- jquery: {
- exports: 'jQuery'
- },
-
- // simple dependency declaration
- //
- 'jquery.flot': ['jquery'],
- 'jquery.flot.pie': ['jquery', 'jquery.flot'],
- 'jquery.flot.events': ['jquery', 'jquery.flot'],
- 'jquery.flot.selection':['jquery', 'jquery.flot'],
- 'jquery.flot.stack': ['jquery', 'jquery.flot'],
- 'jquery.flot.stackpercent':['jquery', 'jquery.flot'],
- 'jquery.flot.time': ['jquery', 'jquery.flot'],
- 'jquery.flot.crosshair':['jquery', 'jquery.flot'],
- 'jquery.flot.fillbelow':['jquery', 'jquery.flot'],
- 'angular-dragdrop': ['jquery', 'angular'],
- 'angular-mocks': ['angular'],
- 'angular-sanitize': ['angular'],
- 'angular-ui': ['angular'],
- 'angular-route': ['angular'],
- 'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
- 'bindonce': ['angular'],
-
- timepicker: ['jquery', 'bootstrap'],
- datepicker: ['jquery', 'bootstrap'],
-
- 'bootstrap-tagsinput': ['jquery'],
- },
-});
diff --git a/public/app/system.conf.js b/public/app/system.conf.js
index d3e45c8f613..7238bd65ec2 100644
--- a/public/app/system.conf.js
+++ b/public/app/system.conf.js
@@ -34,6 +34,9 @@ System.config({
vendor: {
defaultExtension: 'js',
},
+ plugins: {
+ defaultExtension: 'js',
+ },
test: {
defaultExtension: 'js',
},
diff --git a/public/vendor/angular-mocks/.bower.json b/public/vendor/angular-mocks/.bower.json
index 9fea1563a4d..d7796dd7ec5 100644
--- a/public/vendor/angular-mocks/.bower.json
+++ b/public/vendor/angular-mocks/.bower.json
@@ -1,19 +1,19 @@
{
"name": "angular-mocks",
- "version": "1.4.3",
+ "version": "1.5.0-rc.0",
"main": "./angular-mocks.js",
"ignore": [],
"dependencies": {
- "angular": "1.4.3"
+ "angular": "1.5.0-rc.0"
},
"homepage": "https://github.com/angular/bower-angular-mocks",
- "_release": "1.4.3",
+ "_release": "1.5.0-rc.0",
"_resolution": {
"type": "version",
- "tag": "v1.4.3",
- "commit": "7e3beec84afceeb060a3c6def0d7ca965727851c"
+ "tag": "v1.5.0-rc.0",
+ "commit": "aaa4993ea1fa4aac97953d65bcaeb5f6eda6f113"
},
"_source": "git://github.com/angular/bower-angular-mocks.git",
- "_target": "~1.4.3",
+ "_target": "~1.5.0",
"_originalSource": "angular-mocks"
}
\ No newline at end of file
diff --git a/public/vendor/angular-mocks/angular-mocks.js b/public/vendor/angular-mocks/angular-mocks.js
index 9482b702b37..481fb2050c1 100644
--- a/public/vendor/angular-mocks/angular-mocks.js
+++ b/public/vendor/angular-mocks/angular-mocks.js
@@ -1,5 +1,5 @@
/**
- * @license AngularJS v1.4.3
+ * @license AngularJS v1.5.0-rc.0
* (c) 2010-2015 Google, Inc. http://angularjs.org
* License: MIT
*/
@@ -94,7 +94,7 @@ angular.mock.$Browser = function() {
if (fn.id === deferId) fnIndex = index;
});
- if (fnIndex !== undefined) {
+ if (angular.isDefined(fnIndex)) {
self.deferredFns.splice(fnIndex, 1);
return true;
}
@@ -469,7 +469,7 @@ angular.mock.$IntervalProvider = function() {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
- if (fnIndex !== undefined) {
+ if (angular.isDefined(fnIndex)) {
repeatFns.splice(fnIndex, 1);
}
}
@@ -511,7 +511,7 @@ angular.mock.$IntervalProvider = function() {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
- if (fnIndex !== undefined) {
+ if (angular.isDefined(fnIndex)) {
repeatFns[fnIndex].deferred.reject('canceled');
repeatFns.splice(fnIndex, 1);
return true;
@@ -762,36 +762,70 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
.config(['$provide', function($provide) {
- var reflowQueue = [];
- $provide.value('$$animateReflow', function(fn) {
- var index = reflowQueue.length;
- reflowQueue.push(fn);
- return function cancel() {
- reflowQueue.splice(index, 1);
+ $provide.factory('$$forceReflow', function() {
+ function reflowFn() {
+ reflowFn.totalReflows++;
+ }
+ reflowFn.totalReflows = 0;
+ return reflowFn;
+ });
+
+ $provide.factory('$$animateAsyncRun', function() {
+ var queue = [];
+ var queueFn = function() {
+ return function(fn) {
+ queue.push(fn);
+ };
};
+ queueFn.flush = function() {
+ if (queue.length === 0) return false;
+
+ for (var i = 0; i < queue.length; i++) {
+ queue[i]();
+ }
+ queue = [];
+
+ return true;
+ };
+ return queueFn;
});
$provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF',
- function($delegate, $timeout, $browser, $$rAF) {
+ '$$forceReflow', '$$animateAsyncRun', '$rootScope',
+ function($delegate, $timeout, $browser, $$rAF,
+ $$forceReflow, $$animateAsyncRun, $rootScope) {
var animate = {
queue: [],
cancel: $delegate.cancel,
+ on: $delegate.on,
+ off: $delegate.off,
+ pin: $delegate.pin,
+ get reflows() {
+ return $$forceReflow.totalReflows;
+ },
enabled: $delegate.enabled,
- triggerCallbackEvents: function() {
- $$rAF.flush();
- },
- triggerCallbackPromise: function() {
- $timeout.flush(0);
- },
- triggerCallbacks: function() {
- this.triggerCallbackEvents();
- this.triggerCallbackPromise();
- },
- triggerReflow: function() {
- angular.forEach(reflowQueue, function(fn) {
- fn();
- });
- reflowQueue = [];
+ flush: function() {
+ $rootScope.$digest();
+
+ var doNextRun, somethingFlushed = false;
+ do {
+ doNextRun = false;
+
+ if ($$rAF.queue.length) {
+ $$rAF.flush();
+ doNextRun = somethingFlushed = true;
+ }
+
+ if ($$animateAsyncRun.flush()) {
+ doNextRun = somethingFlushed = true;
+ }
+ } while (doNextRun);
+
+ if (!somethingFlushed) {
+ throw new Error('No pending animations ready to be closed or flushed');
+ }
+
+ $rootScope.$digest();
}
};
@@ -916,7 +950,7 @@ angular.mock.dump = function(object) {
* - `$httpBackend.when` - specifies a backend definition
*
*
- * # Request Expectations vs Backend Definitions
+ * ## Request Expectations vs Backend Definitions
*
* Request expectations provide a way to make assertions about requests made by the application and
* to define responses for those requests. The test will fail if the expected requests are not made
@@ -972,7 +1006,7 @@ angular.mock.dump = function(object) {
* the request. The response from the first matched definition is returned.
*
*
- * # Flushing HTTP requests
+ * ## Flushing HTTP requests
*
* The $httpBackend used in production always responds to requests asynchronously. If we preserved
* this behavior in unit testing, we'd have to create async unit tests, which are hard to write,
@@ -982,7 +1016,7 @@ angular.mock.dump = function(object) {
* the async api of the backend, while allowing the test to execute synchronously.
*
*
- * # Unit testing with mock $httpBackend
+ * ## Unit testing with mock $httpBackend
* The following code shows how to setup and use the mock backend when unit testing a controller.
* First we create the controller under test:
*
@@ -1008,7 +1042,7 @@ angular.mock.dump = function(object) {
$http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
$scope.status = '';
}).error(function() {
- $scope.status = 'ERROR!';
+ $scope.status = 'Failed...';
});
};
}
@@ -1098,7 +1132,87 @@ angular.mock.dump = function(object) {
$httpBackend.flush();
});
});
- ```
+ ```
+ *
+ * ## Dynamic responses
+ *
+ * You define a response to a request by chaining a call to `respond()` onto a definition or expectation.
+ * If you provide a **callback** as the first parameter to `respond(callback)` then you can dynamically generate
+ * a response based on the properties of the request.
+ *
+ * The `callback` function should be of the form `function(method, url, data, headers, params)`.
+ *
+ * ### Query parameters
+ *
+ * By default, query parameters on request URLs are parsed into the `params` object. So a request URL
+ * of `/list?q=searchstr&orderby=-name` would set `params` to be `{q: 'searchstr', orderby: '-name'}`.
+ *
+ * ### Regex parameter matching
+ *
+ * If an expectation or definition uses a **regex** to match the URL, you can provide an array of **keys** via a
+ * `params` argument. The index of each **key** in the array will match the index of a **group** in the
+ * **regex**.
+ *
+ * The `params` object in the **callback** will now have properties with these keys, which hold the value of the
+ * corresponding **group** in the **regex**.
+ *
+ * This also applies to the `when` and `expect` shortcut methods.
+ *
+ *
+ * ```js
+ * $httpBackend.expect('GET', /\/user\/(.+)/, undefined, undefined, ['id'])
+ * .respond(function(method, url, data, headers, params) {
+ * // for requested url of '/user/1234' params is {id: '1234'}
+ * });
+ *
+ * $httpBackend.whenPATCH(/\/user\/(.+)\/article\/(.+)/, undefined, undefined, ['user', 'article'])
+ * .respond(function(method, url, data, headers, params) {
+ * // for url of '/user/1234/article/567' params is {user: '1234', article: '567'}
+ * });
+ * ```
+ *
+ * ## Matching route requests
+ *
+ * For extra convenience, `whenRoute` and `expectRoute` shortcuts are available. These methods offer colon
+ * delimited matching of the url path, ignoring the query string. This allows declarations
+ * similar to how application routes are configured with `$routeProvider`. Because these methods convert
+ * the definition url to regex, declaration order is important. Combined with query parameter parsing,
+ * the following is possible:
+ *
+ ```js
+ $httpBackend.whenRoute('GET', '/users/:id')
+ .respond(function(method, url, data, headers, params) {
+ return [200, MockUserList[Number(params.id)]];
+ });
+
+ $httpBackend.whenRoute('GET', '/users')
+ .respond(function(method, url, data, headers, params) {
+ var userList = angular.copy(MockUserList),
+ defaultSort = 'lastName',
+ count, pages, isPrevious, isNext;
+
+ // paged api response '/v1/users?page=2'
+ params.page = Number(params.page) || 1;
+
+ // query for last names '/v1/users?q=Archer'
+ if (params.q) {
+ userList = $filter('filter')({lastName: params.q});
+ }
+
+ pages = Math.ceil(userList.length / pagingLength);
+ isPrevious = params.page > 1;
+ isNext = params.page < pages;
+
+ return [200, {
+ count: userList.length,
+ previous: isPrevious,
+ next: isNext,
+ // sort field -> '/v1/users?sortBy=firstName'
+ results: $filter('orderBy')(userList, params.sortBy || defaultSort)
+ .splice((params.page - 1) * pagingLength, pagingLength)
+ }];
+ });
+ ```
*/
angular.mock.$HttpBackendProvider = function() {
this.$get = ['$rootScope', '$timeout', createHttpBackendMock];
@@ -1155,7 +1269,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
return handleResponse;
function handleResponse() {
- var response = wrapped.response(method, url, data, headers);
+ var response = wrapped.response(method, url, data, headers, wrapped.params(url));
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
copy(response[3] || ''));
@@ -1224,20 +1338,21 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers, statusText])
- * | function(function(method, url, data, headers)}`
+ * | function(function(method, url, data, headers, params)}`
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
*/
- $httpBackend.when = function(method, url, data, headers) {
- var definition = new MockHttpExpectation(method, url, data, headers),
+ $httpBackend.when = function(method, url, data, headers, keys) {
+ var definition = new MockHttpExpectation(method, url, data, headers, keys),
chain = {
respond: function(status, data, headers, statusText) {
definition.passThrough = undefined;
@@ -1267,6 +1382,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1281,6 +1397,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1295,6 +1412,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1311,6 +1429,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1327,6 +1446,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1340,12 +1460,59 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
createShortMethods('when');
+ /**
+ * @ngdoc method
+ * @name $httpBackend#whenRoute
+ * @description
+ * Creates a new backend definition that compares only with the requested route.
+ *
+ * @param {string} method HTTP method.
+ * @param {string} url HTTP url string that supports colon param matching.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled. See #when for more info.
+ */
+ $httpBackend.whenRoute = function(method, url) {
+ var pathObj = parseRoute(url);
+ return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
+ };
+
+ function parseRoute(url) {
+ var ret = {
+ regexp: url
+ },
+ keys = ret.keys = [];
+
+ if (!url || !angular.isString(url)) return ret;
+
+ url = url
+ .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('^' + url, 'i');
+ return ret;
+ }
/**
* @ngdoc method
@@ -1361,20 +1528,21 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers, statusText])
- * | function(function(method, url, data, headers)}`
+ * | function(function(method, url, data, headers, params)}`
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
*/
- $httpBackend.expect = function(method, url, data, headers) {
- var expectation = new MockHttpExpectation(method, url, data, headers),
+ $httpBackend.expect = function(method, url, data, headers, keys) {
+ var expectation = new MockHttpExpectation(method, url, data, headers, keys),
chain = {
respond: function(status, data, headers, statusText) {
expectation.response = createResponse(status, data, headers, statusText);
@@ -1386,7 +1554,6 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
return chain;
};
-
/**
* @ngdoc method
* @name $httpBackend#expectGET
@@ -1396,6 +1563,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled. See #expect for more info.
@@ -1410,6 +1578,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1424,6 +1593,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1441,6 +1611,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1458,6 +1629,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1475,6 +1647,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
@@ -1488,12 +1661,30 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives an url
* and returns true if the url matches the current definition.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described above.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
createShortMethods('expect');
+ /**
+ * @ngdoc method
+ * @name $httpBackend#expectRoute
+ * @description
+ * Creates a new request expectation that compares only with the requested route.
+ *
+ * @param {string} method HTTP method.
+ * @param {string} url HTTP url string that supports colon param matching.
+ * @returns {requestHandler} Returns an object with `respond` method that controls how a matched
+ * request is handled. You can save this object for later use and invoke `respond` again in
+ * order to change how a matched request is handled. See #expect for more info.
+ */
+ $httpBackend.expectRoute = function(method, url) {
+ var pathObj = parseRoute(url);
+ return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
+ };
+
/**
* @ngdoc method
@@ -1583,20 +1774,20 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
function createShortMethods(prefix) {
angular.forEach(['GET', 'DELETE', 'JSONP', 'HEAD'], function(method) {
- $httpBackend[prefix + method] = function(url, headers) {
- return $httpBackend[prefix](method, url, undefined, headers);
+ $httpBackend[prefix + method] = function(url, headers, keys) {
+ return $httpBackend[prefix](method, url, undefined, headers, keys);
};
});
angular.forEach(['PUT', 'POST', 'PATCH'], function(method) {
- $httpBackend[prefix + method] = function(url, data, headers) {
- return $httpBackend[prefix](method, url, data, headers);
+ $httpBackend[prefix + method] = function(url, data, headers, keys) {
+ return $httpBackend[prefix](method, url, data, headers, keys);
};
});
}
}
-function MockHttpExpectation(method, url, data, headers) {
+function MockHttpExpectation(method, url, data, headers, keys) {
this.data = data;
this.headers = headers;
@@ -1635,6 +1826,59 @@ function MockHttpExpectation(method, url, data, headers) {
this.toString = function() {
return method + ' ' + url;
};
+
+ this.params = function(u) {
+ return angular.extend(parseQuery(), pathParams());
+
+ function pathParams() {
+ var keyObj = {};
+ if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj;
+
+ var m = url.exec(u);
+ if (!m) return keyObj;
+ for (var i = 1, len = m.length; i < len; ++i) {
+ var key = keys[i - 1];
+ var val = m[i];
+ if (key && val) {
+ keyObj[key.name || key] = val;
+ }
+ }
+
+ return keyObj;
+ }
+
+ function parseQuery() {
+ var obj = {}, key_value, key,
+ queryStr = u.indexOf('?') > -1
+ ? u.substring(u.indexOf('?') + 1)
+ : "";
+
+ angular.forEach(queryStr.split('&'), function(keyValue) {
+ if (keyValue) {
+ key_value = keyValue.replace(/\+/g,'%20').split('=');
+ key = tryDecodeURIComponent(key_value[0]);
+ if (angular.isDefined(key)) {
+ var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
+ if (!hasOwnProperty.call(obj, key)) {
+ obj[key] = val;
+ } else if (angular.isArray(obj[key])) {
+ obj[key].push(val);
+ } else {
+ obj[key] = [obj[key],val];
+ }
+ }
+ }
+ });
+ return obj;
+ }
+ function tryDecodeURIComponent(value) {
+ try {
+ return decodeURIComponent(value);
+ } catch (e) {
+ // Ignore any invalid uri component
+ }
+ }
+ };
}
function createMockXhr() {
@@ -1743,28 +1987,28 @@ angular.mock.$TimeoutDecorator = ['$delegate', '$browser', function($delegate, $
}];
angular.mock.$RAFDecorator = ['$delegate', function($delegate) {
- var queue = [];
var rafFn = function(fn) {
- var index = queue.length;
- queue.push(fn);
+ var index = rafFn.queue.length;
+ rafFn.queue.push(fn);
return function() {
- queue.splice(index, 1);
+ rafFn.queue.splice(index, 1);
};
};
+ rafFn.queue = [];
rafFn.supported = $delegate.supported;
rafFn.flush = function() {
- if (queue.length === 0) {
+ if (rafFn.queue.length === 0) {
throw new Error('No rAF callbacks present');
}
- var length = queue.length;
+ var length = rafFn.queue.length;
for (var i = 0; i < length; i++) {
- queue[i]();
+ rafFn.queue[i]();
}
- queue = queue.slice(i);
+ rafFn.queue = rafFn.queue.slice(i);
};
return rafFn;
@@ -1803,7 +2047,7 @@ angular.mock.$RootElementProvider = function() {
*
* // Controller definition ...
*
- * myMod.controller('MyDirectiveController', ['log', function($log) {
+ * myMod.controller('MyDirectiveController', ['$log', function($log) {
* $log.info(this.name);
* })];
*
@@ -1812,7 +2056,7 @@ angular.mock.$RootElementProvider = function() {
*
* describe('myDirectiveController', function() {
* it('should write the bound name to the log', inject(function($controller, $log) {
- * var ctrl = $controller('MyDirective', { /* no locals */ }, { name: 'Clark Kent' });
+ * var ctrl = $controller('MyDirectiveController', { /* no locals */ }, { name: 'Clark Kent' });
* expect(ctrl.name).toEqual('Clark Kent');
* expect($log.info.logs).toEqual(['Clark Kent']);
* });
@@ -1959,13 +2203,15 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers, statusText])
- * | function(function(method, url, data, headers)}`
+ * | function(function(method, url, data, headers, params)}`
* – The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string), response headers
* (Object), and the text for the status (string).
@@ -1985,6 +2231,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2000,6 +2248,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2015,6 +2265,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2031,6 +2283,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* and returns true if the url matches the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2047,6 +2301,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* and returns true if the url matches the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2063,6 +2319,8 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* and returns true if the url matches the current definition.
* @param {(string|RegExp)=} data HTTP request body.
* @param {(Object|function(Object))=} headers HTTP headers.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2077,6 +2335,21 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
+ * @param {(Array)=} keys Array of keys to assign to regex matches in request url described on
+ * {@link ngMock.$httpBackend $httpBackend mock}.
+ * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
+ * control how a matched request is handled. You can save this object for later use and invoke
+ * `respond` or `passThrough` again in order to change how a matched request is handled.
+ */
+/**
+ * @ngdoc method
+ * @name $httpBackend#whenRoute
+ * @module ngMockE2E
+ * @description
+ * Creates a new backend definition that compares only with the requested route.
+ *
+ * @param {string} method HTTP method.
+ * @param {string} url HTTP url string that supports colon param matching.
* @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that
* control how a matched request is handled. You can save this object for later use and invoke
* `respond` or `passThrough` again in order to change how a matched request is handled.
@@ -2209,10 +2482,12 @@ if (window.jasmine || window.mocha) {
currentSpec.$injector = null;
currentSpec.$modules = null;
+ currentSpec.$providerInjector = null;
currentSpec = null;
if (injector) {
injector.get('$rootElement').off();
+ injector.get('$rootScope').$destroy();
}
// clean up jquery's fragment cache
@@ -2244,8 +2519,9 @@ if (window.jasmine || window.mocha) {
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
* aliases or as anonymous module initialization functions. The modules are used to
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
- * object literal is passed they will be registered as values in the module, the key being
- * the module name and the value being what is returned.
+ * object literal is passed each key-value pair will be registered on the module via
+ * {@link auto.$provide $provide}.value, the key being the string name (or token) to associate
+ * with the value on the injector.
*/
window.module = angular.mock.module = function() {
var moduleFns = Array.prototype.slice.call(arguments, 0);
@@ -2255,16 +2531,21 @@ if (window.jasmine || window.mocha) {
if (currentSpec.$injector) {
throw new Error('Injector already created, can not register a module!');
} else {
- var modules = currentSpec.$modules || (currentSpec.$modules = []);
+ var fn, modules = currentSpec.$modules || (currentSpec.$modules = []);
angular.forEach(moduleFns, function(module) {
if (angular.isObject(module) && !angular.isArray(module)) {
- modules.push(function($provide) {
+ fn = function($provide) {
angular.forEach(module, function(value, key) {
$provide.value(key, value);
});
- });
+ };
} else {
- modules.push(module);
+ fn = module;
+ }
+ if (currentSpec.$providerInjector) {
+ currentSpec.$providerInjector.invoke(fn);
+ } else {
+ modules.push(fn);
}
});
}
@@ -2378,6 +2659,9 @@ if (window.jasmine || window.mocha) {
function workFn() {
var modules = currentSpec.$modules || [];
var strictDi = !!currentSpec.$injectorStrict;
+ modules.unshift(function($injector) {
+ currentSpec.$providerInjector = $injector;
+ });
modules.unshift('ngMock');
modules.unshift('ng');
var injector = currentSpec.$injector;
diff --git a/public/vendor/angular-mocks/bower.json b/public/vendor/angular-mocks/bower.json
index 43a3eed4474..368fcd264ad 100644
--- a/public/vendor/angular-mocks/bower.json
+++ b/public/vendor/angular-mocks/bower.json
@@ -1,9 +1,9 @@
{
"name": "angular-mocks",
- "version": "1.4.3",
+ "version": "1.5.0-rc.0",
"main": "./angular-mocks.js",
"ignore": [],
"dependencies": {
- "angular": "1.4.3"
+ "angular": "1.5.0-rc.0"
}
}
diff --git a/public/vendor/angular-mocks/package.json b/public/vendor/angular-mocks/package.json
index f1c285856dc..8f9901d874f 100644
--- a/public/vendor/angular-mocks/package.json
+++ b/public/vendor/angular-mocks/package.json
@@ -1,6 +1,6 @@
{
"name": "angular-mocks",
- "version": "1.4.3",
+ "version": "1.5.0-rc.0",
"description": "AngularJS mocks for testing",
"main": "angular-mocks.js",
"scripts": {
diff --git a/public/vendor/angular-route/.bower.json b/public/vendor/angular-route/.bower.json
index a72f0871902..e9cb3ee0535 100644
--- a/public/vendor/angular-route/.bower.json
+++ b/public/vendor/angular-route/.bower.json
@@ -1,19 +1,19 @@
{
"name": "angular-route",
- "version": "1.4.3",
+ "version": "1.5.0-rc.0",
"main": "./angular-route.js",
"ignore": [],
"dependencies": {
- "angular": "1.4.3"
+ "angular": "1.5.0-rc.0"
},
"homepage": "https://github.com/angular/bower-angular-route",
- "_release": "1.4.3",
+ "_release": "1.5.0-rc.0",
"_resolution": {
"type": "version",
- "tag": "v1.4.3",
- "commit": "6d34da91a291260d1826c42f163ac21d094cc0fc"
+ "tag": "v1.5.0-rc.0",
+ "commit": "496e2f35541957c1e4903b99622be5bcdb058a7a"
},
"_source": "git://github.com/angular/bower-angular-route.git",
- "_target": "~1.4.3",
+ "_target": "~1.5.0",
"_originalSource": "angular-route"
}
\ No newline at end of file
diff --git a/public/vendor/angular-route/angular-route.js b/public/vendor/angular-route/angular-route.js
index 40536ad26c8..9d8766a1745 100644
--- a/public/vendor/angular-route/angular-route.js
+++ b/public/vendor/angular-route/angular-route.js
@@ -1,5 +1,5 @@
/**
- * @license AngularJS v1.4.3
+ * @license AngularJS v1.5.0-rc.0
* (c) 2010-2015 Google, Inc. http://angularjs.org
* License: MIT
*/
@@ -796,7 +796,6 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
}
.view-animate.ng-enter, .view-animate.ng-leave {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s;
display:block;
@@ -982,6 +981,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/public/vendor/angular-route/angular-route.min.js b/public/vendor/angular-route/angular-route.min.js
index 4634fdbf910..b7f1830fd42 100644
--- a/public/vendor/angular-route/angular-route.min.js
+++ b/public/vendor/angular-route/angular-route.min.js
@@ -1,15 +1,15 @@
/*
- AngularJS v1.4.3
+ AngularJS v1.5.0-rc.0
(c) 2010-2015 Google, Inc. 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
*/
+
+
+/**
+ * @ngdoc provider
+ * @name $sanitizeProvider
+ *
+ * @description
+ * Creates and configures {@link $sanitize} instance.
+ */
function $SanitizeProvider() {
+ var svgEnabled = false;
+
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ if (svgEnabled) {
+ angular.extend(validElements, svgElements);
+ }
return function(html) {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
- return !/^unsafe/.test($$sanitizeUri(uri, isImage));
+ return !/^unsafe:/.test($$sanitizeUri(uri, isImage));
}));
return buf.join('');
};
}];
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#enableSvg
+ * @kind function
+ *
+ * @description
+ * Enables a subset of svg to be supported by the sanitizer.
+ *
+ *
+ *
By enabling this setting without taking other precautions, you might expose your
+ * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned
+ * outside of the containing element and be rendered over other elements on the page (e.g. a login
+ * link). Such behavior can then result in phishing incidents.
+ *
+ *
To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg
+ * tags within the sanitized content:
',
+ * bindings: {name: '@'}
+ * });
+ *
+ * myMod.component('myComp', {
+ * templateUrl: 'views/my-comp.html',
+ * controller: 'MyCtrl as ctrl',
+ * bindings: {name: '@'}
+ * });
+ *
+ * ```
+ *
+ * See {@link ng.$compileProvider#directive $compileProvider.directive()}.
+ */
+ component: function(name, options) {
+ function factory($injector) {
+ function makeInjectable(fn) {
+ if (angular.isFunction(fn)) {
+ return function(tElement, tAttrs) {
+ return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
+ };
+ } else {
+ return fn;
+ }
+ }
+
+ var template = (!options.template && !options.templateUrl ? '' : options.template);
+ return {
+ controller: options.controller || function() {},
+ controllerAs: identifierForController(options.controller) || options.controllerAs || name,
+ template: makeInjectable(template),
+ templateUrl: makeInjectable(options.templateUrl),
+ transclude: options.transclude === undefined ? true : options.transclude,
+ scope: options.isolate === false ? true : {},
+ bindToController: options.bindings || {},
+ restrict: options.restrict || 'E'
+ };
+ }
+
+ if (options.$canActivate) {
+ factory.$canActivate = options.$canActivate;
+ }
+ if (options.$routeConfig) {
+ factory.$routeConfig = options.$routeConfig;
+ }
+ factory.$inject = ['$injector'];
+
+ return moduleInstance.directive(name, factory);
+ },
+
/**
* @ngdoc method
* @name angular.Module#config
@@ -2222,7 +2377,7 @@ function serializeObject(obj) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
- if (seen.indexOf(val) >= 0) return '<>';
+ if (seen.indexOf(val) >= 0) return '...';
seen.push(val);
}
@@ -2233,7 +2388,7 @@ function serializeObject(obj) {
function toDebugString(obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
- } else if (typeof obj === 'undefined') {
+ } else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
return serializeObject(obj);
@@ -2244,7 +2399,6 @@ function toDebugString(obj) {
/* global angularModule: true,
version: true,
- $LocaleProvider,
$CompileProvider,
htmlAnchorDirective,
@@ -2261,7 +2415,6 @@ function toDebugString(obj) {
ngClassDirective,
ngClassEvenDirective,
ngClassOddDirective,
- ngCspDirective,
ngCloakDirective,
ngControllerDirective,
ngFormDirective,
@@ -2298,14 +2451,18 @@ function toDebugString(obj) {
$AnchorScrollProvider,
$AnimateProvider,
+ $CoreAnimateCssProvider,
$$CoreAnimateQueueProvider,
- $$CoreAnimateRunnerProvider,
+ $$AnimateRunnerFactoryProvider,
+ $$AnimateAsyncRunFactoryProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
+ $DateProvider,
$DocumentProvider,
$ExceptionHandlerProvider,
$FilterProvider,
+ $$ForceReflowProvider,
$InterpolateProvider,
$IntervalProvider,
$$HashMapProvider,
@@ -2313,6 +2470,7 @@ function toDebugString(obj) {
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
+ $xhrFactoryProvider,
$LocationProvider,
$LogProvider,
$ParseProvider,
@@ -2339,8 +2497,9 @@ function toDebugString(obj) {
* @name angular.version
* @module ng
* @description
- * An object that contains information about the current AngularJS version. This object has the
- * following properties:
+ * An object that contains information about the current AngularJS version.
+ *
+ * This object has the following properties:
*
* - `full` – `{string}` – Full version string, such as "0.9.18".
* - `major` – `{number}` – Major version number, such as "0".
@@ -2349,11 +2508,11 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.4.3', // all of these placeholder strings will be replaced by grunt's
+ full: '1.5.0-rc.0', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task
- minor: 4,
- dot: 3,
- codeName: 'foam-acceleration'
+ minor: 5,
+ dot: 0,
+ codeName: 'oblong-panoptikum'
};
@@ -2392,11 +2551,6 @@ function publishExternalAPI(angular) {
});
angularModule = setupModuleLoader(window);
- try {
- angularModule('ngLocale');
- } catch (e) {
- angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
- }
angularModule('ng', ['ngLocale'], ['$provide',
function ngModule($provide) {
@@ -2459,20 +2613,24 @@ function publishExternalAPI(angular) {
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
+ $animateCss: $CoreAnimateCssProvider,
$$animateQueue: $$CoreAnimateQueueProvider,
- $$AnimateRunner: $$CoreAnimateRunnerProvider,
+ $$AnimateRunner: $$AnimateRunnerFactoryProvider,
+ $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
$document: $DocumentProvider,
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
+ $$forceReflow: $$ForceReflowProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
+ $xhrFactory: $xhrFactoryProvider,
$location: $LocationProvider,
$log: $LogProvider,
$parse: $ParseProvider,
@@ -2529,16 +2687,22 @@ function publishExternalAPI(angular) {
*
* If jQuery is available, `angular.element` is an alias for the
* [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element`
- * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite."
+ * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
*
- *
jqLite is a tiny, API-compatible subset of jQuery that allows
- * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most
- * commonly needed functionality with the goal of having a very small footprint.
+ * jqLite is a tiny, API-compatible subset of jQuery that allows
+ * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
+ * commonly needed functionality with the goal of having a very small footprint.
*
- * To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
+ * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the
+ * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a
+ * specific version of jQuery if multiple versions exist on the page.
*
- *
**Note:** all element references in Angular are always wrapped with jQuery or
- * jqLite; they are never raw DOM references.
+ *
**Note:** All element references in Angular are always wrapped with jQuery or
+ * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.
+ *
+ *
**Note:** Keep in mind that this function will not find elements
+ * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)`
+ * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.
*
* ## Angular's jqLite
* jqLite provides only the following jQuery methods:
@@ -2551,7 +2715,8 @@ function publishExternalAPI(angular) {
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
- * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'.
+ * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`.
+ * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing.
* - [`data()`](http://api.jquery.com/data/)
* - [`detach()`](http://api.jquery.com/detach/)
* - [`empty()`](http://api.jquery.com/empty/)
@@ -2561,7 +2726,7 @@ function publishExternalAPI(angular) {
* - [`html()`](http://api.jquery.com/html/)
* - [`next()`](http://api.jquery.com/next/) - Does not support selectors
* - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData
- * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors
+ * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter
* - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors
* - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors
* - [`prepend()`](http://api.jquery.com/prepend/)
@@ -2575,7 +2740,7 @@ function publishExternalAPI(angular) {
* - [`text()`](http://api.jquery.com/text/)
* - [`toggleClass()`](http://api.jquery.com/toggleClass/)
* - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers.
- * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces
+ * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter
* - [`val()`](http://api.jquery.com/val/)
* - [`wrap()`](http://api.jquery.com/wrap/)
*
@@ -2647,10 +2812,10 @@ function camelCase(name) {
replace(MOZ_HACK_REGEXP, 'Moz$1');
}
-var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;
+var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
var HTML_REGEXP = /<|?\w+;/;
-var TAG_NAME_REGEXP = /<([\w:]+)/;
-var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
+var TAG_NAME_REGEXP = /<([\w:-]+)/;
+var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;
var wrapMap = {
'option': [1, ''],
@@ -2685,6 +2850,12 @@ function jqLiteHasData(node) {
return false;
}
+function jqLiteCleanData(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ jqLiteRemoveData(nodes[i]);
+ }
+}
+
function jqLiteBuildFragment(html, context) {
var tmp, tag, wrap,
fragment = context.createDocumentFragment(),
@@ -2737,6 +2908,14 @@ function jqLiteParseHTML(html, context) {
return [];
}
+
+// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
+var jqLiteContains = Node.prototype.contains || function(arg) {
+ // jshint bitwise: false
+ return !!(this.compareDocumentPosition(arg) & 16);
+ // jshint bitwise: true
+};
+
/////////////////////////////////////////////
function JQLite(element) {
if (element instanceof JQLite) {
@@ -2795,17 +2974,23 @@ function jqLiteOff(element, type, fn, unsupported) {
delete events[type];
}
} else {
- forEach(type.split(' '), function(type) {
- if (isDefined(fn)) {
- var listenerFns = events[type];
- arrayRemove(listenerFns || [], fn);
- if (listenerFns && listenerFns.length > 0) {
- return;
- }
- }
- removeEventListenerFn(element, type, handle);
- delete events[type];
+ var removeHandler = function(type) {
+ var listenerFns = events[type];
+ if (isDefined(fn)) {
+ arrayRemove(listenerFns || [], fn);
+ }
+ if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
+ removeEventListenerFn(element, type, handle);
+ delete events[type];
+ }
+ };
+
+ forEach(type.split(' '), function(type) {
+ removeHandler(type);
+ if (MOUSE_EVENT_MAP[type]) {
+ removeHandler(MOUSE_EVENT_MAP[type]);
+ }
});
}
}
@@ -2946,7 +3131,7 @@ function jqLiteInheritedData(element, name, value) {
while (element) {
for (var i = 0, ii = names.length; i < ii; i++) {
- if ((value = jqLite.data(element, names[i])) !== undefined) return value;
+ if (isDefined(value = jqLite.data(element, names[i]))) return value;
}
// If dealing with a document fragment node with a host element, and no parent, use the host
@@ -3052,15 +3237,15 @@ function getBooleanAttrName(element, name) {
return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr;
}
-function getAliasedAttrName(element, name) {
- var nodeName = element.nodeName;
- return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name];
+function getAliasedAttrName(name) {
+ return ALIASED_ATTR[name];
}
forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
- hasData: jqLiteHasData
+ hasData: jqLiteHasData,
+ cleanData: jqLiteCleanData
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -3191,7 +3376,7 @@ forEach({
// in a way that survives minification.
// jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
- (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) {
+ (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
@@ -3212,7 +3397,7 @@ forEach({
// TODO: do we still need this?
var value = fn.$dv;
// Only if we have $dv do we iterate over all, otherwise it is just the first element.
- var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount;
+ var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount;
for (var j = 0; j < jj; j++) {
var nodeValue = fn(this[j], arg1, arg2);
value = value ? value + nodeValue : nodeValue;
@@ -3261,6 +3446,9 @@ function createEventHandler(element, events) {
return event.immediatePropagationStopped === true;
};
+ // Some events have special handlers that wrap the real handler
+ var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper;
+
// Copy event handlers in case event handlers array is modified during execution.
if ((eventFnsLength > 1)) {
eventFns = shallowCopy(eventFns);
@@ -3268,7 +3456,7 @@ function createEventHandler(element, events) {
for (var i = 0; i < eventFnsLength; i++) {
if (!event.isImmediatePropagationStopped()) {
- eventFns[i].call(element, event);
+ handlerWrapper(element, event, eventFns[i]);
}
}
};
@@ -3279,6 +3467,22 @@ function createEventHandler(element, events) {
return eventHandler;
}
+function defaultHandlerWrapper(element, event, handler) {
+ handler.call(element, event);
+}
+
+function specialMouseHandlerWrapper(target, event, handler) {
+ // Refer to jQuery's implementation of mouseenter & mouseleave
+ // Read about mouseenter and mouseleave:
+ // http://www.quirksmode.org/js/events_mouse.html#link8
+ var related = event.relatedTarget;
+ // For mousenter/leave call the handler if related is outside the target.
+ // NB: No relatedTarget if the mouse left/entered the browser window
+ if (!related || (related !== target && !jqLiteContains.call(target, related))) {
+ handler.call(target, event);
+ }
+}
+
//////////////////////////////////////////
// Functions iterating traversal.
// These functions chain results into a single
@@ -3307,35 +3511,28 @@ forEach({
var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
var i = types.length;
- while (i--) {
- type = types[i];
+ var addHandler = function(type, specialHandlerWrapper, noEventListener) {
var eventFns = events[type];
if (!eventFns) {
- events[type] = [];
-
- if (type === 'mouseenter' || type === 'mouseleave') {
- // Refer to jQuery's implementation of mouseenter & mouseleave
- // Read about mouseenter and mouseleave:
- // http://www.quirksmode.org/js/events_mouse.html#link8
-
- jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
- var target = this, related = event.relatedTarget;
- // For mousenter/leave call the handler if related is outside the target.
- // NB: No relatedTarget if the mouse left/entered the browser window
- if (!related || (related !== target && !target.contains(related))) {
- handle(event, type);
- }
- });
-
- } else {
- if (type !== '$destroy') {
- addEventListenerFn(element, type, handle);
- }
+ eventFns = events[type] = [];
+ eventFns.specialHandlerWrapper = specialHandlerWrapper;
+ if (type !== '$destroy' && !noEventListener) {
+ addEventListenerFn(element, type, handle);
}
- eventFns = events[type];
}
+
eventFns.push(fn);
+ };
+
+ while (i--) {
+ type = types[i];
+ if (MOUSE_EVENT_MAP[type]) {
+ addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper);
+ addHandler(type, undefined, true);
+ } else {
+ addHandler(type);
+ }
}
},
@@ -3686,17 +3883,23 @@ var $$HashMapProvider = [function() {
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
+var ARROW_ARG = /^([^\(]+?)=>/;
+var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
+function extractArgs(fn) {
+ var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
+ args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
+ return args;
+}
+
function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
- var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
- args = fnText.match(FN_ARGS);
+ var args = extractArgs(fn);
if (args) {
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
}
@@ -3705,7 +3908,6 @@ function anonFn(fn) {
function annotate(fn, strictDi, name) {
var $inject,
- fnText,
argDecl,
last;
@@ -3720,8 +3922,7 @@ function annotate(fn, strictDi, name) {
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
- fnText = fn.toString().replace(STRIP_COMMENTS, '');
- argDecl = fnText.match(FN_ARGS);
+ argDecl = extractArgs(fn);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
@@ -4262,14 +4463,19 @@ function createInjector(modulesToLoad, strictDi) {
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),
instanceCache = {},
- instanceInjector = (instanceCache.$injector =
+ protoInstanceInjector =
createInternalInjector(instanceCache, function(serviceName, caller) {
var provider = providerInjector.get(serviceName + providerSuffix, caller);
- return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
- }));
+ return instanceInjector.invoke(
+ provider.$get, provider, undefined, serviceName);
+ }),
+ instanceInjector = protoInstanceInjector;
-
- forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
+ providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ var runBlocks = loadModules(modulesToLoad);
+ instanceInjector = protoInstanceInjector.get('$injector');
+ instanceInjector.strictDi = strictDi;
+ forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
return instanceInjector;
@@ -4342,6 +4548,7 @@ function createInjector(modulesToLoad, strictDi) {
// Module Loading
////////////////////////////////////
function loadModules(modulesToLoad) {
+ assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array');
var runBlocks = [], moduleFn;
forEach(modulesToLoad, function(module) {
if (loadedModules.get(module)) return;
@@ -4515,7 +4722,7 @@ function $AnchorScrollProvider() {
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
* in the
- * [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
+ * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
*
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
* match any anchor whenever it changes. This can be disabled by calling
@@ -4798,29 +5005,6 @@ function prepareAnimateOptions(options) {
: {};
}
-var $$CoreAnimateRunnerProvider = function() {
- this.$get = ['$q', '$$rAF', function($q, $$rAF) {
- function AnimateRunner() {}
- AnimateRunner.all = noop;
- AnimateRunner.chain = noop;
- AnimateRunner.prototype = {
- end: noop,
- cancel: noop,
- resume: noop,
- pause: noop,
- complete: noop,
- then: function(pass, fail) {
- return $q(function(resolve) {
- $$rAF(function() {
- resolve();
- });
- }).then(pass, fail);
- }
- };
- return AnimateRunner;
- }];
-};
-
// this is prefixed with Core since it conflicts with
// the animateQueueProvider defined in ngAnimate/animateQueue.js
var $$CoreAnimateQueueProvider = function() {
@@ -4846,65 +5030,75 @@ var $$CoreAnimateQueueProvider = function() {
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- return new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner(); // jshint ignore:line
+
+ // since there are no animations to run the runner needs to be
+ // notified that the animation call is complete.
+ runner.complete();
+ return runner;
}
};
- function addRemoveClassesPostDigest(element, add, remove) {
- var data = postDigestQueue.get(element);
- var classVal;
- if (!data) {
- postDigestQueue.put(element, data = {});
- postDigestElements.push(element);
- }
-
- if (add) {
- forEach(add.split(' '), function(className) {
+ function updateData(data, classes, value) {
+ var changed = false;
+ if (classes) {
+ classes = isString(classes) ? classes.split(' ') :
+ isArray(classes) ? classes : [];
+ forEach(classes, function(className) {
if (className) {
- data[className] = true;
+ changed = true;
+ data[className] = value;
}
});
}
+ return changed;
+ }
- if (remove) {
- forEach(remove.split(' '), function(className) {
- if (className) {
- data[className] = false;
- }
- });
- }
-
- if (postDigestElements.length > 1) return;
-
- $rootScope.$$postDigest(function() {
- forEach(postDigestElements, function(element) {
- var data = postDigestQueue.get(element);
- if (data) {
- var existing = splitClasses(element.attr('class'));
- var toAdd = '';
- var toRemove = '';
- forEach(data, function(status, className) {
- var hasClass = !!existing[className];
- if (status !== hasClass) {
- if (status) {
- toAdd += (toAdd.length ? ' ' : '') + className;
- } else {
- toRemove += (toRemove.length ? ' ' : '') + className;
- }
+ function handleCSSClassChanges() {
+ forEach(postDigestElements, function(element) {
+ var data = postDigestQueue.get(element);
+ if (data) {
+ var existing = splitClasses(element.attr('class'));
+ var toAdd = '';
+ var toRemove = '';
+ forEach(data, function(status, className) {
+ var hasClass = !!existing[className];
+ if (status !== hasClass) {
+ if (status) {
+ toAdd += (toAdd.length ? ' ' : '') + className;
+ } else {
+ toRemove += (toRemove.length ? ' ' : '') + className;
}
- });
+ }
+ });
- forEach(element, function(elm) {
- toAdd && jqLiteAddClass(elm, toAdd);
- toRemove && jqLiteRemoveClass(elm, toRemove);
- });
- postDigestQueue.remove(element);
- }
- });
-
- postDigestElements.length = 0;
+ forEach(element, function(elm) {
+ toAdd && jqLiteAddClass(elm, toAdd);
+ toRemove && jqLiteRemoveClass(elm, toRemove);
+ });
+ postDigestQueue.remove(element);
+ }
});
+ postDigestElements.length = 0;
+ }
+
+
+ function addRemoveClassesPostDigest(element, add, remove) {
+ var data = postDigestQueue.get(element) || {};
+
+ var classesAdded = updateData(data, add, true);
+ var classesRemoved = updateData(data, remove, false);
+
+ if (classesAdded || classesRemoved) {
+
+ postDigestQueue.put(element, data);
+ postDigestElements.push(element);
+
+ if (postDigestElements.length === 1) {
+ $rootScope.$$postDigest(handleCSSClassChanges);
+ }
+ }
}
}];
};
@@ -5025,7 +5219,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* when an animation is detected (and animations are enabled), $animate will do the heavy lifting
* to ensure that animation runs with the triggered DOM operation.
*
- * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
+ * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't
* included and only when it is active then the animation hooks that `$animate` triggers will be
* functional. Once active then all structural `ng-` directives will trigger animations as they perform
* their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
@@ -5334,15 +5528,242 @@ var $AnimateProvider = ['$provide', function($provide) {
}];
}];
-function $$AsyncCallbackProvider() {
- this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) {
- return $$rAF.supported
- ? function(fn) { return $$rAF(fn); }
- : function(fn) {
- return $timeout(fn, 0, false);
+var $$AnimateAsyncRunFactoryProvider = function() {
+ this.$get = ['$$rAF', function($$rAF) {
+ var waitQueue = [];
+
+ function waitForTick(fn) {
+ waitQueue.push(fn);
+ if (waitQueue.length > 1) return;
+ $$rAF(function() {
+ for (var i = 0; i < waitQueue.length; i++) {
+ waitQueue[i]();
+ }
+ waitQueue = [];
+ });
+ }
+
+ return function() {
+ var passed = false;
+ waitForTick(function() {
+ passed = true;
+ });
+ return function(callback) {
+ passed ? callback() : waitForTick(callback);
};
+ };
}];
-}
+};
+
+var $$AnimateRunnerFactoryProvider = function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun',
+ function($q, $sniffer, $$animateAsyncRun) {
+
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ AnimateRunner.chain = function(chain, callback) {
+ var index = 0;
+
+ next();
+ function next() {
+ if (index === chain.length) {
+ callback(true);
+ return;
+ }
+
+ chain[index](function(response) {
+ if (response === false) {
+ callback(false);
+ return;
+ }
+ index++;
+ next();
+ });
+ }
+ };
+
+ AnimateRunner.all = function(runners, callback) {
+ var count = 0;
+ var status = true;
+ forEach(runners, function(runner) {
+ runner.done(onProgress);
+ });
+
+ function onProgress(response) {
+ status = status && response;
+ if (++count === runners.length) {
+ callback(status);
+ }
+ }
+ };
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ this._doneCallbacks = [];
+ this._runInAnimationFrame = $$animateAsyncRun();
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._runInAnimationFrame(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+ }];
+};
+
+/**
+ * @ngdoc service
+ * @name $animateCss
+ * @kind object
+ *
+ * @description
+ * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included,
+ * then the `$animateCss` service will actually perform animations.
+ *
+ * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}.
+ */
+var $CoreAnimateCssProvider = function() {
+ this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) {
+
+ return function(element, initialOptions) {
+ // we always make a copy of the options since
+ // there should never be any side effects on
+ // the input data when running `$animateCss`.
+ var options = copy(initialOptions);
+
+ // there is no point in applying the styles since
+ // there is no animation that goes on at all in
+ // this version of $animateCss.
+ if (options.cleanupStyles) {
+ options.from = options.to = null;
+ }
+
+ if (options.from) {
+ element.css(options.from);
+ options.from = null;
+ }
+
+ /* jshint newcap: false */
+ var closed, runner = new $$AnimateRunner();
+ return {
+ start: run,
+ end: run
+ };
+
+ function run() {
+ $$rAF(function() {
+ applyAnimationContents();
+ if (!closed) {
+ runner.complete();
+ }
+ closed = true;
+ });
+ return runner;
+ }
+
+ function applyAnimationContents() {
+ if (options.addClass) {
+ element.addClass(options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ element.removeClass(options.removeClass);
+ options.removeClass = null;
+ }
+ if (options.to) {
+ element.css(options.to);
+ options.to = null;
+ }
+ }
+ };
+ }];
+};
/* global stripHash: true */
@@ -5432,7 +5853,7 @@ function Browser(window, document, $log, $sniffer) {
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
- reloadLocation = null;
+ pendingLocation = null;
cacheState();
lastHistoryState = cachedState;
@@ -5492,8 +5913,8 @@ function Browser(window, document, $log, $sniffer) {
// Do the assignment again so that those two variables are referentially identical.
lastHistoryState = cachedState;
} else {
- if (!sameBase || reloadLocation) {
- reloadLocation = url;
+ if (!sameBase || pendingLocation) {
+ pendingLocation = url;
}
if (replace) {
location.replace(url);
@@ -5502,14 +5923,18 @@ function Browser(window, document, $log, $sniffer) {
} else {
location.hash = getHash(url);
}
+ if (location.href !== url) {
+ pendingLocation = url;
+ }
}
return self;
// getter
} else {
- // - reloadLocation is needed as browsers don't allow to read out
- // the new location.href if a reload happened.
+ // - pendingLocation is needed as browsers don't allow to read out
+ // the new location.href if a reload happened or if there is a bug like in iOS 9 (see
+ // https://openradar.appspot.com/22186109).
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return reloadLocation || location.href.replace(/%27/g,"'");
+ return pendingLocation || location.href.replace(/%27/g,"'");
}
};
@@ -5531,6 +5956,7 @@ function Browser(window, document, $log, $sniffer) {
urlChangeInit = false;
function cacheStateAndFireUrlChange() {
+ pendingLocation = null;
cacheState();
fireUrlChange();
}
@@ -5766,10 +6192,10 @@ function $BrowserProvider() {
$scope.keys = [];
$scope.cache = $cacheFactory('cacheId');
$scope.put = function(key, value) {
- if ($scope.cache.get(key) === undefined) {
+ if (angular.isUndefined($scope.cache.get(key))) {
$scope.keys.push(key);
}
- $scope.cache.put(key, value === undefined ? null : value);
+ $scope.cache.put(key, angular.isUndefined(value) ? null : value);
};
}]);
@@ -5792,9 +6218,9 @@ function $CacheFactoryProvider() {
var size = 0,
stats = extend({}, options, {id: cacheId}),
- data = {},
+ data = createMap(),
capacity = (options && options.capacity) || Number.MAX_VALUE,
- lruHash = {},
+ lruHash = createMap(),
freshEnd = null,
staleEnd = null;
@@ -5922,6 +6348,8 @@ function $CacheFactoryProvider() {
delete lruHash[key];
}
+ if (!(key in data)) return;
+
delete data[key];
size--;
},
@@ -5936,9 +6364,9 @@ function $CacheFactoryProvider() {
* Clears the cache object of any entries.
*/
removeAll: function() {
- data = {};
+ data = createMap();
size = 0;
- lruHash = {};
+ lruHash = createMap();
freshEnd = staleEnd = null;
},
@@ -6245,18 +6673,24 @@ function $TemplateCacheProvider() {
* and other directives used in the directive's template will also be excluded from execution.
*
* #### `scope`
- * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the
- * same element request a new scope, only one new scope is created. The new scope rule does not
- * apply for the root of the template since the root of the template always gets a new scope.
+ * The scope property can be `true`, an object or a falsy value:
*
- * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from
- * normal scope in that it does not prototypically inherit from the parent scope. This is useful
- * when creating reusable components, which should not accidentally read or modify data in the
- * parent scope.
+ * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
*
- * The 'isolate' scope takes an object hash which defines a set of local scope properties
- * derived from the parent scope. These local properties are useful for aliasing values for
- * templates. Locals definition is a hash of local scope property to its source:
+ * * **`true`:** A new child scope that prototypically inherits from its parent will be created for
+ * the directive's element. If multiple directives on the same element request a new scope,
+ * only one new scope is created. The new scope rule does not apply for the root of the template
+ * since the root of the template always gets a new scope.
+ *
+ * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
+ * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
+ * scope. This is useful when creating reusable components, which should not accidentally read or modify
+ * data in the parent scope.
+ *
+ * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the
+ * directive's element. These local properties are useful for aliasing values for templates. The keys in
+ * the object hash map to the name of the property on the isolate scope; the values define how the property
+ * is bound to the parent scope, via matching attributes on the directive's element:
*
* * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
* always a string since DOM attributes are strings. If no `attr` name is specified then the
@@ -6289,6 +6723,20 @@ function $TemplateCacheProvider() {
* For example, if the expression is `increment(amount)` then we can specify the amount value
* by calling the `localFn` as `localFn({amount: 22})`.
*
+ * In general it's possible to apply more than one directive to one element, but there might be limitations
+ * depending on the type of scope required by the directives. The following points will help explain these limitations.
+ * For simplicity only two directives are taken into account, but it is also applicable for several directives:
+ *
+ * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope
+ * * **child scope** + **no scope** => Both directives will share one single child scope
+ * * **child scope** + **child scope** => Both directives will share one single child scope
+ * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use
+ * its parent's scope
+ * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot
+ * be applied to the same element.
+ * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives
+ * cannot be applied to the same element.
+ *
*
* #### `bindToController`
* When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will
@@ -6297,7 +6745,7 @@ function $TemplateCacheProvider() {
*
* #### `controller`
* Controller constructor function. The controller is instantiated before the
- * pre-linking phase and it is shared with other directives (see
+ * pre-linking phase and can be accessed by other directives (see
* `require` attribute). This allows the directives to communicate with each other and augment
* each other's behavior. The controller is injectable (and supports bracket notation) with the following locals:
*
@@ -6305,10 +6753,10 @@ function $TemplateCacheProvider() {
* * `$element` - Current element
* * `$attrs` - Current attributes object for the element
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
- * `function([scope], cloneLinkingFn, futureParentElement)`.
- * * `scope`: optional argument to override the scope.
- * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content.
- * * `futureParentElement`:
+ * `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
+ * * `scope`: (optional) override the scope.
+ * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
+ * * `futureParentElement` (optional):
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
@@ -6316,7 +6764,10 @@ function $TemplateCacheProvider() {
* as those elements need to created and cloned in a special way when they are defined outside their
* usual containers (e.g. like `
*
+ * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
+ * directive's element, the entire element or multiple parts of the element contents:
+ *
+ * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
+ * * `'element'` - transclude the whole of the directive's element including any directives on this
+ * element that defined at a lower priority than this directive. When used, the `template`
+ * property is ignored.
+ * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
+ *
+ * **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
+ *
+ * This object is a map where the keys are the name of the slot to fill and the value is an element selector
+ * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`)
+ * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc).
+ *
+ * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
+ *
+ * If the element selector is prefixed with a `?` then that slot is optional.
+ *
+ * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `` elements to
+ * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
+ *
+ * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
+ * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
+ * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
+ * injectable into the directive's controller.
+ *
+ *
* #### Transclusion Functions
*
* When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
@@ -7067,6 +7539,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
+ var SIMPLE_ATTR_NAME = /^\w/;
+ var specialAttrHolder = document.createElement('div');
var Attributes = function(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
@@ -7176,7 +7650,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
- aliasedKey = getAliasedAttrName(node, key),
+ aliasedKey = getAliasedAttrName(key),
observer = key,
nodeName;
@@ -7202,7 +7676,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
- if ((nodeName === 'a' && key === 'href') ||
+ if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
(nodeName === 'img' && key === 'src')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
@@ -7243,10 +7717,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (writeAttr !== false) {
- if (value === null || value === undefined) {
+ if (value === null || isUndefined(value)) {
this.$$element.removeAttr(attrName);
} else {
- this.$$element.attr(attrName, value);
+ if (SIMPLE_ATTR_NAME.test(attrName)) {
+ this.$$element.attr(attrName, value);
+ } else {
+ setSpecialAttr(this.$$element[0], attrName, value);
+ }
}
}
@@ -7287,7 +7765,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
listeners.push(fn);
$rootScope.$evalAsync(function() {
- if (!listeners.$$inter && attrs.hasOwnProperty(key)) {
+ if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) {
// no one registered attribute interpolation function, so lets call it manually
fn(attrs[key]);
}
@@ -7299,6 +7777,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 {
@@ -7318,6 +7808,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
+ var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
var bindings = $element.data('$binding') || [];
@@ -7358,7 +7849,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// We can not compile top level text elements since text nodes can be merged and we will
// not be able to attach scope data to them, so we will wrap them in
forEach($compileNodes, function(node, index) {
- if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) {
+ if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */) {
$compileNodes[index] = jqLite(node).wrap('').parent()[0];
}
});
@@ -7370,6 +7861,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function publicLinkFn(scope, cloneConnectFn, options) {
assertArg(scope, 'scope');
+ if (previousCompileContext && previousCompileContext.needsNewScope) {
+ // A parent directive did a replace and a directive on this element asked
+ // for transclusion, which caused us to lose a layer of element on which
+ // we could hold the new transclusion scope, so we will create it manually
+ // here.
+ scope = scope.$parent.$new();
+ }
+
options = options || {};
var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
transcludeControllers = options.transcludeControllers,
@@ -7515,11 +8014,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (nodeLinkFn.scope) {
childScope = scope.$new();
compile.$$addScopeInfo(jqLite(node), childScope);
- var destroyBindings = nodeLinkFn.$$destroyBindings;
- if (destroyBindings) {
- nodeLinkFn.$$destroyBindings = null;
- childScope.$on('$destroyed', destroyBindings);
- }
} else {
childScope = scope;
}
@@ -7538,8 +8032,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = null;
}
- nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
- nodeLinkFn);
+ nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
@@ -7564,6 +8057,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
};
+ // 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;
}
@@ -7608,13 +8112,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
}
- var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
- if (directiveIsMultiElement(directiveNName)) {
- if (ngAttrName === directiveNName + 'Start') {
- attrStartName = name;
- attrEndName = name.substr(0, name.length - 5) + 'end';
- name = name.substr(0, name.length - 6);
- }
+ var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
+ if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
+ attrStartName = name;
+ attrEndName = name.substr(0, name.length - 5) + 'end';
+ name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
@@ -7724,6 +8226,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
};
}
+ /**
+ * 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) {
+ if (eager) {
+ return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+ }
+
+ var compiled;
+
+ return function() {
+ 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
@@ -7768,6 +8301,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = originalReplaceDirective,
childTranscludeFn = transcludeFn,
linkFn,
+ didScanForMultipleTransclusion = false,
+ mightHaveMultipleTransclusionError = false,
directiveValue;
// executes all directives on the current element
@@ -7810,6 +8345,27 @@ 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();
@@ -7839,7 +8395,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
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
@@ -7851,9 +8407,69 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
} else {
+
+ var slots = createMap();
+
$template = jqLite(jqLiteClone(compileNode)).contents();
+
+ if (isObject(directiveValue)) {
+
+ // We have transclusion slots,
+ // collect them up, compile them and store their transclusion functions
+ $template = [];
+
+ 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] || [];
+ slots[slotName].push(node);
+ } else {
+ $template.push(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
+ slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
+ }
+ }
+ }
+
$compileNode.empty(); // clear contents
- childTranscludeFn = compile($template, transcludeFn);
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
+ undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
+ childTranscludeFn.$$slots = slots;
}
}
@@ -7895,8 +8511,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
- if (newIsolateScopeDirective) {
- markDirectivesAsIsolate(templateDirectives);
+ if (newIsolateScopeDirective || newScopeDirective) {
+ // The original directive caused the current element to be replaced but this element
+ // also needs to have a new scope, so we need to tell the template directives
+ // that they would need to get their scope from further up, if they require transclusion
+ markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
}
directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
@@ -8049,10 +8668,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return elementControllers;
}
- function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn,
- thisLinkFn) {
- var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
- attrs;
+ function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
+ var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
+ attrs, removeScopeBindingWatches, removeControllerBindingWatches;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -8062,8 +8680,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrs = new Attributes($element, templateAttrs);
}
+ controllerScope = scope;
if (newIsolateScopeDirective) {
isolateScope = scope.$new(true);
+ } else if (newScopeDirective) {
+ controllerScope = scope.$parent;
}
if (boundTranscludeFn) {
@@ -8071,6 +8692,10 @@ 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) {
@@ -8084,42 +8709,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
- initializeDirectiveBindings(scope, attrs, isolateScope,
- isolateScope.$$isolateBindings,
- newIsolateScopeDirective, isolateScope);
- }
- if (elementControllers) {
- // Initialize bindToController bindings for new/isolate scopes
- var scopeDirective = newIsolateScopeDirective || newScopeDirective;
- var bindings;
- var controllerForBindings;
- if (scopeDirective && elementControllers[scopeDirective.name]) {
- bindings = scopeDirective.$$bindings.bindToController;
- controller = elementControllers[scopeDirective.name];
-
- if (controller && controller.identifier && bindings) {
- controllerForBindings = controller;
- thisLinkFn.$$destroyBindings =
- initializeDirectiveBindings(scope, attrs, controller.instance,
- bindings, scopeDirective);
- }
+ removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
+ isolateScope.$$isolateBindings,
+ newIsolateScopeDirective);
+ if (removeScopeBindingWatches) {
+ isolateScope.$on('$destroy', removeScopeBindingWatches);
}
- for (i in elementControllers) {
- controller = elementControllers[i];
- var controllerResult = controller();
+ }
- if (controllerResult !== controller.instance) {
- // If the controller constructor has a return value, overwrite the instance
- // from setupControllers and update the element data
- controller.instance = controllerResult;
- $element.data('$' + i + 'Controller', controllerResult);
- if (controller === controllerForBindings) {
- // Remove and re-install bindToController bindings
- thisLinkFn.$$destroyBindings();
- thisLinkFn.$$destroyBindings =
- initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective);
- }
- }
+ // Initialize bindToController bindings
+ for (var name in elementControllers) {
+ var controllerDirective = controllerDirectives[name];
+ var controller = elementControllers[name];
+ var bindings = controllerDirective.$$bindings.bindToController;
+
+ if (controller.identifier && bindings) {
+ removeControllerBindingWatches =
+ 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);
}
}
@@ -8158,11 +8775,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// 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;
@@ -8174,15 +8791,36 @@ 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 markDirectivesAsIsolate(directives) {
- // mark all directives as needing isolate scope.
+ // Depending upon the context in which a directive finds itself it might need to have a new isolated
+ // or child scope created. For instance:
+ // * if the directive has been pulled into a template because another directive with a higher priority
+ // asked for element transclusion
+ // * if the directive itself asks for transclusion but it is at the root of a template and the original
+ // element was replaced. See https://github.com/angular/angular.js/issues/12936
+ function markDirectiveScope(directives, isolateScope, newScope) {
for (var j = 0, jj = directives.length; j < jj; j++) {
- directives[j] = inherit(directives[j], {$$isolateScope: true});
+ directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
}
}
@@ -8209,7 +8847,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
i = 0, ii = directives.length; i < ii; i++) {
try {
directive = directives[i];
- if ((maxPriority === undefined || maxPriority > directive.priority) &&
+ if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
directive.restrict.indexOf(location) != -1) {
if (startAttrName) {
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
@@ -8329,7 +8967,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
if (isObject(origAsyncDirective.scope)) {
- markDirectivesAsIsolate(templateDirectives);
+ // the original directive that caused the template to be loaded async required
+ // an isolate scope
+ markDirectiveScope(templateDirectives, true);
}
directives = templateDirectives.concat(directives);
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
@@ -8378,7 +9018,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = boundTranscludeFn;
}
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
- childBoundTranscludeFn, afterTemplateNodeLinkFn);
+ childBoundTranscludeFn);
}
linkQueue = null;
});
@@ -8395,8 +9035,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
}
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn,
- afterTemplateNodeLinkFn);
+ afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
}
};
}
@@ -8505,7 +9144,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile: function() {
return {
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
- var $$observers = (attr.$$observers || (attr.$$observers = {}));
+ var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
@@ -8600,41 +9239,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?
+ // 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 = document.createDocumentFragment();
- fragment.appendChild(firstElementToRemove);
+ 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
// data here because there's no public interface in jQuery to do that and copying over
// event listeners (which is the main use of private data) wouldn't work anyway.
- jqLite(newNode).data(jqLite(firstElementToRemove).data());
+ 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;
}
@@ -8656,9 +9287,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// Set up $watches for isolate scope and controller bindings. This process
// only occurs for isolate scopes and new scopes with controllerAs.
- function initializeDirectiveBindings(scope, attrs, destination, bindings,
- directive, newScope) {
- var onNewScopeDestroyed;
+ function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
+ var removeWatchCollection = [];
forEach(bindings, function(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -8666,24 +9296,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
lastValue,
parentGet, parentSet, compare;
- if (!hasOwnProperty.call(attrs, attrName)) {
- // In the case of user defined a binding with the same name as a method in Object.prototype but didn't set
- // the corresponding attribute. We need to make sure subsequent code won't access to the prototype function
- attrs[attrName] = undefined;
- }
-
switch (mode) {
case '@':
- if (!attrs[attrName] && !optional) {
- destination[scopeName] = undefined;
+ if (!optional && !hasOwnProperty.call(attrs, attrName)) {
+ destination[scopeName] = attrs[attrName] = void 0;
}
-
attrs.$observe(attrName, function(value) {
- destination[scopeName] = value;
+ if (isString(value)) {
+ destination[scopeName] = value;
+ }
});
attrs.$$observers[attrName].$$scope = scope;
- if (attrs[attrName]) {
+ if (isString(attrs[attrName])) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
destination[scopeName] = $interpolate(attrs[attrName])(scope);
@@ -8691,11 +9316,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
case '=':
- if (optional && !attrs[attrName]) {
- return;
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
}
- parentGet = $parse(attrs[attrName]);
+ if (optional && !attrs[attrName]) break;
+ parentGet = $parse(attrs[attrName]);
if (parentGet.literal) {
compare = equals;
} else {
@@ -8723,18 +9350,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
- var unwatch;
+ var removeWatch;
if (definition.collection) {
- unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
+ removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
- unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
+ removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
}
- onNewScopeDestroyed = (onNewScopeDestroyed || []);
- onNewScopeDestroyed.push(unwatch);
+ removeWatchCollection.push(removeWatch);
break;
case '&':
- parentGet = $parse(attrs[attrName]);
+ // Don't assign Object.prototype method to scope
+ parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
// Don't assign noop to destination if expression is not valid
if (parentGet === noop && optional) break;
@@ -8745,16 +9372,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
}
});
- var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
- for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
- onNewScopeDestroyed[i]();
+
+ return removeWatchCollection.length && function removeWatches() {
+ for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
+ removeWatchCollection[i]();
}
- } : noop;
- if (newScope && destroyBindings !== noop) {
- newScope.$on('$destroy', destroyBindings);
- return noop;
- }
- return destroyBindings;
+ };
}
}];
}
@@ -9111,6 +9734,29 @@ function $ExceptionHandlerProvider() {
}];
}
+var $$ForceReflowProvider = function() {
+ this.$get = ['$document', function($document) {
+ return function(domNode) {
+ //the line below will force the browser to perform a repaint so
+ //that all the animated elements within the animation frame will
+ //be properly updated and drawn on screen. This is required to
+ //ensure that the preparation animation is properly flushed so that
+ //the active state picks up from there. DO NOT REMOVE THIS LINE.
+ //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
+ //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
+ //WILL TAKE YEARS AWAY FROM YOUR LIFE.
+ if (domNode) {
+ if (!domNode.nodeType && domNode instanceof jqLite) {
+ domNode = domNode[0];
+ }
+ } else {
+ domNode = $document[0].body;
+ }
+ return domNode.offsetWidth + 1;
+ };
+ }];
+};
+
var APPLICATION_JSON = 'application/json';
var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
var JSON_START = /^\[|^\{(?!\{)/;
@@ -9119,6 +9765,12 @@ var JSON_ENDS = {
'{': /}$/
};
var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
+var $httpMinErr = minErr('$http');
+var $httpMinErrLegacyFn = function(method) {
+ return function() {
+ throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method);
+ };
+};
function serializeValue(v) {
if (isObject(v)) {
@@ -9219,8 +9871,8 @@ function $HttpParamSerializerJQLikeProvider() {
function serialize(toSerialize, prefix, topLevel) {
if (toSerialize === null || isUndefined(toSerialize)) return;
if (isArray(toSerialize)) {
- forEach(toSerialize, function(value) {
- serialize(value, prefix + '[]');
+ forEach(toSerialize, function(value, index) {
+ serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
});
} else if (isObject(toSerialize) && !isDate(toSerialize)) {
forEachSorted(toSerialize, function(value, key) {
@@ -9441,6 +10093,30 @@ function $HttpProvider() {
return useApplyAsync;
};
+ var useLegacyPromise = true;
+ /**
+ * @ngdoc method
+ * @name $httpProvider#useLegacyPromiseExtensions
+ * @description
+ *
+ * Configure `$http` service to return promises without the shorthand methods `success` and `error`.
+ * This should be used to make sure that applications work without these methods.
+ *
+ * Defaults to true. If no value is specified, returns the current configured value.
+ *
+ * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
+ *
+ * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
+ * otherwise, returns the current configured value.
+ **/
+ this.useLegacyPromiseExtensions = function(value) {
+ if (isDefined(value)) {
+ useLegacyPromise = !!value;
+ return this;
+ }
+ return useLegacyPromise;
+ };
+
/**
* @ngdoc property
* @name $httpProvider#interceptors
@@ -9506,66 +10182,47 @@ function $HttpProvider() {
*
*
* ## General usage
- * The `$http` service is a function which takes a single argument — a configuration object —
- * that is used to generate an HTTP request and returns a {@link ng.$q promise}
- * with two $http specific methods: `success` and `error`.
+ * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} —
+ * that is used to generate an HTTP request and returns a {@link ng.$q promise}.
*
* ```js
- * // Simple GET request example :
- * $http.get('/someUrl').
- * success(function(data, status, headers, config) {
+ * // Simple GET request example:
+ * $http({
+ * method: 'GET',
+ * url: '/someUrl'
+ * }).then(function successCallback(response) {
* // this callback will be called asynchronously
* // when the response is available
- * }).
- * error(function(data, status, headers, config) {
+ * }, function errorCallback(response) {
* // called asynchronously if an error occurs
* // or server returns response with an error status.
* });
* ```
*
- * ```js
- * // Simple POST request example (passing data) :
- * $http.post('/someUrl', {msg:'hello word!'}).
- * success(function(data, status, headers, config) {
- * // this callback will be called asynchronously
- * // when the response is available
- * }).
- * error(function(data, status, headers, config) {
- * // called asynchronously if an error occurs
- * // or server returns response with an error status.
- * });
- * ```
+ * The response object has these properties:
*
- *
- * Since the returned value of calling the $http function is a `promise`, you can also use
- * the `then` method to register callbacks, and these callbacks will receive a single argument –
- * an object representing the response. See the API signature and type info below for more
- * details.
+ * - **data** – `{string|Object}` – The response body transformed with the transform
+ * functions.
+ * - **status** – `{number}` – HTTP status code of the response.
+ * - **headers** – `{function([headerName])}` – Header getter function.
+ * - **config** – `{Object}` – The configuration object that was used to generate the request.
+ * - **statusText** – `{string}` – HTTP status text of the response.
*
* A response status code between 200 and 299 is considered a success status and
* will result in the success callback being called. Note that if the response is a redirect,
* XMLHttpRequest will transparently follow it, meaning that the error callback will not be
* called for such responses.
*
- * ## Writing Unit Tests that use $http
- * When unit testing (using {@link ngMock ngMock}), it is necessary to call
- * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
- * request using trained responses.
- *
- * ```
- * $httpBackend.expectGET(...);
- * $http.get(...);
- * $httpBackend.flush();
- * ```
*
* ## Shortcut methods
*
* Shortcut methods are also available. All shortcut methods require passing in the URL, and
- * request data must be passed in for POST/PUT requests.
+ * request data must be passed in for POST/PUT requests. An optional config can be passed as the
+ * last argument.
*
* ```js
- * $http.get('/someUrl').success(successCallback);
- * $http.post('/someUrl', data).success(successCallback);
+ * $http.get('/someUrl', config).then(successCallback, errorCallback);
+ * $http.post('/someUrl', data, config).then(successCallback, errorCallback);
* ```
*
* Complete list of shortcut methods:
@@ -9579,6 +10236,25 @@ function $HttpProvider() {
* - {@link ng.$http#patch $http.patch}
*
*
+ * ## Writing Unit Tests that use $http
+ * When unit testing (using {@link ngMock ngMock}), it is necessary to call
+ * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
+ * request using trained responses.
+ *
+ * ```
+ * $httpBackend.expectGET(...);
+ * $http.get(...);
+ * $httpBackend.flush();
+ * ```
+ *
+ * ## Deprecation Notice
+ *
+ * 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
@@ -9602,7 +10278,7 @@ function $HttpProvider() {
*
* ```
* module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'
+ * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
* });
* ```
*
@@ -9622,7 +10298,7 @@ function $HttpProvider() {
* data: { test: 'test' }
* }
*
- * $http(req).success(function(){...}).error(function(){...});
+ * $http(req).then(function(){...}, function(){...});
* ```
*
* ## Transforming Requests and Responses
@@ -9728,7 +10404,7 @@ function $HttpProvider() {
*
* There are two kinds of interceptors (and two kinds of rejection interceptors):
*
- * * `request`: interceptors get called with a http `config` object. The function is free to
+ * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to
* modify the `config` object or create a new one. The function needs to return the `config`
* object directly, or a promise containing the `config` or a new `config` object.
* * `requestError`: interceptor gets called when a previous interceptor threw an error or
@@ -9854,7 +10530,6 @@ function $HttpProvider() {
* 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:
*
@@ -9899,20 +10574,9 @@ function $HttpProvider() {
* - **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} object with the
- * standard `then` method and two http specific methods: `success` and `error`. The `then`
- * method takes two arguments a success and an error callback which will be called with a
- * response object. The `success` and `error` methods take a single argument - a function that
- * will be called when the request succeeds or fails respectively. The arguments passed into
- * these functions are destructured representation of the response object passed into the
- * `then` method. The response object has these properties:
+ * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object
+ * when the request succeeds or fails.
*
- * - **data** – `{string|Object}` – The response body transformed with the transform
- * functions.
- * - **status** – `{number}` – HTTP status code of the response.
- * - **headers** – `{function([headerName])}` – Header getter function.
- * - **config** – `{Object}` – The configuration object that was used to generate the request.
- * - **statusText** – `{string}` – HTTP status text of the response.
*
* @property {Array.
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
@@ -18509,92 +19256,194 @@ function numberFilter($locale) {
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (isObject(number)) return '';
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
- var isNegative = number < 0;
- number = Math.abs(number);
-
- var isInfinity = number === Infinity;
- if (!isInfinity && !isFinite(number)) return '';
-
- var numStr = number + '',
- formatedText = '',
- hasExponent = false,
- parts = [];
-
- if (isInfinity) formatedText = '\u221e';
-
- if (!isInfinity && numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- number = 0;
- } else {
- formatedText = numStr;
- hasExponent = true;
- }
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
}
- if (!isInfinity && !hasExponent) {
- var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
+ // Exponential form?
+ if ((i = numStr.search(/e/i)) > 0) {
+ // Work out the exponent.
+ if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
+ numberOfIntegerDigits += +numStr.slice(i + 1);
+ numStr = numStr.substring(0, i);
+ } else if (numberOfIntegerDigits < 0) {
+ // There was no decimal point or exponent so it is an integer.
+ numberOfIntegerDigits = numStr.length;
+ }
- // determine fractionSize if it is not specified
- if (isUndefined(fractionSize)) {
- fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
- }
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++);
- // safely round numbers in JS without hitting imprecisions of floating-point arithmetics
- // inspired by:
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
- number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);
-
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
-
- var i, pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
-
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (i = 0; i < pos; i++) {
- if ((pos - i) % group === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
- }
-
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i) % lgroup === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
-
- // format fraction part.
- while (fraction.length < fractionSize) {
- fraction += '0';
- }
-
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ if (i == (zeros = numStr.length)) {
+ // The digits are all zero.
+ digits = [0];
+ numberOfIntegerDigits = 1;
} else {
- if (fractionSize > 0 && number < 1) {
- formatedText = number.toFixed(fractionSize);
- number = parseFloat(formatedText);
+ // Count the number of trailing zeros
+ zeros--;
+ while (numStr.charAt(zeros) == ZERO_CHAR) zeros--;
+
+ // Trailing zeros are insignificant so ignore them
+ numberOfIntegerDigits -= i;
+ digits = [];
+ // Convert string to array of digits without leading/trailing zeros.
+ for (j = 0; i <= zeros; i++, j++) {
+ digits[j] = +numStr.charAt(i);
}
}
- if (number === 0) {
- isNegative = false;
+ // If the number overflows the maximum allowed digits then use an exponent.
+ if (numberOfIntegerDigits > MAX_DIGITS) {
+ digits = digits.splice(0, MAX_DIGITS - 1);
+ exponent = numberOfIntegerDigits - 1;
+ numberOfIntegerDigits = 1;
}
- parts.push(isNegative ? pattern.negPre : pattern.posPre,
- formatedText,
- isNegative ? pattern.negSuf : pattern.posSuf);
- return parts.join('');
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
+}
+
+/**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
+
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
+
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
+
+ if (roundAt > 0) {
+ digits.splice(roundAt);
+ } else {
+ // We rounded to zero so reset the parsedNumber
+ parsedNumber.i = 1;
+ digits.length = roundAt = fractionSize + 1;
+ for (var i=0; i < roundAt; i++) digits[i] = 0;
+ }
+
+ if (digit >= 5) digits[roundAt - 1]++;
+
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < fractionSize; fractionLen++) digits.push(0);
+
+
+ // Do any carrying, e.g. a digit was rounded up to 10
+ var carry = digits.reduceRight(function(carry, d, i, digits) {
+ d = d + carry;
+ digits[i] = d % 10;
+ return Math.floor(d / 10);
+ }, 0);
+ if (carry) {
+ digits.unshift(carry);
+ parsedNumber.i++;
+ }
+}
+
+/**
+ * Format a number into a string
+ * @param {number} number The number to format
+ * @param {{
+ * minFrac, // the minimum number of digits required in the fraction part of the number
+ * maxFrac, // the maximum number of digits required in the fraction part of the number
+ * gSize, // number of digits in each group of separated digits
+ * lgSize, // number of digits in the last group of digits before the decimal separator
+ * negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
+ * posPre, // the string to go in front of a positive number
+ * negSuf, // the string to go after a negative number (e.g. `)`)
+ * posSuf // the string to go after a positive number
+ * }} pattern
+ * @param {string} groupSep The string to separate groups of number (e.g. `,`)
+ * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
+ * @param {[type]} fractionSize The size of the fractional part of the number
+ * @return {string} The number formatted as a string
+ */
+function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
+
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
+
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
+ } else {
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
+
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
+ }
+
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length > pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
+ }
+
+ if (exponent) {
+ formattedText += 'e+' + exponent;
+ }
+ }
+ if (number < 0 && !isZero) {
+ return pattern.negPre + formattedText + pattern.negSuf;
+ } else {
+ return pattern.posPre + formattedText + pattern.posSuf;
+ }
}
function padNumber(num, digits, trim) {
@@ -18604,7 +19453,7 @@ function padNumber(num, digits, trim) {
num = -num;
}
num = '' + num;
- while (num.length < digits) num = '0' + num;
+ while (num.length < digits) num = ZERO_CHAR + num;
if (trim) {
num = num.substr(num.length - digits);
}
@@ -19060,7 +19909,7 @@ function limitToFilter() {
if (!isArray(input) && !isString(input)) return input;
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
- begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin;
+ begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
if (limit >= 0) {
return input.slice(begin, begin + limit);
@@ -19083,8 +19932,9 @@ function limitToFilter() {
* Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
* for strings and numerically for numbers. Note: if you notice numbers are not being sorted
* as expected, make sure they are actually being saved as numbers and not strings.
+ * Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported.
*
- * @param {Array} array The array to sort.
+ * @param {Array} array The array (or array-like object) to sort.
* @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
* used by the comparator to determine the order of elements.
*
@@ -19115,17 +19965,6 @@ function limitToFilter() {
* `reverse` is not set, which means it defaults to `false`.
-
@@ -19141,6 +19980,17 @@ function limitToFilter() {
+
+ angular.module('orderByExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends =
+ [{name:'John', phone:'555-1212', age:10},
+ {name:'Mary', phone:'555-9876', age:19},
+ {name:'Mike', phone:'555-4321', age:21},
+ {name:'Adam', phone:'555-5678', age:35},
+ {name:'Julie', phone:'555-8765', age:29}];
+ }]);
+
*
* The predicate and reverse parameters can be controlled dynamically through scope properties,
@@ -19148,49 +19998,24 @@ function limitToFilter() {
* @example
-
-
@@ -19239,24 +20098,42 @@ function limitToFilter() {
{ name: 'Adam', phone: '555-5678', age: 35 },
{ name: 'Julie', phone: '555-8765', age: 29 }
];
- $scope.order = function(predicate, reverse) {
- $scope.friends = orderBy($scope.friends, predicate, reverse);
+ $scope.order = function(predicate) {
+ $scope.predicate = predicate;
+ $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
+ $scope.friends = orderBy($scope.friends, predicate, $scope.reverse);
};
- $scope.order('-age',false);
+ $scope.order('age', true);
}]);
+
+
+ .sortorder:after {
+ content: '\25b2';
+ }
+ .sortorder.reverse:after {
+ content: '\25bc';
+ }
+
*/
orderByFilter.$inject = ['$parse'];
function orderByFilter($parse) {
return function(array, sortPredicate, reverseOrder) {
- 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});
// The next three lines are a version of a Swartzian Transform idiom from Perl
// (sometimes called the Decorate-Sort-Undecorate idiom)
@@ -19878,6 +20755,7 @@ function nullFormRenameControl(control, name) {
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
+ * @property {boolean} $pending True if at least one containing control or form is pending.
* @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
* @property {Object} $error Is an object hash, containing references to controls or
@@ -19917,8 +20795,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
controls = [];
- var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
-
// init state
form.$error = {};
form.$$success = {};
@@ -19929,8 +20805,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$valid = true;
form.$invalid = false;
form.$submitted = false;
-
- parentForm.$addControl(form);
+ form.$$parentForm = nullFormCtrl;
/**
* @ngdoc method
@@ -19969,11 +20844,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
/**
* @ngdoc method
* @name form.FormController#$addControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
- * Register a control with the form.
+ * Register a control with the form. Input elements using ngModelController do this automatically
+ * when they are linked.
*
- * Input elements using ngModelController do this automatically when they are linked.
+ * Note that the current state of the control will not be reflected on the new parent form. This
+ * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine`
+ * state.
+ *
+ * However, if the method is used programmatically, for example by adding dynamically created controls,
+ * or controls that have been previously removed without destroying their corresponding DOM element,
+ * it's the developers responsiblity to make sure the current state propagates to the parent form.
+ *
+ * For example, if an input control is added that is already `$dirty` and has `$error` properties,
+ * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
form.$addControl = function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
@@ -19984,6 +20871,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
if (control.$name) {
form[control.$name] = control;
}
+
+ control.$$parentForm = form;
};
// Private API: rename a form control
@@ -20000,11 +20889,18 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
/**
* @ngdoc method
* @name form.FormController#$removeControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
* Deregister a control from the form.
*
* Input elements using ngModelController do this automatically when they are destroyed.
+ *
+ * Note that only the removed control's validation state (`$errors`etc.) will be removed from the
+ * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be
+ * different from case to case. For example, removing the only `$dirty` control from a form may or
+ * may not mean that the form is still `$dirty`.
*/
form.$removeControl = function(control) {
if (control.$name && form[control.$name] === control) {
@@ -20021,6 +20917,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
});
arrayRemove(controls, control);
+ control.$$parentForm = nullFormCtrl;
};
@@ -20057,7 +20954,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
delete object[property];
}
},
- parentForm: parentForm,
$animate: $animate
});
@@ -20076,7 +20972,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
$animate.addClass(element, DIRTY_CLASS);
form.$dirty = true;
form.$pristine = false;
- parentForm.$setDirty();
+ form.$$parentForm.$setDirty();
};
/**
@@ -20132,7 +21028,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$setSubmitted = function() {
$animate.addClass(element, SUBMITTED_CLASS);
form.$submitted = true;
- parentForm.$setSubmitted();
+ form.$$parentForm.$setSubmitted();
};
}
@@ -20182,6 +21078,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* # CSS classes
* - `ng-valid` is set if the form is valid.
* - `ng-invalid` is set if the form is invalid.
+ * - `ng-pending` is set if the form is pending.
* - `ng-pristine` is set if the form is pristine.
* - `ng-dirty` is set if the form is dirty.
* - `ng-submitted` is set if the form was submitted.
@@ -20257,7 +21154,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
+ *