summaryrefslogtreecommitdiff
path: root/xstatic/pkg/angular/data/angular.js
diff options
context:
space:
mode:
Diffstat (limited to 'xstatic/pkg/angular/data/angular.js')
-rw-r--r--xstatic/pkg/angular/data/angular.js4295
1 files changed, 3122 insertions, 1173 deletions
diff --git a/xstatic/pkg/angular/data/angular.js b/xstatic/pkg/angular/data/angular.js
index ea563c0..54f6558 100644
--- a/xstatic/pkg/angular/data/angular.js
+++ b/xstatic/pkg/angular/data/angular.js
@@ -1,9 +1,9 @@
/**
- * @license AngularJS v1.4.10
- * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.5.8
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
-(function(window, document, undefined) {'use strict';
+(function(window) {'use strict';
/**
* @description
@@ -57,7 +57,7 @@ function minErr(module, ErrorConstructor) {
return match;
});
- message += '\nhttp://errors.angularjs.org/1.4.10/' +
+ message += '\nhttp://errors.angularjs.org/1.5.8/' +
(module ? module + '/' : '') + code;
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
@@ -126,7 +126,6 @@ function minErr(module, ErrorConstructor) {
includes: true,
arrayRemove: true,
copy: true,
- shallowCopy: true,
equals: true,
csp: true,
jq: true,
@@ -171,6 +170,7 @@ function minErr(module, ErrorConstructor) {
* @ngdoc module
* @name ng
* @module ng
+ * @installation
* @description
*
* # ng (core module)
@@ -188,29 +188,9 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
-/**
- * @ngdoc function
- * @name angular.lowercase
- * @module ng
- * @kind function
- *
- * @description Converts the specified string to lowercase.
- * @param {string} string String to be converted to lowercase.
- * @returns {string} Lowercased string.
- */
-var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
var hasOwnProperty = Object.prototype.hasOwnProperty;
-/**
- * @ngdoc function
- * @name angular.uppercase
- * @module ng
- * @kind function
- *
- * @description Converts the specified string to uppercase.
- * @param {string} string String to be converted to uppercase.
- * @returns {string} Uppercased string.
- */
+var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
@@ -230,7 +210,7 @@ var manualUppercase = function(s) {
// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods
-// with correct but slower alternatives.
+// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387
if ('i' !== 'I'.toLowerCase()) {
lowercase = manualLowercase;
uppercase = manualUppercase;
@@ -257,7 +237,7 @@ var
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
*/
-msie = document.documentMode;
+msie = window.document.documentMode;
/**
@@ -273,7 +253,7 @@ function isArrayLike(obj) {
// arrays, strings and jQuery/jqLite objects are array like
// * jqLite is either the jQuery or jqLite constructor function
- // * we have to check the existance of jqLite first as this method is called
+ // * we have to check the existence of jqLite first as this method is called
// via the forEach method when constructing the jqLite object in the first place
if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true;
@@ -535,18 +515,28 @@ noop.$inject = [];
* functional style.
*
```js
- function transformer(transformationFn, value) {
- return (transformationFn || angular.identity)(value);
- };
+ function transformer(transformationFn, value) {
+ return (transformationFn || angular.identity)(value);
+ };
+
+ // E.g.
+ function getResult(fn, input) {
+ return (fn || angular.identity)(input);
+ };
+
+ getResult(function(n) { return n * 2; }, 21); // returns 42
+ getResult(null, 21); // returns 21
+ getResult(undefined, 21); // returns 21
```
- * @param {*} value to be returned.
- * @returns {*} the value passed in.
+ *
+ * @param {*} value to be returned.
+ * @returns {*} the value passed in.
*/
function identity($) {return $;}
identity.$inject = [];
-function valueFn(value) {return function() {return value;};}
+function valueFn(value) {return function valueRef() {return value;};}
function hasCustomToString(obj) {
return isFunction(obj.toString) && obj.toString !== toString;
@@ -753,6 +743,10 @@ function isTypedArray(value) {
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
}
+function isArrayBuffer(obj) {
+ return toString.call(obj) === '[object ArrayBuffer]';
+}
+
var trim = function(value) {
return isString(value) ? value.trim() : value;
@@ -781,8 +775,8 @@ var escapeForRegexp = function(s) {
*/
function isElement(node) {
return !!(node &&
- (node.nodeName // we are a direct element
- || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API
+ (node.nodeName // We are a direct element.
+ || (node.prop && node.attr && node.find))); // We have an on and find method part of jQuery API.
}
/**
@@ -827,7 +821,13 @@ function arrayRemove(array, value) {
* * If a destination is provided, all of its elements (for arrays) or properties (for objects)
* are deleted and then all elements/properties from the source are copied to it.
* * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned.
- * * If `source` is identical to 'destination' an exception will be thrown.
+ * * If `source` is identical to `destination` an exception will be thrown.
+ *
+ * <br />
+ * <div class="alert alert-warning">
+ * Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
+ * and on `destination`) will be ignored.
+ * </div>
*
* @param {*} source The source that will be used to make a copy.
* Can be any type, including primitives, `null`, and `undefined`.
@@ -836,48 +836,49 @@ function arrayRemove(array, value) {
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
- <example module="copyExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form novalidate class="simple-form">
- Name: <input type="text" ng-model="user.name" /><br />
- E-mail: <input type="email" ng-model="user.email" /><br />
- Gender: <input type="radio" ng-model="user.gender" value="male" />male
- <input type="radio" ng-model="user.gender" value="female" />female<br />
- <button ng-click="reset()">RESET</button>
- <button ng-click="update(user)">SAVE</button>
- </form>
- <pre>form = {{user | json}}</pre>
- <pre>master = {{master | json}}</pre>
- </div>
-
- <script>
- angular.module('copyExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.master= {};
-
- $scope.update = function(user) {
- // Example with 1 argument
- $scope.master= angular.copy(user);
- };
+ <example module="copyExample">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <form novalidate class="simple-form">
+ <label>Name: <input type="text" ng-model="user.name" /></label><br />
+ <label>Age: <input type="number" ng-model="user.age" /></label><br />
+ Gender: <label><input type="radio" ng-model="user.gender" value="male" />male</label>
+ <label><input type="radio" ng-model="user.gender" value="female" />female</label><br />
+ <button ng-click="reset()">RESET</button>
+ <button ng-click="update(user)">SAVE</button>
+ </form>
+ <pre>form = {{user | json}}</pre>
+ <pre>master = {{master | json}}</pre>
+ </div>
+ </file>
+ <file name="script.js">
+ // Module: copyExample
+ angular.
+ module('copyExample', []).
+ controller('ExampleController', ['$scope', function($scope) {
+ $scope.master = {};
+
+ $scope.reset = function() {
+ // Example with 1 argument
+ $scope.user = angular.copy($scope.master);
+ };
- $scope.reset = function() {
- // Example with 2 arguments
- angular.copy($scope.master, $scope.user);
- };
+ $scope.update = function(user) {
+ // Example with 2 arguments
+ angular.copy(user, $scope.master);
+ };
- $scope.reset();
- }]);
- </script>
- </file>
- </example>
+ $scope.reset();
+ }]);
+ </file>
+ </example>
*/
function copy(source, destination) {
var stackSource = [];
var stackDest = [];
if (destination) {
- if (isTypedArray(destination)) {
+ if (isTypedArray(destination) || isArrayBuffer(destination)) {
throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
}
if (source === destination) {
@@ -904,7 +905,7 @@ function copy(source, destination) {
function copyRecurse(source, destination) {
var h = destination.$$hashKey;
- var result, key;
+ var key;
if (isArray(source)) {
for (var i = 0, ii = source.length; i < ii; i++) {
destination.push(copyElement(source[i]));
@@ -951,24 +952,10 @@ function copy(source, destination) {
}
var needsRecurse = false;
- var destination;
+ var destination = copyType(source);
- if (isArray(source)) {
- destination = [];
- needsRecurse = true;
- } else if (isTypedArray(source)) {
- destination = new source.constructor(source);
- } else if (isDate(source)) {
- destination = new Date(source.getTime());
- } else if (isRegExp(source)) {
- destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
- destination.lastIndex = source.lastIndex;
- } else if (isBlob(source)) {
- destination = new source.constructor([source], {type: source.type});
- } else if (isFunction(source.cloneNode)) {
- destination = source.cloneNode(true);
- } else {
- destination = Object.create(getPrototypeOf(source));
+ if (destination === undefined) {
+ destination = isArray(source) ? [] : Object.create(getPrototypeOf(source));
needsRecurse = true;
}
@@ -979,31 +966,48 @@ function copy(source, destination) {
? copyRecurse(source, destination)
: destination;
}
-}
-
-/**
- * Creates a shallow copy of an object, an array or a primitive.
- *
- * Assumes that there are no proto properties for objects.
- */
-function shallowCopy(src, dst) {
- if (isArray(src)) {
- dst = dst || [];
- for (var i = 0, ii = src.length; i < ii; i++) {
- dst[i] = src[i];
- }
- } else if (isObject(src)) {
- dst = dst || {};
-
- for (var key in src) {
- if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
- dst[key] = src[key];
- }
+ function copyType(source) {
+ switch (toString.call(source)) {
+ case '[object Int8Array]':
+ case '[object Int16Array]':
+ case '[object Int32Array]':
+ case '[object Float32Array]':
+ case '[object Float64Array]':
+ case '[object Uint8Array]':
+ case '[object Uint8ClampedArray]':
+ case '[object Uint16Array]':
+ case '[object Uint32Array]':
+ return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length);
+
+ case '[object ArrayBuffer]':
+ //Support: IE10
+ if (!source.slice) {
+ var copied = new ArrayBuffer(source.byteLength);
+ new Uint8Array(copied).set(new Uint8Array(source));
+ return copied;
+ }
+ return source.slice(0);
+
+ case '[object Boolean]':
+ case '[object Number]':
+ case '[object String]':
+ case '[object Date]':
+ return new source.constructor(source.valueOf());
+
+ case '[object RegExp]':
+ var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
+ re.lastIndex = source.lastIndex;
+ return re;
+
+ case '[object Blob]':
+ return new source.constructor([source], {type: source.type});
+ }
+
+ if (isFunction(source.cloneNode)) {
+ return source.cloneNode(true);
}
}
-
- return dst || src;
}
@@ -1035,44 +1039,78 @@ function shallowCopy(src, dst) {
* @param {*} o1 Object or value to compare.
* @param {*} o2 Object or value to compare.
* @returns {boolean} True if arguments are equal.
+ *
+ * @example
+ <example module="equalsExample" name="equalsExample">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <form novalidate>
+ <h3>User 1</h3>
+ Name: <input type="text" ng-model="user1.name">
+ Age: <input type="number" ng-model="user1.age">
+
+ <h3>User 2</h3>
+ Name: <input type="text" ng-model="user2.name">
+ Age: <input type="number" ng-model="user2.age">
+
+ <div>
+ <br/>
+ <input type="button" value="Compare" ng-click="compare()">
+ </div>
+ User 1: <pre>{{user1 | json}}</pre>
+ User 2: <pre>{{user2 | json}}</pre>
+ Equal: <pre>{{result}}</pre>
+ </form>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) {
+ $scope.user1 = {};
+ $scope.user2 = {};
+ $scope.result;
+ $scope.compare = function() {
+ $scope.result = angular.equals($scope.user1, $scope.user2);
+ };
+ }]);
+ </file>
+ </example>
*/
function equals(o1, o2) {
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
- if (t1 == t2) {
- if (t1 == 'object') {
- if (isArray(o1)) {
- if (!isArray(o2)) return false;
- if ((length = o1.length) == o2.length) {
- for (key = 0; key < length; key++) {
- if (!equals(o1[key], o2[key])) return false;
- }
- return true;
- }
- } else if (isDate(o1)) {
- if (!isDate(o2)) return false;
- return equals(o1.getTime(), o2.getTime());
- } else if (isRegExp(o1)) {
- return isRegExp(o2) ? o1.toString() == o2.toString() : false;
- } else {
- if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
- isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
- keySet = createMap();
- for (key in o1) {
- if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (t1 == t2 && t1 == 'object') {
+ if (isArray(o1)) {
+ if (!isArray(o2)) return false;
+ if ((length = o1.length) == o2.length) {
+ for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
- keySet[key] = true;
- }
- for (key in o2) {
- if (!(key in keySet) &&
- key.charAt(0) !== '$' &&
- isDefined(o2[key]) &&
- !isFunction(o2[key])) return false;
}
return true;
}
+ } else if (isDate(o1)) {
+ if (!isDate(o2)) return false;
+ return equals(o1.getTime(), o2.getTime());
+ } else if (isRegExp(o1)) {
+ if (!isRegExp(o2)) return false;
+ return o1.toString() == o2.toString();
+ } else {
+ if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
+ isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
+ keySet = createMap();
+ for (key in o1) {
+ if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (!equals(o1[key], o2[key])) return false;
+ keySet[key] = true;
+ }
+ for (key in o2) {
+ if (!(key in keySet) &&
+ key.charAt(0) !== '$' &&
+ isDefined(o2[key]) &&
+ !isFunction(o2[key])) return false;
+ }
+ return true;
}
}
return false;
@@ -1082,8 +1120,8 @@ var csp = function() {
if (!isDefined(csp.rules)) {
- var ngCspElement = (document.querySelector('[ng-csp]') ||
- document.querySelector('[data-ng-csp]'));
+ var ngCspElement = (window.document.querySelector('[ng-csp]') ||
+ window.document.querySelector('[data-ng-csp]'));
if (ngCspElement) {
var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
@@ -1158,7 +1196,7 @@ var jq = function() {
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
- if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
name = el.getAttribute(prefix + 'jq');
break;
}
@@ -1210,7 +1248,7 @@ function bind(self, fn) {
: fn.call(self);
};
} else {
- // in IE, native methods are not functions so they cannot be bound (note: they don't need to be)
+ // In IE, native methods are not functions so they cannot be bound (note: they don't need to be).
return fn;
}
}
@@ -1223,7 +1261,7 @@ function toJsonReplacer(key, value) {
val = undefined;
} else if (isWindow(value)) {
val = '$WINDOW';
- } else if (value && document === value) {
+ } else if (value && window.document === value) {
val = '$DOCUMENT';
} else if (isScope(value)) {
val = '$SCOPE';
@@ -1247,6 +1285,27 @@ function toJsonReplacer(key, value) {
* @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
* If set to an integer, the JSON output will contain that many spaces per indentation.
* @returns {string|undefined} JSON-ified string representing `obj`.
+ * @knownIssue
+ *
+ * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date`
+ * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the
+ * `Date.prototype.toJSON` method as follows:
+ *
+ * ```
+ * var _DatetoJSON = Date.prototype.toJSON;
+ * Date.prototype.toJSON = function() {
+ * try {
+ * return _DatetoJSON.call(this);
+ * } catch(e) {
+ * if (e instanceof RangeError) {
+ * return null;
+ * }
+ * throw e;
+ * }
+ * };
+ * ```
+ *
+ * See https://github.com/angular/angular.js/pull/14221 for more information.
*/
function toJson(obj, pretty) {
if (isUndefined(obj)) return undefined;
@@ -1337,7 +1396,7 @@ function tryDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch (e) {
- // Ignore any invalid uri component
+ // Ignore any invalid uri component.
}
}
@@ -1463,10 +1522,17 @@ function getNgAttribute(element, ngAttr) {
* designates the **root element** of the application and is typically placed near the root element
* of the page - e.g. on the `<body>` or `<html>` tags.
*
- * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
- * found in the document will be used to define the root element to auto-bootstrap as an
- * application. To run multiple applications in an HTML document you must manually bootstrap them using
- * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other.
+ * There are a few things to keep in mind when using `ngApp`:
+ * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
+ * found in the document will be used to define the root element to auto-bootstrap as an
+ * application. To run multiple applications in an HTML document you must manually bootstrap them using
+ * {@link angular.bootstrap} instead.
+ * - AngularJS applications cannot be nested within each other.
+ * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`.
+ * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and
+ * {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
*
* You can specify an **AngularJS module** to be used as the root module for the application. This
* module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It
@@ -1575,7 +1641,7 @@ function angularInit(element, bootstrap) {
module,
config = {};
- // The element `element` has priority over any other element
+ // The element `element` has priority over any other element.
forEach(ngAttrPrefixes, function(prefix) {
var name = prefix + 'app';
@@ -1606,16 +1672,25 @@ function angularInit(element, bootstrap) {
* @description
* Use this function to manually start up angular application.
*
- * See: {@link guide/bootstrap Bootstrap}
- *
- * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually.
- * They must use {@link ng.directive:ngApp ngApp}.
+ * For more information, see the {@link guide/bootstrap Bootstrap guide}.
*
* Angular will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
* multiple instances of Angular try to work on the DOM.
*
+ * <div class="alert alert-warning">
+ * **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually.
+ * They must use {@link ng.directive:ngApp ngApp}.
+ * </div>
+ *
+ * <div class="alert alert-warning">
+ * **Note:** Do not bootstrap the app on an element with a directive that uses {@link ng.$compile#transclusion transclusion},
+ * such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
+ * </div>
+ *
* ```html
* <!doctype html>
* <html>
@@ -1659,11 +1734,11 @@ function bootstrap(element, modules, config) {
element = jqLite(element);
if (element.injector()) {
- var tag = (element[0] === document) ? 'document' : startingTag(element);
- //Encode angle brackets to prevent input from being sanitized to empty string #8683
+ var tag = (element[0] === window.document) ? 'document' : startingTag(element);
+ // Encode angle brackets to prevent input from being sanitized to empty string #8683.
throw ngMinErr(
'btstrpd',
- "App Already Bootstrapped with this Element '{0}'",
+ "App already bootstrapped with this element '{0}'",
tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
}
@@ -1758,7 +1833,6 @@ function snake_case(name, separator) {
}
var bindJQueryFired = false;
-var skipDestroyOnNextJQueryCleanData;
function bindJQuery() {
var originalCleanData;
@@ -1792,15 +1866,11 @@ function bindJQuery() {
originalCleanData = jQuery.cleanData;
jQuery.cleanData = function(elems) {
var events;
- if (!skipDestroyOnNextJQueryCleanData) {
- for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
- if (events && events.$destroy) {
- jQuery(elem).triggerHandler('$destroy');
- }
+ for (var i = 0, elem; (elem = elems[i]) != null; i++) {
+ events = jQuery._data(elem, "events");
+ if (events && events.$destroy) {
+ jQuery(elem).triggerHandler('$destroy');
}
- } else {
- skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
@@ -2115,9 +2185,9 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#decorator
* @module ng
- * @param {string} The name of the service to decorate.
- * @param {Function} This function will be invoked when the service needs to be
- * instantiated and should return the decorated service instance.
+ * @param {string} name The name of the service to decorate.
+ * @param {Function} decorFn This function will be invoked when the service needs to be
+ * instantiated and should return the decorated service instance.
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
@@ -2202,6 +2272,19 @@ function setupModuleLoader(window) {
/**
* @ngdoc method
+ * @name angular.Module#component
+ * @module ng
+ * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
+ * @param {Object} options Component definition object (a simplified
+ * {@link ng.$compile#directive-definition-object directive definition object})
+ *
+ * @description
+ * See {@link ng.$compileProvider#component $compileProvider.component()}.
+ */
+ component: invokeLaterAndSetModuleName('$compileProvider', 'component'),
+
+ /**
+ * @ngdoc method
* @name angular.Module#config
* @module ng
* @param {Function} configFn Execute this function on module load. Useful for service
@@ -2267,7 +2350,34 @@ function setupModuleLoader(window) {
}
-/* global: toDebugString: true */
+/* global shallowCopy: true */
+
+/**
+ * Creates a shallow copy of an object, an array or a primitive.
+ *
+ * Assumes that there are no proto properties for objects.
+ */
+function shallowCopy(src, dst) {
+ if (isArray(src)) {
+ dst = dst || [];
+
+ for (var i = 0, ii = src.length; i < ii; i++) {
+ dst[i] = src[i];
+ }
+ } else if (isObject(src)) {
+ dst = dst || {};
+
+ for (var key in src) {
+ if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst || src;
+}
+
+/* global toDebugString: true */
function serializeObject(obj) {
var seen = [];
@@ -2358,6 +2468,7 @@ function toDebugString(obj) {
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
+ $DateProvider,
$DocumentProvider,
$ExceptionHandlerProvider,
$FilterProvider,
@@ -2370,6 +2481,7 @@ function toDebugString(obj) {
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
$xhrFactoryProvider,
+ $jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
$ParseProvider,
@@ -2407,11 +2519,11 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.4.10', // all of these placeholder strings will be replaced by grunt's
+ full: '1.5.8', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task
- minor: 4,
- dot: 10,
- codeName: 'benignant-oscillation'
+ minor: 5,
+ dot: 8,
+ codeName: 'arbitrary-fallbacks'
};
@@ -2442,7 +2554,7 @@ function publishExternalAPI(angular) {
'isDate': isDate,
'lowercase': lowercase,
'uppercase': uppercase,
- 'callbacks': {counter: 0},
+ 'callbacks': {$$counter: 0},
'getTestability': getTestability,
'$$minErr': minErr,
'$$csp': csp,
@@ -2531,6 +2643,7 @@ function publishExternalAPI(angular) {
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
$xhrFactory: $xhrFactoryProvider,
+ $jsonpCallbacks: $jsonpCallbacksProvider,
$location: $LocationProvider,
$log: $LogProvider,
$parse: $ParseProvider,
@@ -2607,7 +2720,7 @@ function publishExternalAPI(angular) {
* ## Angular's jqLite
* jqLite provides only the following jQuery methods:
*
- * - [`addClass()`](http://api.jquery.com/addClass/)
+ * - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument
* - [`after()`](http://api.jquery.com/after/)
* - [`append()`](http://api.jquery.com/append/)
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
@@ -2634,12 +2747,12 @@ function publishExternalAPI(angular) {
* - [`ready()`](http://api.jquery.com/ready/)
* - [`remove()`](http://api.jquery.com/remove/)
* - [`removeAttr()`](http://api.jquery.com/removeAttr/)
- * - [`removeClass()`](http://api.jquery.com/removeClass/)
+ * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument
* - [`removeData()`](http://api.jquery.com/removeData/)
* - [`replaceWith()`](http://api.jquery.com/replaceWith/)
* - [`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.
+ * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument
+ * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers
* - [`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/)
@@ -2668,6 +2781,9 @@ function publishExternalAPI(angular) {
* - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top
* parent element is reached.
*
+ * @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See
+ * https://github.com/angular/angular.js/issues/14251 for more information.
+ *
* @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery.
* @returns {Object} jQuery object.
*/
@@ -2750,6 +2866,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(),
@@ -2760,7 +2882,7 @@ function jqLiteBuildFragment(html, context) {
nodes.push(context.createTextNode(html));
} else {
// Convert html into DOM nodes
- tmp = tmp || fragment.appendChild(context.createElement("div"));
+ tmp = fragment.appendChild(context.createElement("div"));
tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
wrap = wrapMap[tag] || wrapMap._default;
tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
@@ -2788,7 +2910,7 @@ function jqLiteBuildFragment(html, context) {
}
function jqLiteParseHTML(html, context) {
- context = context || document;
+ context = context || window.document;
var parsed;
if ((parsed = SINGLE_TAG_REGEXP.exec(html))) {
@@ -2814,7 +2936,7 @@ function jqLiteWrapNode(node, wrapper) {
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
-var jqLiteContains = Node.prototype.contains || function(arg) {
+var jqLiteContains = window.Node.prototype.contains || function(arg) {
// jshint bitwise: false
return !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
@@ -3062,7 +3184,7 @@ function jqLiteRemove(element, keepData) {
function jqLiteDocumentLoaded(action, win) {
win = win || window;
if (win.document.readyState === 'complete') {
- // Force the action to be run async for consistent behaviour
+ // Force the action to be run async for consistent behavior
// from the action's point of view
// i.e. it will definitely not be in a $apply
win.setTimeout(action);
@@ -3086,8 +3208,8 @@ var JQLitePrototype = JQLite.prototype = {
}
// check if document is already loaded
- if (document.readyState === 'complete') {
- setTimeout(trigger);
+ if (window.document.readyState === 'complete') {
+ window.setTimeout(trigger);
} else {
this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
// we can not use jqLite since we are not done loading and jQuery could be loaded later.
@@ -3148,7 +3270,8 @@ function getAliasedAttrName(name) {
forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
- hasData: jqLiteHasData
+ hasData: jqLiteHasData,
+ cleanData: jqLiteCleanData
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -3776,22 +3899,37 @@ var $$HashMapProvider = [function() {
/**
* @ngdoc module
* @name auto
+ * @installation
* @description
*
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
+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 stringifyFn(fn) {
+ // Support: Chrome 50-51 only
+ // Creating a new string by adding `' '` at the end, to hack around some bug in Chrome v50/51
+ // (See https://github.com/angular/angular.js/issues/14487.)
+ // TODO (gkalpak): Remove workaround when Chrome v52 is released
+ return Function.prototype.toString.call(fn) + ' ';
+}
+
+function extractArgs(fn) {
+ var fnText = stringifyFn(fn).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]+/, ' ') + ')';
}
@@ -3800,7 +3938,6 @@ function anonFn(fn) {
function annotate(fn, strictDi, name) {
var $inject,
- fnText,
argDecl,
last;
@@ -3815,8 +3952,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);
@@ -4053,18 +4189,20 @@ function annotate(fn, strictDi, name) {
* these cases the {@link auto.$provide $provide} service has additional helper methods to register
* services without specifying a provider.
*
- * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the
+ * * {@link auto.$provide#provider provider(name, provider)} - registers a **service provider** with the
* {@link auto.$injector $injector}
- * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by
+ * * {@link auto.$provide#constant constant(name, obj)} - registers a value/object that can be accessed by
* providers and services.
- * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by
+ * * {@link auto.$provide#value value(name, obj)} - registers a value/object that can only be accessed by
* services, not providers.
- * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`,
+ * * {@link auto.$provide#factory factory(name, fn)} - registers a service **factory function**
* that will be wrapped in a **service provider** object, whose `$get` property will contain the
* given factory function.
- * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class`
+ * * {@link auto.$provide#service service(name, Fn)} - registers a **constructor function**
* that will be wrapped in a **service provider** object, whose `$get` property will instantiate
* a new object using the given constructor function.
+ * * {@link auto.$provide#decorator decorator(name, decorFn)} - registers a **decorator function** that
+ * will be able to modify or replace the implementation of another service.
*
* See the individual methods for more information and examples.
*/
@@ -4321,18 +4459,20 @@ function annotate(fn, strictDi, name) {
* @name $provide#decorator
* @description
*
- * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator
+ * Register a **decorator function** with the {@link auto.$injector $injector}. A decorator function
* intercepts the creation of a service, allowing it to override or modify the behavior of the
- * service. The object returned by the decorator may be the original service, or a new service
- * object which replaces or wraps and delegates to the original service.
+ * service. The return value of the decorator function may be the original service, or a new service
+ * that replaces (or wraps and delegates to) the original service.
+ *
+ * You can find out more about using decorators in the {@link guide/decorators} guide.
*
* @param {string} name The name of the service to decorate.
* @param {Function|Array.<string|Function>} decorator This function will be invoked when the service needs to be
- * instantiated and should return the decorated service instance. The function is called using
+ * provided and should return the decorated service instance. The function is called using
* the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable.
* Local injection arguments:
*
- * * `$delegate` - The original service instance, which can be monkey patched, configured,
+ * * `$delegate` - The original service instance, which can be replaced, monkey patched, configured,
* decorated or delegated to.
*
* @example
@@ -4371,14 +4511,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;
@@ -4528,48 +4673,67 @@ function createInjector(modulesToLoad, strictDi) {
}
}
- function invoke(fn, self, locals, serviceName) {
- if (typeof locals === 'string') {
- serviceName = locals;
- locals = null;
- }
+ function injectionArgs(fn, locals, serviceName) {
var args = [],
- $inject = createInjector.$$annotate(fn, strictDi, serviceName),
- length, i,
- key;
+ $inject = createInjector.$$annotate(fn, strictDi, serviceName);
- for (i = 0, length = $inject.length; i < length; i++) {
- key = $inject[i];
+ for (var i = 0, length = $inject.length; i < length; i++) {
+ var key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
- args.push(
- locals && locals.hasOwnProperty(key)
- ? locals[key]
- : getService(key, serviceName)
- );
+ args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
+ getService(key, serviceName));
}
+ return args;
+ }
+
+ function isClass(func) {
+ // IE 9-11 do not support classes and IE9 leaks with the code below.
+ if (msie <= 11) {
+ return false;
+ }
+ // Support: Edge 12-13 only
+ // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/
+ return typeof func === 'function'
+ && /^(?:class\b|constructor\()/.test(stringifyFn(func));
+ }
+
+ function invoke(fn, self, locals, serviceName) {
+ if (typeof locals === 'string') {
+ serviceName = locals;
+ locals = null;
+ }
+
+ var args = injectionArgs(fn, locals, serviceName);
if (isArray(fn)) {
- fn = fn[length];
+ fn = fn[fn.length - 1];
}
- // http://jsperf.com/angularjs-invoke-apply-vs-switch
- // #5388
- return fn.apply(self, args);
+ if (!isClass(fn)) {
+ // http://jsperf.com/angularjs-invoke-apply-vs-switch
+ // #5388
+ return fn.apply(self, args);
+ } else {
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(fn, args))();
+ }
}
+
function instantiate(Type, locals, serviceName) {
// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
- // Object creation: http://jsperf.com/create-constructor/2
- var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
- var returnedValue = invoke(Type, instance, locals, serviceName);
-
- return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
+ var ctor = (isArray(Type) ? Type[Type.length - 1] : Type);
+ var args = injectionArgs(Type, locals, serviceName);
+ // Empty object at position 0 is ignored for invocation with `new`, but required.
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(ctor, args))();
}
+
return {
invoke: invoke,
instantiate: instantiate,
@@ -4625,7 +4789,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://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
+ * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-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
@@ -4909,7 +5073,7 @@ function prepareAnimateOptions(options) {
}
var $$CoreAnimateJsProvider = function() {
- this.$get = function() {};
+ this.$get = noop;
};
// this is prefixed with Core since it conflicts with
@@ -5181,15 +5345,20 @@ var $AnimateProvider = ['$provide', function($provide) {
* // remove all the animation event listeners listening for `enter`
* $animate.off('enter');
*
+ * // remove listeners for all animation events from the container element
+ * $animate.off(container);
+ *
* // remove all the animation event listeners listening for `enter` on the given element and its children
* $animate.off('enter', container);
*
- * // remove the event listener function provided by `listenerFn` that is set
- * // to listen for `enter` on the given `element` as well as its children
+ * // remove the event listener function provided by `callback` that is set
+ * // to listen for `enter` on the given `container` as well as its children
* $animate.off('enter', container, callback);
* ```
*
- * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...)
+ * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move,
+ * addClass, removeClass, etc...), or the container element. If it is the element, all other
+ * arguments are ignored.
* @param {DOMElement=} container the container element the event listener was placed on
* @param {Function=} callback the callback function that was registered as the listener
*/
@@ -5270,7 +5439,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} parent the parent element which will append the element as
* a child (so long as the after element is not present)
* @param {DOMElement=} after the sibling element after which the element will be appended
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5296,7 +5471,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} parent the parent element which will append the element as
* a child (so long as the after element is not present)
* @param {DOMElement=} after the sibling element after which the element will be appended
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5317,7 +5498,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* digest once the animation has completed.
*
* @param {DOMElement} element the element which will be removed from the DOM
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5341,7 +5528,13 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5365,7 +5558,13 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5390,7 +5589,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces)
* @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5408,7 +5613,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
* If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take
- * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and
+ * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and
* `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding
* style in `to`, the style in `from` is applied immediately, and no animation is run.
* If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate`
@@ -5430,8 +5635,14 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation.
* @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If
* this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element.
- * (Note that if no animation is detected then this value will not be appplied to the element.)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * (Note that if no animation is detected then this value will not be applied to the element.)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5668,7 +5879,7 @@ var $CoreAnimateCssProvider = function() {
options.from = null;
}
- /* jshint newcap: false*/
+ /* jshint newcap: false */
var closed, runner = new $$AnimateRunner();
return {
start: run,
@@ -5729,7 +5940,6 @@ var $CoreAnimateCssProvider = function() {
*/
function Browser(window, document, $log, $sniffer) {
var self = this,
- rawDocument = document[0],
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
@@ -5792,7 +6002,14 @@ function Browser(window, document, $log, $sniffer) {
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
- pendingLocation = null;
+ pendingLocation = null,
+ getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
+ try {
+ return history.state;
+ } catch (e) {
+ // MSIE can reportedly throw when there is no state (UNCONFIRMED).
+ }
+ };
cacheState();
lastHistoryState = cachedState;
@@ -5852,7 +6069,7 @@ function Browser(window, document, $log, $sniffer) {
// Do the assignment again so that those two variables are referentially identical.
lastHistoryState = cachedState;
} else {
- if (!sameBase || pendingLocation) {
+ if (!sameBase) {
pendingLocation = url;
}
if (replace) {
@@ -5866,6 +6083,9 @@ function Browser(window, document, $log, $sniffer) {
pendingLocation = url;
}
}
+ if (pendingLocation) {
+ pendingLocation = url;
+ }
return self;
// getter
} else {
@@ -5900,14 +6120,6 @@ function Browser(window, document, $log, $sniffer) {
fireUrlChange();
}
- function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
- }
-
// This variable should be used *only* inside the cacheState function.
var lastCachedState = null;
function cacheState() {
@@ -6518,8 +6730,9 @@ function $TemplateCacheProvider() {
* There are many different options for a directive.
*
* The difference resides in the return value of the factory function.
- * You can either return a "Directive Definition Object" (see below) that defines the directive properties,
- * or just the `postLink` function (all other properties will have the default values).
+ * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)}
+ * that defines the directive properties, or just the `postLink` function (all other properties will have
+ * the default values).
*
* <div class="alert alert-success">
* **Best Practice:** It's recommended to use the "directive definition object" form.
@@ -6583,6 +6796,125 @@ function $TemplateCacheProvider() {
* });
* ```
*
+ * ### Life-cycle hooks
+ * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the
+ * directive:
+ * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
+ * had their bindings initialized (and before the pre &amp; post linking functions for the directives on
+ * this element). This is a good place to put initialization code for your controller.
+ * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
+ * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
+ * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
+ * component such as cloning the bound value to prevent accidental mutation of the outer value.
+ * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on
+ * changes. Any actions that you wish to take in response to the changes that you detect must be
+ * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook
+ * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not
+ * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments;
+ * if detecting changes, you must store the previous value(s) for comparison to the current values.
+ * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
+ * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
+ * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
+ * components will have their `$onDestroy()` hook called before child components.
+ * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
+ * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
+ * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
+ * they are waiting for their template to load asynchronously and their own compilation and linking has been
+ * suspended until that occurs.
+ *
+ * #### Comparison with Angular 2 life-cycle hooks
+ * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are
+ * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2:
+ *
+ * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`.
+ * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor.
+ * In Angular 2 you can only define hooks on the prototype of the Component class.
+ * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to
+ * `ngDoCheck` in Angular 2
+ * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be
+ * propagated throughout the application.
+ * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an
+ * error or do nothing depending upon the state of `enableProdMode()`.
+ *
+ * #### Life-cycle hook examples
+ *
+ * This example shows how you can check for mutations to a Date object even though the identity of the object
+ * has not changed.
+ *
+ * <example name="doCheckDateExample" module="do-check-module">
+ * <file name="app.js">
+ * angular.module('do-check-module', [])
+ * .component('app', {
+ * template:
+ * 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' +
+ * 'Date: {{ $ctrl.date }}' +
+ * '<test date="$ctrl.date"></test>',
+ * controller: function() {
+ * this.date = new Date();
+ * this.month = this.date.getMonth();
+ * this.updateDate = function() {
+ * this.date.setMonth(this.month);
+ * };
+ * }
+ * })
+ * .component('test', {
+ * bindings: { date: '<' },
+ * template:
+ * '<pre>{{ $ctrl.log | json }}</pre>',
+ * controller: function() {
+ * var previousValue;
+ * this.log = [];
+ * this.$doCheck = function() {
+ * var currentValue = this.date && this.date.valueOf();
+ * if (previousValue !== currentValue) {
+ * this.log.push('doCheck: date mutated: ' + this.date);
+ * previousValue = currentValue;
+ * }
+ * };
+ * }
+ * });
+ * </file>
+ * <file name="index.html">
+ * <app></app>
+ * </file>
+ * </example>
+ *
+ * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the
+ * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large
+ * arrays or objects can have a negative impact on your application performance)
+ *
+ * <example name="doCheckArrayExample" module="do-check-module">
+ * <file name="index.html">
+ * <div ng-init="items = []">
+ * <button ng-click="items.push(items.length)">Add Item</button>
+ * <button ng-click="items = []">Reset Items</button>
+ * <pre>{{ items }}</pre>
+ * <test items="items"></test>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('do-check-module', [])
+ * .component('test', {
+ * bindings: { items: '<' },
+ * template:
+ * '<pre>{{ $ctrl.log | json }}</pre>',
+ * controller: function() {
+ * this.log = [];
+ *
+ * this.$doCheck = function() {
+ * if (this.items_ref !== this.items) {
+ * this.log.push('doCheck: items changed');
+ * this.items_ref = this.items;
+ * }
+ * if (!angular.equals(this.items_clone, this.items)) {
+ * this.log.push('doCheck: items mutated');
+ * this.items_clone = angular.copy(this.items);
+ * }
+ * };
+ * }
+ * });
+ * </file>
+ * </example>
*
*
* ### Directive Definition Object
@@ -6594,7 +6926,7 @@ function $TemplateCacheProvider() {
* When this property is set to true, the HTML compiler will collect DOM nodes between
* nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
* together as the directive elements. It is recommended that this feature be used on directives
- * which are not strictly behavioural (such as {@link ngClick}), and which
+ * which are not strictly behavioral (such as {@link ngClick}), and which
* do not manipulate or replace child nodes (such as {@link ngInclude}).
*
* #### `priority`
@@ -6656,6 +6988,30 @@ function $TemplateCacheProvider() {
* to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
* `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
*
+ * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
+ * expression passed via the attribute `attr`. The expression is evaluated in the context of the
+ * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
+ * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
+ *
+ * For example, given `<my-component my-attr="parentModel">` and directive definition of
+ * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
+ * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
+ * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
+ * two caveats:
+ * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
+ * sets the same value. That means if your bound value is an object, changes to its properties
+ * in the isolated scope will be reflected in the parent scope (because both reference the same object).
+ * 2. one-way binding watches changes to the **identity** of the parent value. That means the
+ * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
+ * to the value has changed. In most cases, this should not be of concern, but can be important
+ * to know if you one-way bind to an object, and then replace that object in the isolated scope.
+ * If you now change a property of the object in your parent scope, the change will not be
+ * propagated to the isolated scope, because the identity of the object on the parent scope
+ * has not changed. Instead you must assign a new object.
+ *
+ * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
+ * back to the parent. However, it does not make this completely impossible.
+ *
* * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
* no `attr` name is specified then the attribute name is assumed to be the same as the local name.
* Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
@@ -6687,8 +7043,18 @@ function $TemplateCacheProvider() {
* definition: `controller: 'myCtrl as myAlias'`.
*
* When an isolate scope is used for a directive (see above), `bindToController: true` will
- * allow a component to have its properties bound to the controller, rather than to scope. When the controller
- * is instantiated, the initial values of the isolate scope bindings are already available.
+ * allow a component to have its properties bound to the controller, rather than to scope.
+ *
+ * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller
+ * properties. You can access these bindings once they have been initialized by providing a controller method called
+ * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
+ * initialized.
+ *
+ * <div class="alert alert-warning">
+ * **Deprecation warning:** although bindings for non-ES6 class controllers are currently
+ * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
+ * code that relies upon bindings inside a `$onInit` method on the controller, instead.
+ * </div>
*
* It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
* This will set up the scope bindings to the controller directly. Note that `scope` can still be used
@@ -6708,10 +7074,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)
@@ -6719,14 +7085,30 @@ 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 `<svg>`).
* * See also the `directive.templateNamespace` property.
- *
+ * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
+ * then the default translusion is provided.
+ * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
+ * `true` if the specified slot contains content (i.e. one or more DOM nodes).
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
- * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the
- * injected argument will be an array in corresponding order. If no such directive can be
- * found, or if the directive does not have a controller, then an error is raised (unless no link function
- * is specified, in which case error checking is skipped). The name can be prefixed with:
+ * `require` property can be a string, an array or an object:
+ * * a **string** containing the name of the directive to pass to the linking function
+ * * an **array** containing the names of directives to pass to the linking function. The argument passed to the
+ * linking function will be an array of controllers in the same order as the names in the `require` property
+ * * an **object** whose property values are the names of the directives to pass to the linking function. The argument
+ * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
+ * controllers.
+ *
+ * If the `require` property is an object and `bindToController` is truthy, then the required controllers are
+ * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
+ * have been constructed but before `$onInit` is called.
+ * If the name of the required controller is the same as the local name (the key), the name can be
+ * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`.
+ * See the {@link $compileProvider#component} helper for an example of how this can be used.
+ * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
+ * raised (unless no link function is specified and the required controllers are not being bound to the directive
+ * controller, in which case error checking is skipped). The name can be prefixed with:
*
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
@@ -6819,14 +7201,6 @@ function $TemplateCacheProvider() {
* The contents are compiled and provided to the directive as a **transclusion function**. See the
* {@link $compile#transclusion Transclusion} section below.
*
- * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
- * directive's element or the entire element:
- *
- * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
- * * `'element'` - transclude the whole of the directive's element including any directives on this
- * element that defined at a lower priority than this directive. When used, the `template`
- * property is ignored.
- *
*
* #### `compile`
*
@@ -6913,8 +7287,8 @@ function $TemplateCacheProvider() {
* any other controller.
*
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
- * This is the same as the `$transclude`
- * parameter of directive controllers, see there for details.
+ * This is the same as the `$transclude` parameter of directive controllers,
+ * see {@link ng.$compile#-controller- the controller section for details}.
* `function([scope], cloneLinkingFn, futureParentElement)`.
*
* #### Pre-linking function
@@ -6956,6 +7330,34 @@ function $TemplateCacheProvider() {
* Testing Transclusion Directives}.
* </div>
*
+ * 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 `<my-custom-element>` 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
@@ -6976,7 +7378,7 @@ function $TemplateCacheProvider() {
* content and the `scope` is the newly created transclusion scope, to which the clone is bound.
*
* <div class="alert alert-info">
- * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function
+ * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function
* since you then get a fresh clone of the original DOM and also have access to the new transclusion scope.
* </div>
*
@@ -7008,7 +7410,7 @@ function $TemplateCacheProvider() {
* </div>
*
* The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat}
- * automatically destroy their transluded clones as necessary so you do not need to worry about this if
+ * automatically destroy their transcluded clones as necessary so you do not need to worry about this if
* you are simply using {@link ngTransclude} to inject the transclusion into your directive.
*
*
@@ -7224,6 +7626,9 @@ function $TemplateCacheProvider() {
var $compileMinErr = minErr('$compile');
+function UNINITIALIZED_VALUE() {}
+var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
+
/**
* @ngdoc provider
* @name $compileProvider
@@ -7246,9 +7651,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var bindingCache = createMap();
function parseIsolateBindings(scope, directiveName, isController) {
- var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
+ var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/;
- var bindings = {};
+ var bindings = createMap();
forEach(scope, function(definition, scopeName) {
if (definition in bindingCache) {
@@ -7320,15 +7725,29 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function assertValidDirectiveName(name) {
var letter = name.charAt(0);
if (!letter || letter !== lowercase(letter)) {
- throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
+ throw $compileMinErr('baddir', "Directive/Component name '{0}' is invalid. The first character must be a lowercase letter", name);
}
if (name !== name.trim()) {
throw $compileMinErr('baddir',
- "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
+ "Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
name);
}
}
+ function getDirectiveRequire(directive) {
+ var require = directive.require || (directive.controller && directive.name);
+
+ if (!isArray(require) && isObject(require)) {
+ forEach(require, function(value, key) {
+ var match = value.match(REQUIRE_PREFIX_REGEXP);
+ var name = value.substring(match[0].length);
+ if (!name) require[key] = match[0] + key;
+ });
+ }
+
+ return require;
+ }
+
/**
* @ngdoc method
* @name $compileProvider#directive
@@ -7340,11 +7759,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
* will match as <code>ng-bind</code>), or an object map of directives where the keys are the
* names and the values are the factories.
- * @param {Function|Array} directiveFactory An injectable directive factory function. See
- * {@link guide/directive} for more info.
+ * @param {Function|Array} directiveFactory An injectable directive factory function. See the
+ * {@link guide/directive directive guide} and the {@link $compile compile API} for more info.
* @returns {ng.$compileProvider} Self for chaining.
*/
- this.directive = function registerDirective(name, directiveFactory) {
+ this.directive = function registerDirective(name, directiveFactory) {
assertNotHasOwnProperty(name, 'directive');
if (isString(name)) {
assertValidDirectiveName(name);
@@ -7365,7 +7784,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directive.priority = directive.priority || 0;
directive.index = index;
directive.name = directive.name || name;
- directive.require = directive.require || (directive.controller && directive.name);
+ directive.require = getDirectiveRequire(directive);
directive.restrict = directive.restrict || 'EA';
directive.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
@@ -7383,6 +7802,147 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return this;
};
+ /**
+ * @ngdoc method
+ * @name $compileProvider#component
+ * @module ng
+ * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`)
+ * @param {Object} options Component definition object (a simplified
+ * {@link ng.$compile#directive-definition-object directive definition object}),
+ * with the following properties (all optional):
+ *
+ * - `controller` – `{(string|function()=}` – controller constructor function that should be
+ * associated with newly created scope or the name of a {@link ng.$compile#-controller-
+ * registered controller} if passed as a string. An empty `noop` function by default.
+ * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope.
+ * If present, the controller will be published to scope under the `controllerAs` name.
+ * If not present, this will default to be `$ctrl`.
+ * - `template` – `{string=|function()=}` – html template as a string or a function that
+ * returns an html template as a string which should be used as the contents of this component.
+ * Empty string by default.
+ *
+ * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
+ * template that should be used as the contents of this component.
+ *
+ * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties.
+ * Component properties are always bound to the component controller and not to the scope.
+ * See {@link ng.$compile#-bindtocontroller- `bindToController`}.
+ * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled.
+ * Disabled by default.
+ * - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to
+ * this component's controller. The object keys specify the property names under which the required
+ * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}.
+ * - `$...` – additional properties to attach to the directive factory function and the controller
+ * constructor function. (This is used by the component router to annotate)
+ *
+ * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls.
+ * @description
+ * Register a **component definition** with the compiler. This is a shorthand for registering a special
+ * type of directive, which represents a self-contained UI component in your application. Such components
+ * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`).
+ *
+ * Component definitions are very simple and do not require as much configuration as defining general
+ * directives. Component definitions usually consist only of a template and a controller backing it.
+ *
+ * In order to make the definition easier, components enforce best practices like use of `controllerAs`,
+ * `bindToController`. They always have **isolate scope** and are restricted to elements.
+ *
+ * Here are a few examples of how you would usually define components:
+ *
+ * ```js
+ * var myMod = angular.module(...);
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * controller: function() {
+ * this.name = 'shahar';
+ * }
+ * });
+ *
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * bindings: {name: '@'}
+ * });
+ *
+ * myMod.component('myComp', {
+ * templateUrl: 'views/my-comp.html',
+ * controller: 'MyCtrl',
+ * controllerAs: 'ctrl',
+ * bindings: {name: '@'}
+ * });
+ *
+ * ```
+ * For more examples, and an in-depth guide, see the {@link guide/component component guide}.
+ *
+ * <br />
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
+ */
+ this.component = function registerComponent(name, options) {
+ var controller = options.controller || function() {};
+
+ function factory($injector) {
+ function makeInjectable(fn) {
+ if (isFunction(fn) || isArray(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);
+ var ddo = {
+ controller: controller,
+ controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
+ template: makeInjectable(template),
+ templateUrl: makeInjectable(options.templateUrl),
+ transclude: options.transclude,
+ scope: {},
+ bindToController: options.bindings || {},
+ restrict: 'E',
+ require: options.require
+ };
+
+ // Copy annotations (starting with $) over to the DDO
+ forEach(options, function(val, key) {
+ if (key.charAt(0) === '$') ddo[key] = val;
+ });
+
+ return ddo;
+ }
+
+ // TODO(pete) remove the following `forEach` before we release 1.6.0
+ // The component-router@0.2.0 looks for the annotations on the controller constructor
+ // Nothing in Angular looks for annotations on the factory function but we can't remove
+ // it from 1.5.x yet.
+
+ // Copy any annotation properties (starting with $) over to the factory and controller constructor functions
+ // These could be used by libraries such as the new component router
+ forEach(options, function(val, key) {
+ if (key.charAt(0) === '$') {
+ factory[key] = val;
+ // Don't try to copy over annotations to named controller
+ if (isFunction(controller)) controller[key] = val;
+ }
+ });
+
+ factory.$inject = ['$injector'];
+
+ return this.directive(name, factory);
+ };
+
/**
* @ngdoc method
@@ -7474,13 +8034,83 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
+
+ var TTL = 10;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#onChangesTtl
+ * @description
+ *
+ * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
+ * assuming that the model is unstable.
+ *
+ * The current default is 10 iterations.
+ *
+ * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
+ * in several iterations of calls to these hooks. However if an application needs more than the default 10
+ * iterations to stabilize then you should investigate what is causing the model to continuously change during
+ * the `$onChanges` hook execution.
+ *
+ * Increasing the TTL could have performance implications, so you should not change it without proper justification.
+ *
+ * @param {number} limit The number of `$onChanges` hook iterations.
+ * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
+ */
+ this.onChangesTtl = function(value) {
+ if (arguments.length) {
+ TTL = value;
+ return this;
+ }
+ return TTL;
+ };
+
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
- var Attributes = function(element, attributesToCopy) {
+ var SIMPLE_ATTR_NAME = /^\w/;
+ var specialAttrHolder = window.document.createElement('div');
+
+
+
+ var onChangesTtl = TTL;
+ // The onChanges hooks should all be run together in a single digest
+ // When changes occur, the call to trigger their hooks will be added to this queue
+ var onChangesQueue;
+
+ // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
+ function flushOnChangesQueue() {
+ try {
+ if (!(--onChangesTtl)) {
+ // We have hit the TTL limit so reset everything
+ onChangesQueue = undefined;
+ throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
+ }
+ // We must run this hook in an apply since the $$postDigest runs outside apply
+ $rootScope.$apply(function() {
+ var errors = [];
+ for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
+ try {
+ onChangesQueue[i]();
+ } catch (e) {
+ errors.push(e);
+ }
+ }
+ // Reset the queue to trigger a new schedule next time there is a change
+ onChangesQueue = undefined;
+ if (errors.length) {
+ throw errors;
+ }
+ });
+ } finally {
+ onChangesTtl++;
+ }
+ }
+
+
+ function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
var i, l, key;
@@ -7494,7 +8124,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
this.$$element = element;
- };
+ }
Attributes.prototype = {
/**
@@ -7615,11 +8245,11 @@ 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');
- } else if (nodeName === 'img' && key === 'srcset') {
+ } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
// sanitize img[srcset] values
var result = "";
@@ -7659,7 +8289,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
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);
+ }
}
}
@@ -7713,6 +8347,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 = "<span " + attrName + ">";
+ 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 {
@@ -7759,6 +8405,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
} : noop;
+ compile.$$createComment = function(directiveName, comment) {
+ var content = '';
+ if (debugInfoEnabled) {
+ content = ' ' + (directiveName || '') + ': ';
+ if (comment) content += comment + ' ';
+ }
+ return window.document.createComment(content);
+ };
+
return compile;
//================================
@@ -7779,7 +8434,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var domNode = $compileNodes[i];
if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
- jqLiteWrapNode(domNode, $compileNodes[i] = document.createElement('span'));
+ jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span'));
}
}
@@ -7853,7 +8508,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!node) {
return 'html';
} else {
- return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html';
+ return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html';
}
}
@@ -7972,8 +8627,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
-
- var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
+ function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
transcludedScope = scope.$new(false, containingScope);
@@ -7985,7 +8639,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transcludeControllers: controllers,
futureParentElement: futureParentElement
});
- };
+ }
+
+ // We need to attach the transclusion slots onto the `boundTranscludeFn`
+ // so that they are available inside the `controllersBoundTransclude` function
+ var boundSlots = boundTranscludeFn.$$slots = createMap();
+ for (var slotName in transcludeFn.$$slots) {
+ if (transcludeFn.$$slots[slotName]) {
+ boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
+ } else {
+ boundSlots[slotName] = null;
+ }
+ }
return boundTranscludeFn;
}
@@ -8078,19 +8743,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
addTextInterpolateDirective(directives, node.nodeValue);
break;
case NODE_TYPE_COMMENT: /* Comment */
- try {
- match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
- if (match) {
- nName = directiveNormalize(match[1]);
- if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[2]);
- }
- }
- } catch (e) {
- // turns out that under some circumstances IE9 throws errors when one attempts to read
- // comment's node value.
- // Just ignore it and continue. (Can't seem to reproduce in test case.)
- }
+ collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective);
break;
}
@@ -8098,6 +8751,24 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return directives;
}
+ function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
+ // function created because of performance, try/catch disables
+ // the optimization of the whole function #14848
+ try {
+ var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
+ if (match) {
+ var nName = directiveNormalize(match[1]);
+ if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
+ attrs[nName] = trim(match[2]);
+ }
+ }
+ } catch (e) {
+ // turns out that under some circumstances IE9 throws errors when one attempts to read
+ // comment's node value.
+ // Just ignore it and continue. (Can't seem to reproduce in test case.)
+ }
+ }
+
/**
* Given a node with an directive-start it collects all of the siblings until it finds
* directive-end.
@@ -8139,13 +8810,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @returns {Function}
*/
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
- return function(scope, element, attrs, controllers, transcludeFn) {
+ return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) {
element = groupScan(element[0], attrStart, attrEnd);
return linkFn(scope, element, attrs, controllers, transcludeFn);
};
}
/**
+ * A function generator that is used to support both eager and lazy compilation
+ * linking function.
+ * @param eager
+ * @param $compileNodes
+ * @param transcludeFn
+ * @param maxPriority
+ * @param ignoreDirective
+ * @param previousCompileContext
+ * @returns {Function}
+ */
+ function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
+ var compiled;
+
+ if (eager) {
+ return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+ }
+ return function lazyCompilation() {
+ if (!compiled) {
+ compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+
+ // Null out all of these references in order to make them eligible for garbage collection
+ // since this is a potentially long lived closure
+ $compileNodes = transcludeFn = previousCompileContext = null;
+ }
+ return compiled.apply(this, arguments);
+ };
+ }
+
+ /**
* Once the directives have been collected, their compile functions are executed. This method
* is responsible for inlining directive templates as well as terminating the application
* of the directives if the terminal directive has been reached.
@@ -8189,6 +8889,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = originalReplaceDirective,
childTranscludeFn = transcludeFn,
linkFn,
+ didScanForMultipleTransclusion = false,
+ mightHaveMultipleTransclusionError = false,
directiveValue;
// executes all directives on the current element
@@ -8231,6 +8933,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();
@@ -8255,12 +8978,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
terminalPriority = directive.priority;
$template = $compileNode;
$compileNode = templateAttrs.$$element =
- jqLite(document.createComment(' ' + directiveName + ': ' +
- templateAttrs[directiveName] + ' '));
+ jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName]));
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);
- childTranscludeFn = compile($template, transcludeFn, terminalPriority,
+ // Support: Chrome < 50
+ // https://github.com/angular/angular.js/issues/14041
+
+ // In the versions of V8 prior to Chrome 50, the document fragment that is created
+ // in the `replaceWith` function is improperly garbage collected despite still
+ // being referenced by the `parentNode` property of all of the child nodes. By adding
+ // a reference to the fragment via a different property, we can avoid that incorrect
+ // behavior.
+ // TODO: remove this line after Chrome 50 has been released
+ $template[0].$$parentNode = $template[0].parentNode;
+
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name, {
// Don't pass in:
// - controllerDirectives - otherwise we'll create duplicates controllers
@@ -8272,10 +9005,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, undefined,
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
+ childTranscludeFn.$$slots = slots;
}
}
@@ -8341,7 +9133,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = directive;
}
+ /* jshint -W021 */
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
+ /* jshint +W021 */
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
@@ -8353,10 +9147,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else if (directive.compile) {
try {
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
+ var context = directive.$$originalDirective || directive;
if (isFunction(linkFn)) {
- addLinkFns(null, linkFn, attrStart, attrEnd);
+ addLinkFns(null, bind(context, linkFn), attrStart, attrEnd);
} else if (linkFn) {
- addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
+ addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
@@ -8403,77 +9198,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
-
- function getControllers(directiveName, require, $element, elementControllers) {
- var value;
-
- if (isString(require)) {
- var match = require.match(REQUIRE_PREFIX_REGEXP);
- var name = require.substring(match[0].length);
- var inheritType = match[1] || match[3];
- var optional = match[2] === '?';
-
- //If only parents then start at the parent element
- if (inheritType === '^^') {
- $element = $element.parent();
- //Otherwise attempt getting the controller from elementControllers in case
- //the element is transcluded (and has no data) and to avoid .data if possible
- } else {
- value = elementControllers && elementControllers[name];
- value = value && value.instance;
- }
-
- if (!value) {
- var dataName = '$' + name + 'Controller';
- value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
- }
-
- if (!value && !optional) {
- throw $compileMinErr('ctreq',
- "Controller '{0}', required by directive '{1}', can't be found!",
- name, directiveName);
- }
- } else if (isArray(require)) {
- value = [];
- for (var i = 0, ii = require.length; i < ii; i++) {
- value[i] = getControllers(directiveName, require[i], $element, elementControllers);
- }
- }
-
- return value || null;
- }
-
- function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
- var elementControllers = createMap();
- for (var controllerKey in controllerDirectives) {
- var directive = controllerDirectives[controllerKey];
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- };
-
- var controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
-
- // For directives with element transclusion the element is a comment.
- // In this case .data will not attach any data.
- // Instead, we save the controllers for the element in a local hash and attach to .data
- // later, once we have the actual element.
- elementControllers[directive.name] = controllerInstance;
- $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
- }
- return elementControllers;
- }
-
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
- attrs, removeScopeBindingWatches, removeControllerBindingWatches;
+ var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
+ attrs, scopeBindingInfo;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -8495,10 +9222,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
transcludeFn = controllersBoundTransclude;
transcludeFn.$$boundTransclude = boundTranscludeFn;
+ // expose the slots on the `$transclude` function
+ transcludeFn.isSlotFilled = function(slotName) {
+ return !!boundTranscludeFn.$$slots[slotName];
+ };
}
if (controllerDirectives) {
- elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
+ elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective);
}
if (newIsolateScopeDirective) {
@@ -8508,11 +9239,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
- removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
+ scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope,
isolateScope.$$isolateBindings,
newIsolateScopeDirective);
- if (removeScopeBindingWatches) {
- isolateScope.$on('$destroy', removeScopeBindingWatches);
+ if (scopeBindingInfo.removeWatches) {
+ isolateScope.$on('$destroy', scopeBindingInfo.removeWatches);
}
}
@@ -8523,8 +9254,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var bindings = controllerDirective.$$bindings.bindToController;
if (controller.identifier && bindings) {
- removeControllerBindingWatches =
+ controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ } else {
+ controller.bindingInfo = {};
}
var controllerResult = controller();
@@ -8533,12 +9266,48 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// from setupControllers
controller.instance = controllerResult;
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
- removeControllerBindingWatches && removeControllerBindingWatches();
- removeControllerBindingWatches =
+ controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches();
+ controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
}
+ // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
+ forEach(controllerDirectives, function(controllerDirective, name) {
+ var require = controllerDirective.require;
+ if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
+ extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
+ }
+ });
+
+ // Handle the init and destroy lifecycle hooks on all controllers that have them
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$onChanges)) {
+ try {
+ controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ if (isFunction(controllerInstance.$onInit)) {
+ try {
+ controllerInstance.$onInit();
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ if (isFunction(controllerInstance.$doCheck)) {
+ controllerScope.$watch(function() { controllerInstance.$doCheck(); });
+ controllerInstance.$doCheck();
+ }
+ if (isFunction(controllerInstance.$onDestroy)) {
+ controllerScope.$on('$destroy', function callOnDestroyHook() {
+ controllerInstance.$onDestroy();
+ });
+ }
+ });
+
// PRELINKING
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[i];
@@ -8572,13 +9341,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
);
}
+ // Trigger $postLink lifecycle hooks
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$postLink)) {
+ controllerInstance.$postLink();
+ }
+ });
+
// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
- function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
+ function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
var transcludeControllers;
-
// No scope passed in:
if (!isScope(scope)) {
+ slotName = futureParentElement;
futureParentElement = cloneAttachFn;
cloneAttachFn = scope;
scope = undefined;
@@ -8590,11 +9367,99 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ if (slotName) {
+ // slotTranscludeFn can be one of three things:
+ // * a transclude function - a filled slot
+ // * `null` - an optional slot that was not filled
+ // * `undefined` - a slot that was not declared (i.e. invalid)
+ var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
+ if (slotTranscludeFn) {
+ return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ } else if (isUndefined(slotTranscludeFn)) {
+ throw $compileMinErr('noslot',
+ 'No parent directive that requires a transclusion with slot name "{0}". ' +
+ 'Element: {1}',
+ slotName, startingTag($element));
+ }
+ } else {
+ return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ }
}
}
}
+ function getControllers(directiveName, require, $element, elementControllers) {
+ var value;
+
+ if (isString(require)) {
+ var match = require.match(REQUIRE_PREFIX_REGEXP);
+ var name = require.substring(match[0].length);
+ var inheritType = match[1] || match[3];
+ var optional = match[2] === '?';
+
+ //If only parents then start at the parent element
+ if (inheritType === '^^') {
+ $element = $element.parent();
+ //Otherwise attempt getting the controller from elementControllers in case
+ //the element is transcluded (and has no data) and to avoid .data if possible
+ } else {
+ value = elementControllers && elementControllers[name];
+ value = value && value.instance;
+ }
+
+ if (!value) {
+ var dataName = '$' + name + 'Controller';
+ value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
+ }
+
+ if (!value && !optional) {
+ throw $compileMinErr('ctreq',
+ "Controller '{0}', required by directive '{1}', can't be found!",
+ name, directiveName);
+ }
+ } else if (isArray(require)) {
+ value = [];
+ for (var i = 0, ii = require.length; i < ii; i++) {
+ value[i] = getControllers(directiveName, require[i], $element, elementControllers);
+ }
+ } else if (isObject(require)) {
+ value = {};
+ forEach(require, function(controller, property) {
+ value[property] = getControllers(directiveName, controller, $element, elementControllers);
+ });
+ }
+
+ return value || null;
+ }
+
+ function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) {
+ var elementControllers = createMap();
+ for (var controllerKey in controllerDirectives) {
+ var directive = controllerDirectives[controllerKey];
+ var locals = {
+ $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
+ $element: $element,
+ $attrs: attrs,
+ $transclude: transcludeFn
+ };
+
+ var controller = directive.controller;
+ if (controller == '@') {
+ controller = attrs[directive.name];
+ }
+
+ var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
+
+ // For directives with element transclusion the element is a comment.
+ // In this case .data will not attach any data.
+ // Instead, we save the controllers for the element in a local hash and attach to .data
+ // later, once we have the actual element.
+ elementControllers[directive.name] = controllerInstance;
+ $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
+ }
+ return elementControllers;
+ }
+
// Depending upon the context in which a directive finds itself it might need to have a new isolated
// or child scope created. For instance:
// * if the directive has been pulled into a template because another directive with a higher priority
@@ -8698,18 +9563,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// copy the new attributes on the old attrs object
forEach(src, function(value, key) {
- if (key == 'class') {
- safeAddClass($element, value);
- dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
- } else if (key == 'style') {
- $element.attr('style', $element.attr('style') + ';' + value);
- dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value;
- // `dst` will never contain hasOwnProperty as DOM parser won't let it.
- // You will get an "InvalidCharacterError: DOM Exception 5" error if you
- // have an attribute like "has-own-property" or "data-has-own-property", etc.
- } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
+ // Check if we already set this attribute in the loop above.
+ // `dst` will never contain hasOwnProperty as DOM parser won't let it.
+ // You will get an "InvalidCharacterError: DOM Exception 5" error if you
+ // have an attribute like "has-own-property" or "data-has-own-property", etc.
+ if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') {
dst[key] = value;
- dstAttr[key] = srcAttr[key];
+
+ if (key !== 'class' && key !== 'style') {
+ dstAttr[key] = srcAttr[key];
+ }
}
});
}
@@ -8889,7 +9752,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
switch (type) {
case 'svg':
case 'math':
- var wrapper = document.createElement('div');
+ var wrapper = window.document.createElement('div');
wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
return wrapper.childNodes[0].childNodes;
default:
@@ -9029,9 +9892,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parent.replaceChild(newNode, firstElementToRemove);
}
- // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it?
- var fragment = document.createDocumentFragment();
- fragment.appendChild(firstElementToRemove);
+ // Append all the `elementsToRemove` to a fragment. This will...
+ // - remove them from the DOM
+ // - allow them to still be traversed with .nextSibling
+ // - allow a single fragment.qSA to fetch all elements being removed
+ var fragment = window.document.createDocumentFragment();
+ for (i = 0; i < removeCount; i++) {
+ fragment.appendChild(elementsToRemove[i]);
+ }
if (jqLite.hasData(firstElementToRemove)) {
// Copy over user data (that includes Angular's $scope etc.). Don't copy private
@@ -9039,31 +9907,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// event listeners (which is the main use of private data) wouldn't work anyway.
jqLite.data(newNode, jqLite.data(firstElementToRemove));
- // Remove data of the replaced element. We cannot just call .remove()
- // on the element it since that would deallocate scope that is needed
- // for the new node. Instead, remove the data "manually".
- if (!jQuery) {
- delete jqLite.cache[firstElementToRemove[jqLite.expando]];
- } else {
- // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after
- // the replaced element. The cleanData version monkey-patched by Angular would cause
- // the scope to be trashed and we do need the very same scope to work with the new
- // element. However, we cannot just cache the non-patched version and use it here as
- // that would break if another library patches the method after Angular does (one
- // example is jQuery UI). Instead, set a flag indicating scope destroying should be
- // skipped this one time.
- skipDestroyOnNextJQueryCleanData = true;
- jQuery.cleanData([firstElementToRemove]);
- }
+ // Remove $destroy event listeners from `firstElementToRemove`
+ jqLite(firstElementToRemove).off('$destroy');
}
- for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
- var element = elementsToRemove[k];
- jqLite(element).remove(); // must do this way to clean up expando
- fragment.appendChild(element);
- delete elementsToRemove[k];
- }
+ // Cleanup any data/listeners on the elements and children.
+ // This includes invoking the $destroy event on any elements with listeners.
+ jqLite.cleanData(fragment.querySelectorAll('*'));
+ // Update the jqLite collection to only contain the `newNode`
+ for (i = 1; i < removeCount; i++) {
+ delete elementsToRemove[i];
+ }
elementsToRemove[0] = newNode;
elementsToRemove.length = 1;
}
@@ -9087,12 +9942,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// only occurs for isolate scopes and new scopes with controllerAs.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
- forEach(bindings, function(definition, scopeName) {
+ var initialChanges = {};
+ var changes;
+ forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
- mode = definition.mode, // @, =, or &
+ mode = definition.mode, // @, =, <, or &
lastValue,
- parentGet, parentSet, compare;
+ parentGet, parentSet, compare, removeWatch;
switch (mode) {
@@ -9101,7 +9958,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
- if (isString(value)) {
+ if (isString(value) || isBoolean(value)) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, value, oldValue);
destination[scopeName] = value;
}
});
@@ -9116,6 +9975,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// the value to boolean rather than a string, so we special case this situation
destination[scopeName] = lastValue;
}
+ initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
break;
case '=':
@@ -9129,7 +9989,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (parentGet.literal) {
compare = equals;
} else {
- compare = function(a, b) { return a === b || (a !== a && b !== b); };
+ compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); };
}
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
@@ -9153,7 +10013,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
- var removeWatch;
if (definition.collection) {
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
@@ -9162,6 +10021,30 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
removeWatchCollection.push(removeWatch);
break;
+ case '<':
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
+ }
+ if (optional && !attrs[attrName]) break;
+
+ parentGet = $parse(attrs[attrName]);
+
+ var initialValue = destination[scopeName] = parentGet(scope);
+ initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
+
+ removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
+ if (oldValue === newValue) {
+ if (oldValue === initialValue) return;
+ oldValue = initialValue;
+ }
+ recordChanges(scopeName, newValue, oldValue);
+ destination[scopeName] = newValue;
+ }, parentGet.literal);
+
+ removeWatchCollection.push(removeWatch);
+ break;
+
case '&':
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
@@ -9176,15 +10059,52 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
- return removeWatchCollection.length && function removeWatches() {
- for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
- removeWatchCollection[i]();
+ function recordChanges(key, currentValue, previousValue) {
+ if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
+ // If we have not already scheduled the top level onChangesQueue handler then do so now
+ if (!onChangesQueue) {
+ scope.$$postDigest(flushOnChangesQueue);
+ onChangesQueue = [];
+ }
+ // If we have not already queued a trigger of onChanges for this controller then do so now
+ if (!changes) {
+ changes = {};
+ onChangesQueue.push(triggerOnChangesHook);
+ }
+ // If the has been a change on this property already then we need to reuse the previous value
+ if (changes[key]) {
+ previousValue = changes[key].previousValue;
+ }
+ // Store this change
+ changes[key] = new SimpleChange(previousValue, currentValue);
+ }
+ }
+
+ function triggerOnChangesHook() {
+ destination.$onChanges(changes);
+ // Now clear the changes so that we schedule onChanges when more changes arrive
+ changes = undefined;
+ }
+
+ return {
+ initialChanges: initialChanges,
+ removeWatches: removeWatchCollection.length && function removeWatches() {
+ for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
+ removeWatchCollection[i]();
+ }
}
};
}
}];
}
+function SimpleChange(previous, current) {
+ this.previousValue = previous;
+ this.currentValue = current;
+}
+SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
+
+
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
/**
* Converts all accepted directives format into proper directive name.
@@ -9316,6 +10236,15 @@ function $ControllerProvider() {
/**
* @ngdoc method
+ * @name $controllerProvider#has
+ * @param {string} name Controller name to check.
+ */
+ this.has = function(name) {
+ return controllers.hasOwnProperty(name);
+ };
+
+ /**
+ * @ngdoc method
* @name $controllerProvider#register
* @param {string|Object} name Controller name, or an object map of controllers where the keys are
* the names and the values are the constructors.
@@ -9370,7 +10299,7 @@ function $ControllerProvider() {
* It's just a simple call to {@link auto.$injector $injector}, but extracted into
* a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
*/
- return function(expression, locals, later, ident) {
+ return function $controller(expression, locals, later, ident) {
// PRIVATE API:
// param `later` --- indicates that the controller's constructor is invoked at a later time.
// If true, $controller will allocate the object with the correct
@@ -9421,7 +10350,7 @@ function $ControllerProvider() {
}
var instantiate;
- return instantiate = extend(function() {
+ return instantiate = extend(function $controllerInit() {
var result = $injector.invoke(expression, instance, locals, constructor);
if (result !== instance && (isObject(result) || isFunction(result))) {
instance = result;
@@ -9504,18 +10433,21 @@ function $DocumentProvider() {
*
* ## Example:
*
+ * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught
+ * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead
+ * of `$log.error()`.
+ *
* ```js
- * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
- * return function(exception, cause) {
- * exception.message += ' (caused by "' + cause + '")';
- * throw exception;
- * };
- * });
+ * angular.
+ * module('exceptionOverwrite', []).
+ * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) {
+ * return function myExceptionHandler(exception, cause) {
+ * logErrorsToBackend(exception, cause);
+ * $log.warn(exception, cause);
+ * };
+ * }]);
* ```
*
- * This example will override the normal action of `$exceptionHandler`, to make angular
- * exceptions fail hard when they happen, instead of just logging to the console.
- *
* <hr />
* Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
* methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
@@ -9525,7 +10457,7 @@ function $DocumentProvider() {
* `try { ... } catch(e) { $exceptionHandler(e); }`
*
* @param {Error} exception Exception associated with the error.
- * @param {string=} cause optional information about the context in which
+ * @param {string=} cause Optional information about the context in which
* the error was thrown.
*
*/
@@ -9595,7 +10527,7 @@ function $HttpParamSerializerProvider() {
* * `{'foo': 'bar'}` results in `foo=bar`
* * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
* * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
- * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
+ * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
*
* Note that serializer will sort the request parameters alphabetically.
* */
@@ -9607,7 +10539,7 @@ function $HttpParamSerializerProvider() {
forEachSorted(params, function(value, key) {
if (value === null || isUndefined(value)) return;
if (isArray(value)) {
- forEach(value, function(v, k) {
+ forEach(value, function(v) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
@@ -10010,10 +10942,13 @@ function $HttpProvider() {
* - **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.
+ * A response status code between 200 and 299 is considered a success status and will result in
+ * the success callback being called. Any response status code outside of that range is
+ * considered an error status and will result in the error callback being called.
+ * Also, status codes less than -1 are normalized to zero. -1 usually means the request was
+ * aborted, e.g. using a `config.timeout`.
+ * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning
+ * that the outcome (success or error) will be determined by the final response status code.
*
*
* ## Shortcut methods
@@ -10080,7 +11015,7 @@ function $HttpProvider() {
*
* ```
* module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'
+ * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
* });
* ```
*
@@ -10115,7 +11050,7 @@ function $HttpProvider() {
* That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
* For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
* function will be reflected on the scope and in any templates where the object is data-bound.
- * To prevent his, transform functions should have no side-effects.
+ * To prevent this, transform functions should have no side-effects.
* If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
* </div>
*
@@ -10143,7 +11078,7 @@ function $HttpProvider() {
*
* ### Overriding the Default Transformations Per Request
*
- * If you wish override the request/response transformations only for a single request then provide
+ * If you wish to override the request/response transformations only for a single request then provide
* `transformRequest` and/or `transformResponse` properties on the configuration object passed
* into `$http`.
*
@@ -10186,7 +11121,7 @@ function $HttpProvider() {
* * cache a specific response - set config.cache value to TRUE or to a cache object
*
* If caching is enabled, but neither the default cache nor config.cache are set to a cache object,
- * then the default `$cacheFactory($http)` object is used.
+ * then the default `$cacheFactory("$http")` object is used.
*
* The default cache value can be set by updating the
* {@link ng.$http#defaults `$http.defaults.cache`} property or the
@@ -10361,6 +11296,12 @@ function $HttpProvider() {
* - **headers** – `{Object}` – Map of strings or functions which return strings representing
* HTTP headers to send to the server. If the return value of a function is null, the
* header will not be sent. Functions accept a config object as an argument.
+ * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object.
+ * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`.
+ * The handler will be called in the context of a `$apply` block.
+ * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload
+ * object. To bind events to the XMLHttpRequest object, use `eventHandlers`.
+ * The handler will be called in the context of a `$apply` block.
* - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
* - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
* - **transformRequest** –
@@ -10490,7 +11431,7 @@ function $HttpProvider() {
*/
function $http(requestConfig) {
- if (!angular.isObject(requestConfig)) {
+ if (!isObject(requestConfig)) {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}
@@ -10508,48 +11449,25 @@ function $HttpProvider() {
config.headers = mergeHeaders(requestConfig);
config.method = uppercase(config.method);
config.paramSerializer = isString(config.paramSerializer) ?
- $injector.get(config.paramSerializer) : config.paramSerializer;
+ $injector.get(config.paramSerializer) : config.paramSerializer;
- var serverRequest = function(config) {
- var headers = config.headers;
- var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
-
- // strip content-type if data is undefined
- if (isUndefined(reqData)) {
- forEach(headers, function(value, header) {
- if (lowercase(header) === 'content-type') {
- delete headers[header];
- }
- });
- }
-
- if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
- config.withCredentials = defaults.withCredentials;
- }
-
- // send request
- return sendReq(config, reqData).then(transformResponse, transformResponse);
- };
-
- var chain = [serverRequest, undefined];
+ var requestInterceptors = [];
+ var responseInterceptors = [];
var promise = $q.when(config);
// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
if (interceptor.request || interceptor.requestError) {
- chain.unshift(interceptor.request, interceptor.requestError);
+ requestInterceptors.unshift(interceptor.request, interceptor.requestError);
}
if (interceptor.response || interceptor.responseError) {
- chain.push(interceptor.response, interceptor.responseError);
+ responseInterceptors.push(interceptor.response, interceptor.responseError);
}
});
- while (chain.length) {
- var thenFn = chain.shift();
- var rejectFn = chain.shift();
-
- promise = promise.then(thenFn, rejectFn);
- }
+ promise = chainInterceptors(promise, requestInterceptors);
+ promise = promise.then(serverRequest);
+ promise = chainInterceptors(promise, responseInterceptors);
if (useLegacyPromise) {
promise.success = function(fn) {
@@ -10576,14 +11494,18 @@ function $HttpProvider() {
return promise;
- function transformResponse(response) {
- // make a copy since the response must be cacheable
- var resp = extend({}, response);
- resp.data = transformData(response.data, response.headers, response.status,
- config.transformResponse);
- return (isSuccess(response.status))
- ? resp
- : $q.reject(resp);
+
+ function chainInterceptors(promise, interceptors) {
+ for (var i = 0, ii = interceptors.length; i < ii;) {
+ var thenFn = interceptors[i++];
+ var rejectFn = interceptors[i++];
+
+ promise = promise.then(thenFn, rejectFn);
+ }
+
+ interceptors.length = 0;
+
+ return promise;
}
function executeHeaderFns(headers, config) {
@@ -10610,7 +11532,7 @@ function $HttpProvider() {
defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]);
- // using for-in instead of forEach to avoid unecessary iteration after header has been found
+ // using for-in instead of forEach to avoid unnecessary iteration after header has been found
defaultHeadersIteration:
for (defHeaderName in defHeaders) {
lowercaseDefHeaderName = lowercase(defHeaderName);
@@ -10627,6 +11549,37 @@ function $HttpProvider() {
// execute if header value is a function for merged headers
return executeHeaderFns(reqHeaders, shallowCopy(config));
}
+
+ function serverRequest(config) {
+ var headers = config.headers;
+ var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
+
+ // strip content-type if data is undefined
+ if (isUndefined(reqData)) {
+ forEach(headers, function(value, header) {
+ if (lowercase(header) === 'content-type') {
+ delete headers[header];
+ }
+ });
+ }
+
+ if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
+ config.withCredentials = defaults.withCredentials;
+ }
+
+ // send request
+ return sendReq(config, reqData).then(transformResponse, transformResponse);
+ }
+
+ function transformResponse(response) {
+ // make a copy since the response must be cacheable
+ var resp = extend({}, response);
+ resp.data = transformData(response.data, response.headers, response.status,
+ config.transformResponse);
+ return (isSuccess(response.status))
+ ? resp
+ : $q.reject(resp);
+ }
}
$http.pendingRequests = [];
@@ -10673,6 +11626,8 @@ function $HttpProvider() {
*
* @description
* Shortcut method to perform `JSONP` request.
+ * If you would like to customise where and how the callbacks are stored then try overriding
+ * or decorating the {@link $jsonpCallbacks} service.
*
* @param {string} url Relative or absolute URL specifying the destination of the request.
* The name of the callback should be the string `JSON_CALLBACK`.
@@ -10819,11 +11774,35 @@ function $HttpProvider() {
}
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
- config.withCredentials, config.responseType);
+ config.withCredentials, config.responseType,
+ createApplyHandlers(config.eventHandlers),
+ createApplyHandlers(config.uploadEventHandlers));
}
return promise;
+ function createApplyHandlers(eventHandlers) {
+ if (eventHandlers) {
+ var applyHandlers = {};
+ forEach(eventHandlers, function(eventHandler, key) {
+ applyHandlers[key] = function(event) {
+ if (useApplyAsync) {
+ $rootScope.$applyAsync(callEventHandler);
+ } else if ($rootScope.$$phase) {
+ callEventHandler();
+ } else {
+ $rootScope.$apply(callEventHandler);
+ }
+
+ function callEventHandler() {
+ eventHandler(event);
+ }
+ };
+ });
+ return applyHandlers;
+ }
+ }
+
/**
* Callback registered to $httpBackend():
@@ -10922,7 +11901,7 @@ function $xhrFactoryProvider() {
/**
* @ngdoc service
* @name $httpBackend
- * @requires $window
+ * @requires $jsonpCallbacks
* @requires $document
* @requires $xhrFactory
*
@@ -10937,28 +11916,24 @@ function $xhrFactoryProvider() {
* $httpBackend} which can be trained with responses.
*/
function $HttpBackendProvider() {
- this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) {
- return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]);
+ this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) {
+ return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]);
}];
}
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
// TODO(vojta): fix the signature
- return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
+ return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();
- if (lowercase(method) == 'jsonp') {
- var callbackId = '_' + (callbacks.counter++).toString(36);
- callbacks[callbackId] = function(data) {
- callbacks[callbackId].data = data;
- callbacks[callbackId].called = true;
- };
-
- var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
- callbackId, function(status, text) {
- completeRequest(callback, status, callbacks[callbackId].data, "", text);
- callbacks[callbackId] = noop;
+ if (lowercase(method) === 'jsonp') {
+ var callbackPath = callbacks.createCallback(url);
+ var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
+ // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
+ var response = (status === 200) && callbacks.getResponse(callbackPath);
+ completeRequest(callback, status, response, "", text);
+ callbacks.removeCallback(callbackPath);
});
} else {
@@ -11004,6 +11979,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
xhr.onerror = requestError;
xhr.onabort = requestError;
+ forEach(eventHandlers, function(value, key) {
+ xhr.addEventListener(key, value);
+ });
+
+ forEach(uploadEventHandlers, function(value, key) {
+ xhr.upload.addEventListener(key, value);
+ });
+
if (withCredentials) {
xhr.withCredentials = true;
}
@@ -11052,7 +12035,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
};
- function jsonpReq(url, callbackId, done) {
+ function jsonpReq(url, callbackPath, done) {
+ url = url.replace('JSON_CALLBACK', callbackPath);
// we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
// - fetches local scripts via XHR and evals them
// - adds and immediately removes script elements from the document
@@ -11070,7 +12054,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
var text = "unknown";
if (event) {
- if (event.type === "load" && !callbacks[callbackId].called) {
+ if (event.type === "load" && !callbacks.wasCalled(callbackPath)) {
event = { type: "error" };
}
text = event.type;
@@ -11109,6 +12093,14 @@ $interpolateMinErr.interr = function(text, err) {
*
* Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
*
+ * <div class="alert alert-danger">
+ * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular
+ * template within a Python Jinja template (or any other template language). Mixing templating
+ * languages is **very dangerous**. The embedding template language will not safely escape Angular
+ * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS)
+ * security bugs!
+ * </div>
+ *
* @example
<example name="custom-interpolation-markup" module="customInterpolationApp">
<file name="index.html">
@@ -11209,6 +12201,15 @@ function $InterpolateProvider() {
return value;
}
+ //TODO: this is the same as the constantWatchDelegate in parse.js
+ function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
+ var unwatch;
+ return unwatch = scope.$watch(function constantInterpolateWatch(scope) {
+ unwatch();
+ return constantInterp(scope);
+ }, listener, objectEquality);
+ }
+
/**
* @ngdoc service
* @name $interpolate
@@ -11252,7 +12253,7 @@ function $InterpolateProvider() {
*
* `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior.
*
- * ####Escaped Interpolation
+ * #### Escaped Interpolation
* $interpolate provides a mechanism for escaping interpolation markers. Start and end markers
* can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash).
* It will be rendered as a regular start/end marker, and will not be interpreted as an expression
@@ -11288,6 +12289,30 @@ function $InterpolateProvider() {
* </file>
* </example>
*
+ * @knownIssue
+ * It is currently not possible for an interpolated expression to contain the interpolation end
+ * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e.
+ * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string.
+ *
+ * @knownIssue
+ * All directives and components must use the standard `{{` `}}` interpolation symbols
+ * in their templates. If you change the application interpolation symbols the {@link $compile}
+ * service will attempt to denormalize the standard symbols to the custom symbols.
+ * The denormalization process is not clever enough to know not to replace instances of the standard
+ * symbols where they would not normally be treated as interpolation symbols. For example in the following
+ * code snippet the closing braces of the literal object will get incorrectly denormalized:
+ *
+ * ```
+ * <div data-context='{"context":{"id":3,"type":"page"}}">
+ * ```
+ *
+ * The workaround is to ensure that such instances are separated by whitespace:
+ * ```
+ * <div data-context='{"context":{"id":3,"type":"page"} }">
+ * ```
+ *
+ * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information.
+ *
* @param {string} text The text with markup to interpolate.
* @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
* embedded expression in order to return an interpolation function. Strings with no
@@ -11304,6 +12329,19 @@ function $InterpolateProvider() {
* - `context`: evaluation context for all expressions embedded in the interpolated text
*/
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
+ // Provide a quick exit and simplified result function for text with no interpolation
+ if (!text.length || text.indexOf(startSymbol) === -1) {
+ var constantInterp;
+ if (!mustHaveExpression) {
+ var unescapedText = unescapeText(text);
+ constantInterp = valueFn(unescapedText);
+ constantInterp.exp = text;
+ constantInterp.expressions = [];
+ constantInterp.$$watchDelegate = constantWatchDelegate;
+ }
+ return constantInterp;
+ }
+
allOrNothing = !!allOrNothing;
var startIndex,
endIndex,
@@ -11440,8 +12478,8 @@ function $InterpolateProvider() {
}
function $IntervalProvider() {
- this.$get = ['$rootScope', '$window', '$q', '$$q',
- function($rootScope, $window, $q, $$q) {
+ this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
+ function($rootScope, $window, $q, $$q, $browser) {
var intervals = {};
@@ -11582,11 +12620,12 @@ function $IntervalProvider() {
count = isDefined(count) ? count : 0;
- promise.then(null, null, (!hasParams) ? fn : function() {
- fn.apply(null, args);
- });
-
promise.$$intervalId = setInterval(function tick() {
+ if (skipApply) {
+ $browser.defer(callback);
+ } else {
+ $rootScope.$evalAsync(callback);
+ }
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
@@ -11602,6 +12641,14 @@ function $IntervalProvider() {
intervals[promise.$$intervalId] = deferred;
return promise;
+
+ function callback() {
+ if (!hasParams) {
+ fn(iteration);
+ } else {
+ fn.apply(null, args);
+ }
+ }
}
@@ -11631,6 +12678,87 @@ function $IntervalProvider() {
/**
* @ngdoc service
+ * @name $jsonpCallbacks
+ * @requires $window
+ * @description
+ * This service handles the lifecycle of callbacks to handle JSONP requests.
+ * Override this service if you wish to customise where the callbacks are stored and
+ * how they vary compared to the requested url.
+ */
+var $jsonpCallbacksProvider = function() {
+ this.$get = ['$window', function($window) {
+ var callbacks = $window.angular.callbacks;
+ var callbackMap = {};
+
+ function createCallback(callbackId) {
+ var callback = function(data) {
+ callback.data = data;
+ callback.called = true;
+ };
+ callback.id = callbackId;
+ return callback;
+ }
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#createCallback
+ * @param {string} url the url of the JSONP request
+ * @returns {string} the callback path to send to the server as part of the JSONP request
+ * @description
+ * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback
+ * to pass to the server, which will be used to call the callback with its payload in the JSONP response.
+ */
+ createCallback: function(url) {
+ var callbackId = '_' + (callbacks.$$counter++).toString(36);
+ var callbackPath = 'angular.callbacks.' + callbackId;
+ var callback = createCallback(callbackId);
+ callbackMap[callbackPath] = callbacks[callbackId] = callback;
+ return callbackPath;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#wasCalled
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @returns {boolean} whether the callback has been called, as a result of the JSONP response
+ * @description
+ * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the
+ * callback that was passed in the request.
+ */
+ wasCalled: function(callbackPath) {
+ return callbackMap[callbackPath].called;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#getResponse
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @returns {*} the data received from the response via the registered callback
+ * @description
+ * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback
+ * in the JSONP response.
+ */
+ getResponse: function(callbackPath) {
+ return callbackMap[callbackPath].data;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#removeCallback
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @description
+ * {@link $httpBackend} calls this method to remove the callback after the JSONP request has
+ * completed or timed-out.
+ */
+ removeCallback: function(callbackPath) {
+ var callback = callbackMap[callbackPath];
+ delete callbacks[callback.id];
+ delete callbackMap[callbackPath];
+ }
+ };
+ }];
+};
+
+/**
+ * @ngdoc service
* @name $locale
*
* @description
@@ -11688,17 +12816,20 @@ function parseAppUrl(relativeUrl, locationObj) {
}
}
+function startsWith(haystack, needle) {
+ return haystack.lastIndexOf(needle, 0) === 0;
+}
/**
*
- * @param {string} begin
- * @param {string} whole
- * @returns {string} returns text from whole after begin or undefined if it does not begin with
- * expected string.
+ * @param {string} base
+ * @param {string} url
+ * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with
+ * the expected string.
*/
-function beginsWith(begin, whole) {
- if (whole.indexOf(begin) === 0) {
- return whole.substr(begin.length);
+function stripBaseUrl(base, url) {
+ if (startsWith(url, base)) {
+ return url.substr(base.length);
}
}
@@ -11744,7 +12875,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
* @private
*/
this.$$parse = function(url) {
- var pathUrl = beginsWith(appBaseNoFile, url);
+ var pathUrl = stripBaseUrl(appBaseNoFile, url);
if (!isString(pathUrl)) {
throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url,
appBaseNoFile);
@@ -11781,14 +12912,14 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
- if (isDefined(appUrl = beginsWith(appBase, url))) {
+ if (isDefined(appUrl = stripBaseUrl(appBase, url))) {
prevAppUrl = appUrl;
- if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) {
- rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
+ if (isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
+ rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
- } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) {
+ } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
} else if (appBaseNoFile == url + '/') {
rewrittenUrl = appBaseNoFile;
@@ -11822,14 +12953,14 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
* @private
*/
this.$$parse = function(url) {
- var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
+ var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url);
var withoutHashUrl;
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
// The rest of the url starts with a hash so we have
// got either a hashbang path or a plain hash fragment
- withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl);
+ withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
// There was no hashbang prefix so we just have a hash fragment
withoutHashUrl = withoutBaseUrl;
@@ -11877,7 +13008,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
var firstPathSegmentMatch;
//Get the relative path from the input URL.
- if (url.indexOf(base) === 0) {
+ if (startsWith(url, base)) {
url = url.replace(base, '');
}
@@ -11940,7 +13071,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
if (appBase == stripHash(url)) {
rewrittenUrl = url;
- } else if ((appUrl = beginsWith(appBaseNoFile, url))) {
+ } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
} else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
@@ -11966,6 +13097,12 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var locationPrototype = {
/**
+ * Ensure absolute url is initialized.
+ * @private
+ */
+ $$absUrl:'',
+
+ /**
* Are we in html5 mode?
* @private
*/
@@ -12122,7 +13259,7 @@ var locationPrototype = {
* ```
*
* @param {(string|number)=} path New path
- * @return {string} path
+ * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter
*/
path: locationGetterSetter('$$path', function(path) {
path = path !== null ? path.toString() : '';
@@ -12548,7 +13685,7 @@ function $LocationProvider() {
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
- if (isUndefined(beginsWith(appBaseNoFile, newUrl))) {
+ if (isUndefined(stripBaseUrl(appBaseNoFile, newUrl))) {
// If we are navigating outside of the app then force a reload
$window.location.href = newUrl;
return;
@@ -12841,23 +13978,22 @@ function ensureSafeMemberName(name, fullExpression) {
return name;
}
-function getStringValue(name, fullExpression) {
- // From the JavaScript docs:
+function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
// into a string via the toString method.
+ // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names
//
- // So, to ensure that we are checking the same `name` that JavaScript would use,
- // we cast it to a string, if possible.
- // Doing `name + ''` can cause a repl error if the result to `toString` is not a string,
- // this is, this will handle objects that misbehave.
- name = name + '';
- if (!isString(name)) {
- throw $parseMinErr('iseccst',
- 'Cannot convert object to primitive value! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
+ // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it
+ // to a string. It's not always possible. If `name` is an object and its `toString` method is
+ // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown:
+ //
+ // TypeError: Cannot convert object to primitive value
+ //
+ // For performance reasons, we don't catch this error here and allow it to propagate up the call
+ // stack. Note that you'll get the same error in JavaScript if you try to access a property using
+ // such a 'broken' object as a key.
+ return name + '';
}
function ensureSafeObject(obj, fullExpression) {
@@ -12944,7 +14080,7 @@ Lexer.prototype = {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
- } else if (this.isIdent(ch)) {
+ } else if (this.isIdentifierStart(this.peekMultichar())) {
this.readIdent();
} else if (this.is(ch, '(){}[].,;:?')) {
this.tokens.push({index: this.index, text: ch});
@@ -12988,12 +14124,49 @@ Lexer.prototype = {
ch === '\n' || ch === '\v' || ch === '\u00A0');
},
- isIdent: function(ch) {
+ isIdentifierStart: function(ch) {
+ return this.options.isIdentifierStart ?
+ this.options.isIdentifierStart(ch, this.codePointAt(ch)) :
+ this.isValidIdentifierStart(ch);
+ },
+
+ isValidIdentifierStart: function(ch) {
return ('a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' === ch || ch === '$');
},
+ isIdentifierContinue: function(ch) {
+ return this.options.isIdentifierContinue ?
+ this.options.isIdentifierContinue(ch, this.codePointAt(ch)) :
+ this.isValidIdentifierContinue(ch);
+ },
+
+ isValidIdentifierContinue: function(ch, cp) {
+ return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch);
+ },
+
+ codePointAt: function(ch) {
+ if (ch.length === 1) return ch.charCodeAt(0);
+ /*jshint bitwise: false*/
+ return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
+ /*jshint bitwise: true*/
+ },
+
+ peekMultichar: function() {
+ var ch = this.text.charAt(this.index);
+ var peek = this.peek();
+ if (!peek) {
+ return ch;
+ }
+ var cp1 = ch.charCodeAt(0);
+ var cp2 = peek.charCodeAt(0);
+ if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) {
+ return ch + peek;
+ }
+ return ch;
+ },
+
isExpOperator: function(ch) {
return (ch === '-' || ch === '+' || this.isNumber(ch));
},
@@ -13042,12 +14215,13 @@ Lexer.prototype = {
readIdent: function() {
var start = this.index;
+ this.index += this.peekMultichar().length;
while (this.index < this.text.length) {
- var ch = this.text.charAt(this.index);
- if (!(this.isIdent(ch) || this.isNumber(ch))) {
+ var ch = this.peekMultichar();
+ if (!this.isIdentifierContinue(ch)) {
break;
}
- this.index++;
+ this.index += ch.length;
}
this.tokens.push({
index: start,
@@ -13118,6 +14292,7 @@ AST.ArrayExpression = 'ArrayExpression';
AST.Property = 'Property';
AST.ObjectExpression = 'ObjectExpression';
AST.ThisExpression = 'ThisExpression';
+AST.LocalsExpression = 'LocalsExpression';
// Internal use only
AST.NGValueParameter = 'NGValueParameter';
@@ -13256,8 +14431,10 @@ AST.prototype = {
primary = this.arrayDeclaration();
} else if (this.expect('{')) {
primary = this.object();
- } else if (this.constants.hasOwnProperty(this.peek().text)) {
- primary = copy(this.constants[this.consume().text]);
+ } else if (this.selfReferential.hasOwnProperty(this.peek().text)) {
+ primary = copy(this.selfReferential[this.consume().text]);
+ } else if (this.options.literals.hasOwnProperty(this.peek().text)) {
+ primary = { type: AST.Literal, value: this.options.literals[this.consume().text]};
} else if (this.peek().identifier) {
primary = this.identifier();
} else if (this.peek().constant) {
@@ -13298,7 +14475,7 @@ AST.prototype = {
var args = [];
if (this.peekToken().text !== ')') {
do {
- args.push(this.expression());
+ args.push(this.filterChain());
} while (this.expect(','));
}
return args;
@@ -13344,13 +14521,28 @@ AST.prototype = {
property = {type: AST.Property, kind: 'init'};
if (this.peek().constant) {
property.key = this.constant();
+ property.computed = false;
+ this.consume(':');
+ property.value = this.expression();
} else if (this.peek().identifier) {
property.key = this.identifier();
+ property.computed = false;
+ if (this.peek(':')) {
+ this.consume(':');
+ property.value = this.expression();
+ } else {
+ property.value = property.key;
+ }
+ } else if (this.peek('[')) {
+ this.consume('[');
+ property.key = this.expression();
+ this.consume(']');
+ property.computed = true;
+ this.consume(':');
+ property.value = this.expression();
} else {
this.throwError("invalid key", this.peek());
}
- this.consume(':');
- property.value = this.expression();
properties.push(property);
} while (this.expect(','));
}
@@ -13409,16 +14601,9 @@ AST.prototype = {
return false;
},
-
- /* `undefined` is not a constant, it is an identifier,
- * but using it as an identifier is not supported
- */
- constants: {
- 'true': { type: AST.Literal, value: true },
- 'false': { type: AST.Literal, value: false },
- 'null': { type: AST.Literal, value: null },
- 'undefined': {type: AST.Literal, value: undefined },
- 'this': {type: AST.ThisExpression }
+ selfReferential: {
+ 'this': {type: AST.ThisExpression },
+ '$locals': {type: AST.LocalsExpression }
}
};
@@ -13526,7 +14711,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
argsToWatch = [];
forEach(ast.properties, function(property) {
findConstantAndWatchExpressions(property.value, $filter);
- allConstants = allConstants && property.value.constant;
+ allConstants = allConstants && property.value.constant && !property.computed;
if (!property.value.constant) {
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
}
@@ -13538,6 +14723,10 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.constant = false;
ast.toWatch = [];
break;
+ case AST.LocalsExpression:
+ ast.constant = false;
+ ast.toWatch = [];
+ break;
}
}
@@ -13694,7 +14883,7 @@ ASTCompiler.prototype = {
},
recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
- var left, right, self = this, args, expression;
+ var left, right, self = this, args, expression, computed;
recursionFn = recursionFn || noop;
if (!skipWatchIdCheck && isDefined(ast.watchId)) {
intoId = intoId || this.nextId();
@@ -13891,22 +15080,50 @@ ASTCompiler.prototype = {
break;
case AST.ObjectExpression:
args = [];
+ computed = false;
forEach(ast.properties, function(property) {
- self.recurse(property.value, self.nextId(), undefined, function(expr) {
- args.push(self.escape(
- property.key.type === AST.Identifier ? property.key.name :
- ('' + property.key.value)) +
- ':' + expr);
- });
+ if (property.computed) {
+ computed = true;
+ }
});
- expression = '{' + args.join(',') + '}';
- this.assign(intoId, expression);
- recursionFn(expression);
+ if (computed) {
+ intoId = intoId || this.nextId();
+ this.assign(intoId, '{}');
+ forEach(ast.properties, function(property) {
+ if (property.computed) {
+ left = self.nextId();
+ self.recurse(property.key, left);
+ } else {
+ left = property.key.type === AST.Identifier ?
+ property.key.name :
+ ('' + property.key.value);
+ }
+ right = self.nextId();
+ self.recurse(property.value, right);
+ self.assign(self.member(intoId, left, property.computed), right);
+ });
+ } else {
+ forEach(ast.properties, function(property) {
+ self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) {
+ args.push(self.escape(
+ property.key.type === AST.Identifier ? property.key.name :
+ ('' + property.key.value)) +
+ ':' + expr);
+ });
+ });
+ expression = '{' + args.join(',') + '}';
+ this.assign(intoId, expression);
+ }
+ recursionFn(intoId || expression);
break;
case AST.ThisExpression:
this.assign(intoId, 's');
recursionFn('s');
break;
+ case AST.LocalsExpression:
+ this.assign(intoId, 'l');
+ recursionFn('l');
+ break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
recursionFn('v');
@@ -13973,7 +15190,13 @@ ASTCompiler.prototype = {
},
nonComputedMember: function(left, right) {
- return left + '.' + right;
+ var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/;
+ var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
+ if (SAFE_IDENTIFIER.test(right)) {
+ return left + '.' + right;
+ } else {
+ return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]';
+ }
},
computedMember: function(left, right) {
@@ -14014,7 +15237,7 @@ ASTCompiler.prototype = {
},
getStringValue: function(item) {
- this.assign(item, 'getStringValue(' + item + ',text)');
+ this.assign(item, 'getStringValue(' + item + ')');
},
ensureSafeAssignContext: function(item) {
@@ -14098,7 +15321,7 @@ ASTInterpreter.prototype = {
forEach(ast.body, function(expression) {
expressions.push(self.recurse(expression.expression));
});
- var fn = ast.body.length === 0 ? function() {} :
+ var fn = ast.body.length === 0 ? noop :
ast.body.length === 1 ? expressions[0] :
function(scope, locals) {
var lastValue;
@@ -14217,16 +15440,28 @@ ASTInterpreter.prototype = {
case AST.ObjectExpression:
args = [];
forEach(ast.properties, function(property) {
- args.push({key: property.key.type === AST.Identifier ?
- property.key.name :
- ('' + property.key.value),
- value: self.recurse(property.value)
- });
+ if (property.computed) {
+ args.push({key: self.recurse(property.key),
+ computed: true,
+ value: self.recurse(property.value)
+ });
+ } else {
+ args.push({key: property.key.type === AST.Identifier ?
+ property.key.name :
+ ('' + property.key.value),
+ computed: false,
+ value: self.recurse(property.value)
+ });
+ }
});
return function(scope, locals, assign, inputs) {
var value = {};
for (var i = 0; i < args.length; ++i) {
- value[args[i].key] = args[i].value(scope, locals, assign, inputs);
+ if (args[i].computed) {
+ value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs);
+ } else {
+ value[args[i].key] = args[i].value(scope, locals, assign, inputs);
+ }
}
return context ? {value: value} : value;
};
@@ -14234,8 +15469,12 @@ ASTInterpreter.prototype = {
return function(scope) {
return context ? {value: scope} : scope;
};
+ case AST.LocalsExpression:
+ return function(scope, locals) {
+ return context ? {value: locals} : locals;
+ };
case AST.NGValueParameter:
- return function(scope, locals, assign, inputs) {
+ return function(scope, locals, assign) {
return context ? {value: assign} : assign;
};
}
@@ -14449,7 +15688,7 @@ var Parser = function(lexer, $filter, options) {
this.lexer = lexer;
this.$filter = $filter;
this.options = options;
- this.ast = new AST(this.lexer);
+ this.ast = new AST(lexer, options);
this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
new ASTCompiler(this.ast, $filter);
};
@@ -14526,16 +15765,73 @@ function getValueOf(value) {
function $ParseProvider() {
var cacheDefault = createMap();
var cacheExpensive = createMap();
+ var literals = {
+ 'true': true,
+ 'false': false,
+ 'null': null,
+ 'undefined': undefined
+ };
+ var identStart, identContinue;
+
+ /**
+ * @ngdoc method
+ * @name $parseProvider#addLiteral
+ * @description
+ *
+ * Configure $parse service to add literal values that will be present as literal at expressions.
+ *
+ * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name.
+ * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`.
+ *
+ **/
+ this.addLiteral = function(literalName, literalValue) {
+ literals[literalName] = literalValue;
+ };
+
+ /**
+ * @ngdoc method
+ * @name $parseProvider#setIdentifierFns
+ * @description
+ *
+ * Allows defining the set of characters that are allowed in Angular expressions. The function
+ * `identifierStart` will get called to know if a given character is a valid character to be the
+ * first character for an identifier. The function `identifierContinue` will get called to know if
+ * a given character is a valid character to be a follow-up identifier character. The functions
+ * `identifierStart` and `identifierContinue` will receive as arguments the single character to be
+ * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in
+ * mind that the `string` parameter can be two characters long depending on the character
+ * representation. It is expected for the function to return `true` or `false`, whether that
+ * character is allowed or not.
+ *
+ * Since this function will be called extensivelly, keep the implementation of these functions fast,
+ * as the performance of these functions have a direct impact on the expressions parsing speed.
+ *
+ * @param {function=} identifierStart The function that will decide whether the given character is
+ * a valid identifier start character.
+ * @param {function=} identifierContinue The function that will decide whether the given character is
+ * a valid identifier continue character.
+ */
+ this.setIdentifierFns = function(identifierStart, identifierContinue) {
+ identStart = identifierStart;
+ identContinue = identifierContinue;
+ return this;
+ };
this.$get = ['$filter', function($filter) {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
- expensiveChecks: false
+ expensiveChecks: false,
+ literals: copy(literals),
+ isIdentifierStart: isFunction(identStart) && identStart,
+ isIdentifierContinue: isFunction(identContinue) && identContinue
},
$parseOptionsExpensive = {
csp: noUnsafeEval,
- expensiveChecks: true
+ expensiveChecks: true,
+ literals: copy(literals),
+ isIdentifierStart: isFunction(identStart) && identStart,
+ isIdentifierContinue: isFunction(identContinue) && identContinue
};
var runningChecksEnabled = false;
@@ -14729,13 +16025,9 @@ function $ParseProvider() {
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch;
return unwatch = scope.$watch(function constantWatch(scope) {
- return parsedExpression(scope);
- }, function constantListener(value, old, scope) {
- if (isFunction(listener)) {
- listener.apply(this, arguments);
- }
unwatch();
- }, objectEquality);
+ return parsedExpression(scope);
+ }, listener, objectEquality);
}
function addInterceptor(parsedExpression, interceptorFn) {
@@ -14788,15 +16080,15 @@ function $ParseProvider() {
* [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
- * implementations, and the other which resembles ES6 promises to some degree.
+ * implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
* # $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
- * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony,
+ * function as the first argument. This is similar to the native Promise implementation from ES6,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
- * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are
+ * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
* available yet.
*
* It can be used like so:
@@ -14828,7 +16120,7 @@ function $ParseProvider() {
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
- * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise.
+ * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise.
*
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
@@ -14910,7 +16202,7 @@ function $ParseProvider() {
*
* **Methods**
*
- * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or
+ * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or
* will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously
* as soon as the result is available. The callbacks are called with a single argument: the result
* or rejection reason. Additionally, the notify callback may be called zero or more times to
@@ -14921,7 +16213,8 @@ function $ParseProvider() {
* with the value which is resolved in that promise using
* [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
* It also notifies via the return value of the `notifyCallback` method. The promise cannot be
- * resolved or rejected from the notifyCallback method.
+ * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback
+ * arguments are optional.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
@@ -14961,7 +16254,7 @@ function $ParseProvider() {
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
- * # Testing
+ * # Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
@@ -15018,18 +16311,6 @@ function $$QProvider() {
*/
function qFactory(nextTick, exceptionHandler) {
var $qMinErr = minErr('$q', TypeError);
- function callOnce(self, resolveFn, rejectFn) {
- var called = false;
- function wrap(fn) {
- return function(value) {
- if (called) return;
- called = true;
- fn.call(self, value);
- };
- }
-
- return [wrap(resolveFn), wrap(rejectFn)];
- }
/**
* @ngdoc method
@@ -15042,7 +16323,12 @@ function qFactory(nextTick, exceptionHandler) {
* @returns {Deferred} Returns a new instance of deferred.
*/
var defer = function() {
- return new Deferred();
+ var d = new Deferred();
+ //Necessary to support unbound execution :/
+ d.resolve = simpleBind(d, d.resolve);
+ d.reject = simpleBind(d, d.reject);
+ d.notify = simpleBind(d, d.notify);
+ return d;
};
function Promise() {
@@ -15115,10 +16401,6 @@ function qFactory(nextTick, exceptionHandler) {
function Deferred() {
this.promise = new Promise();
- //Necessary to support unbound execution :/
- this.resolve = simpleBind(this, this.resolve);
- this.reject = simpleBind(this, this.reject);
- this.notify = simpleBind(this, this.notify);
}
extend(Deferred.prototype, {
@@ -15136,23 +16418,34 @@ function qFactory(nextTick, exceptionHandler) {
},
$$resolve: function(val) {
- var then, fns;
-
- fns = callOnce(this, this.$$resolve, this.$$reject);
+ var then;
+ var that = this;
+ var done = false;
try {
if ((isObject(val) || isFunction(val))) then = val && val.then;
if (isFunction(then)) {
this.promise.$$state.status = -1;
- then.call(val, fns[0], fns[1], this.notify);
+ then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify));
} else {
this.promise.$$state.value = val;
this.promise.$$state.status = 1;
scheduleProcessQueue(this.promise.$$state);
}
} catch (e) {
- fns[1](e);
+ rejectPromise(e);
exceptionHandler(e);
}
+
+ function resolvePromise(val) {
+ if (done) return;
+ done = true;
+ that.$$resolve(val);
+ }
+ function rejectPromise(val) {
+ if (done) return;
+ done = true;
+ that.$$reject(val);
+ }
},
reject: function(reason) {
@@ -15336,16 +16629,35 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
}
+ /**
+ * @ngdoc method
+ * @name $q#race
+ * @kind function
+ *
+ * @description
+ * Returns a promise that resolves or rejects as soon as one of those promises
+ * resolves or rejects, with the value or reason from that promise.
+ *
+ * @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
+ * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises`
+ * resolves or rejects, with the value or reason from that promise.
+ */
+
+ function race(promises) {
+ var deferred = defer();
+
+ forEach(promises, function(promise) {
+ when(promise).then(deferred.resolve, deferred.reject);
+ });
+
+ return deferred.promise;
+ }
+
var $Q = function Q(resolver) {
if (!isFunction(resolver)) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}
- if (!(this instanceof Q)) {
- // More useful when $Q is the Promise itself.
- return new Q(resolver);
- }
-
var deferred = new Deferred();
function resolveFn(value) {
@@ -15361,11 +16673,16 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
};
+ // Let's make the instanceof operator work for promises, so that
+ // `new $q(fn) instanceof $q` would evaluate to true.
+ $Q.prototype = Promise.prototype;
+
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
$Q.resolve = resolve;
$Q.all = all;
+ $Q.race = race;
return $Q;
}
@@ -15494,8 +16811,8 @@ function $RootScopeProvider() {
return ChildScope;
}
- this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
- function($injector, $exceptionHandler, $parse, $browser) {
+ this.$get = ['$exceptionHandler', '$parse', '$browser',
+ function($exceptionHandler, $parse, $browser) {
function destroyChildScope($event) {
$event.currentScope.$$destroyed = true;
@@ -15779,7 +17096,7 @@ function $RootScopeProvider() {
* - `newVal` contains the current value of the `watchExpression`
* - `oldVal` contains the previous value of the `watchExpression`
* - `scope` refers to the current scope
- * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of
+ * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of
* comparing for reference equality.
* @returns {function()} Returns a deregistration function for this listener.
*/
@@ -16150,7 +17467,7 @@ function $RootScopeProvider() {
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
- logIdx, logMsg, asyncTask;
+ logIdx, asyncTask;
beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
@@ -16169,15 +17486,19 @@ function $RootScopeProvider() {
dirty = false;
current = target;
- while (asyncQueue.length) {
+ // It's safe for asyncQueuePosition to be a local variable here because this loop can't
+ // be reentered recursively. Calling $digest from a function passed to $applyAsync would
+ // lead to a '$digest already in progress' error.
+ for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
try {
- asyncTask = asyncQueue.shift();
+ asyncTask = asyncQueue[asyncQueuePosition];
asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
lastDirtyWatch = null;
}
+ asyncQueue.length = 0;
traverseScopesLoop:
do { // "traverse the scopes" loop
@@ -16248,13 +17569,15 @@ function $RootScopeProvider() {
clearPhase();
- while (postDigestQueue.length) {
+ // postDigestQueuePosition isn't local here because this loop can be reentered recursively.
+ while (postDigestQueuePosition < postDigestQueue.length) {
try {
- postDigestQueue.shift()();
+ postDigestQueue[postDigestQueuePosition++]();
} catch (e) {
$exceptionHandler(e);
}
}
+ postDigestQueue.length = postDigestQueuePosition = 0;
},
@@ -16709,6 +18032,8 @@ function $RootScopeProvider() {
var postDigestQueue = $rootScope.$$postDigestQueue = [];
var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];
+ var postDigestQueuePosition = 0;
+
return $rootScope;
@@ -17196,6 +18521,11 @@ function $SceDelegateProvider() {
* returns the originally supplied value if the queried context type is a supertype of the
* created type. If this condition isn't satisfied, throws an exception.
*
+ * <div class="alert alert-danger">
+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ * </div>
+ *
* @param {string} type The kind of context in which this value is to be used.
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} call.
@@ -17272,7 +18602,7 @@ function $SceDelegateProvider() {
* You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>`
* to the top of your HTML document.
*
- * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for
+ * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for
* security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
*
* Here's an example of a binding in a privileged context:
@@ -17930,6 +19260,10 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
+ // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
+ // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
+ isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
+ hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
@@ -17945,7 +19279,7 @@ function $SnifferProvider() {
for (var prop in bodyStyle) {
if (match = vendorRegex.exec(prop)) {
vendorPrefix = match[0];
- vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
+ vendorPrefix = vendorPrefix[0].toUpperCase() + vendorPrefix.substr(1);
break;
}
}
@@ -17974,7 +19308,7 @@ function $SnifferProvider() {
// so let's not use the history API also
// We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
// jshint -W018
- history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
+ history: !!(hasHistoryPushState && !(android < 4) && !boxee),
// jshint +W018
hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
@@ -18000,29 +19334,66 @@ function $SnifferProvider() {
}];
}
-var $compileMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$compile');
/**
- * @ngdoc service
- * @name $templateRequest
- *
+ * @ngdoc provider
+ * @name $templateRequestProvider
* @description
- * The `$templateRequest` service runs security checks then downloads the provided template using
- * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
- * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
- * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
- * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
- * when `tpl` is of type string and `$templateCache` has the matching entry.
- *
- * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
- * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
- *
- * @return {Promise} a promise for the HTTP response data of the given URL.
+ * Used to configure the options passed to the {@link $http} service when making a template request.
*
- * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
+ * For example, it can be used for specifying the "Accept" header that is sent to the server, when
+ * requesting a template.
*/
function $TemplateRequestProvider() {
+
+ var httpOptions;
+
+ /**
+ * @ngdoc method
+ * @name $templateRequestProvider#httpOptions
+ * @description
+ * The options to be passed to the {@link $http} service when making the request.
+ * You can use this to override options such as the "Accept" header for template requests.
+ *
+ * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the
+ * options if not overridden here.
+ *
+ * @param {string=} value new value for the {@link $http} options.
+ * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter.
+ */
+ this.httpOptions = function(val) {
+ if (val) {
+ httpOptions = val;
+ return this;
+ }
+ return httpOptions;
+ };
+
+ /**
+ * @ngdoc service
+ * @name $templateRequest
+ *
+ * @description
+ * The `$templateRequest` service runs security checks then downloads the provided template using
+ * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
+ * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
+ * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
+ * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
+ * when `tpl` is of type string and `$templateCache` has the matching entry.
+ *
+ * If you want to pass custom options to the `$http` service, such as setting the Accept header you
+ * can configure this via {@link $templateRequestProvider#httpOptions}.
+ *
+ * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
+ * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
+ *
+ * @return {Promise} a promise for the HTTP response data of the given URL.
+ *
+ * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
+ */
this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
+
function handleRequestFn(tpl, ignoreRequestError) {
handleRequestFn.totalPendingRequests++;
@@ -18031,7 +19402,7 @@ function $TemplateRequestProvider() {
// are included in there. This also makes Angular accept any script
// directive, no matter its name. However, we still need to unwrap trusted
// types.
- if (!isString(tpl) || !$templateCache.get(tpl)) {
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
tpl = $sce.getTrustedResourceUrl(tpl);
}
@@ -18045,12 +19416,10 @@ function $TemplateRequestProvider() {
transformResponse = null;
}
- var httpOptions = {
- cache: $templateCache,
- transformResponse: transformResponse
- };
-
- return $http.get(tpl, httpOptions)
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
['finally'](function() {
handleRequestFn.totalPendingRequests--;
})
@@ -18061,7 +19430,7 @@ function $TemplateRequestProvider() {
function handleError(resp) {
if (!ignoreRequestError) {
- throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
+ throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
tpl, resp.status, resp.statusText);
}
return $q.reject(resp);
@@ -18291,7 +19660,7 @@ function $TimeoutProvider() {
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
-var urlParsingNode = document.createElement("a");
+var urlParsingNode = window.document.createElement("a");
var originUrl = urlResolve(window.location.href);
@@ -18665,10 +20034,11 @@ function $FilterProvider($provide) {
* - `Object`: A pattern object can be used to filter specific properties on objects contained
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
* which have property `name` containing "M" and property `phone` containing "1". A special
- * property name `$` can be used (as in `{$:"text"}`) to accept a match against any
- * property of the object or its nested object properties. That's equivalent to the simple
- * substring match with a `string` as described above. The predicate can be negated by prefixing
- * the string with `!`.
+ * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
+ * against any property of the object or its nested object properties. That's equivalent to the
+ * simple substring match with a `string` as described above. The special property name can be
+ * overwritten, using the `anyPropertyKey` parameter.
+ * The predicate can be negated by prefixing the string with `!`.
* For example `{name: "!M"}` predicate will return an array of items which have property `name`
* not containing "M".
*
@@ -18702,6 +20072,9 @@ function $FilterProvider($provide) {
* Primitive values are converted to strings. Objects are not compared against primitives,
* unless they have a custom `toString` method (e.g. `Date` objects).
*
+ * @param {string=} anyPropertyKey The special property name that matches against any property.
+ * By default `$`.
+ *
* @example
<example>
<file name="index.html">
@@ -18770,8 +20143,9 @@ function $FilterProvider($provide) {
</file>
</example>
*/
+
function filterFilter() {
- return function(array, expression, comparator) {
+ return function(array, expression, comparator, anyPropertyKey) {
if (!isArrayLike(array)) {
if (array == null) {
return array;
@@ -18780,6 +20154,7 @@ function filterFilter() {
}
}
+ anyPropertyKey = anyPropertyKey || '$';
var expressionType = getTypeForFilter(expression);
var predicateFn;
var matchAgainstAnyProp;
@@ -18796,7 +20171,7 @@ function filterFilter() {
//jshint -W086
case 'object':
//jshint +W086
- predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp);
+ predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
return array;
@@ -18807,8 +20182,8 @@ function filterFilter() {
}
// Helper functions for `filterFilter`
-function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
- var shouldMatchPrimitives = isObject(expression) && ('$' in expression);
+function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) {
+ var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression);
var predicateFn;
if (comparator === true) {
@@ -18836,25 +20211,25 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
predicateFn = function(item) {
if (shouldMatchPrimitives && !isObject(item)) {
- return deepCompare(item, expression.$, comparator, false);
+ return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false);
}
- return deepCompare(item, expression, comparator, matchAgainstAnyProp);
+ return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp);
};
return predicateFn;
}
-function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
+function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) {
var actualType = getTypeForFilter(actual);
var expectedType = getTypeForFilter(expected);
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
- return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
+ return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp);
} else if (isArray(actual)) {
// In case `actual` is an array, consider it a match
// if ANY of it's items matches `expected`
return actual.some(function(item) {
- return deepCompare(item, expected, comparator, matchAgainstAnyProp);
+ return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp);
});
}
@@ -18863,11 +20238,11 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
var key;
if (matchAgainstAnyProp) {
for (key in actual) {
- if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) {
+ if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
return true;
}
}
- return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false);
+ return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false);
} else if (expectedType === 'object') {
for (key in expected) {
var expectedVal = expected[key];
@@ -18875,9 +20250,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
continue;
}
- var matchAnyProperty = key === '$';
+ var matchAnyProperty = key === anyPropertyKey;
var actualVal = matchAnyProperty ? actual : actual[key];
- if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) {
+ if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) {
return false;
}
}
@@ -18991,7 +20366,9 @@ function currencyFilter($locale) {
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
- * @returns {string} Number rounded to fractionSize and places a “,” after each third digit.
+ * @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current
+ * locale (e.g., in the en_US locale it will have "." as the decimal separator and
+ * include "," group separators after each third digit).
*
* @example
<example module="numberFilterExample">
@@ -19119,18 +20496,37 @@ function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
var digit = digits[roundAt];
if (roundAt > 0) {
- digits.splice(roundAt);
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
+
+ // Set non-fractional digits beyond `roundAt` to 0
+ for (var j = roundAt; j < digits.length; j++) {
+ digits[j] = 0;
+ }
} else {
// We rounded to zero so reset the parsedNumber
+ fractionLen = Math.max(0, fractionLen);
parsedNumber.i = 1;
- digits.length = roundAt = fractionSize + 1;
- for (var i=0; i < roundAt; i++) digits[i] = 0;
+ digits.length = Math.max(1, roundAt = fractionSize + 1);
+ digits[0] = 0;
+ for (var i = 1; i < roundAt; i++) digits[i] = 0;
}
- if (digit >= 5) digits[roundAt - 1]++;
+ if (digit >= 5) {
+ if (roundAt - 1 < 0) {
+ for (var k = 0; k > roundAt; k--) {
+ digits.unshift(0);
+ parsedNumber.i++;
+ }
+ digits.unshift(1);
+ parsedNumber.i++;
+ } else {
+ digits[roundAt - 1]++;
+ }
+ }
// Pad out with zeros to get the required fraction length
- for (; fractionLen < fractionSize; fractionLen++) digits.push(0);
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
// Do any carrying, e.g. a digit was rounded up to 10
@@ -19194,7 +20590,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
// extract decimals digits
if (integerLen > 0) {
- decimals = digits.splice(integerLen);
+ decimals = digits.splice(integerLen, digits.length);
} else {
decimals = digits;
digits = [0];
@@ -19202,11 +20598,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
// format the integer digits with grouping separators
var groups = [];
- if (digits.length > pattern.lgSize) {
- groups.unshift(digits.splice(-pattern.lgSize).join(''));
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
}
while (digits.length > pattern.gSize) {
- groups.unshift(digits.splice(-pattern.gSize).join(''));
+ groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
}
if (digits.length) {
groups.unshift(digits.join(''));
@@ -19229,11 +20625,15 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
}
}
-function padNumber(num, digits, trim) {
+function padNumber(num, digits, trim, negWrap) {
var neg = '';
- if (num < 0) {
- neg = '-';
- num = -num;
+ if (num < 0 || (negWrap && num <= 0)) {
+ if (negWrap) {
+ num = -num + 1;
+ } else {
+ num = -num;
+ neg = '-';
+ }
}
num = '' + num;
while (num.length < digits) num = ZERO_CHAR + num;
@@ -19244,7 +20644,7 @@ function padNumber(num, digits, trim) {
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
@@ -19252,14 +20652,15 @@ function dateGetter(name, size, offset, trim) {
value += offset;
}
if (value === 0 && offset == -12) value = 12;
- return padNumber(value, size, trim);
+ return padNumber(value, size, trim, negWrap);
};
}
-function dateStrGetter(name, shortForm) {
+function dateStrGetter(name, shortForm, standAlone) {
return function(date, formats) {
var value = date['get' + name]();
- var get = uppercase(shortForm ? ('SHORT' + name) : name);
+ var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
+ var get = uppercase(propPrefix + name);
return formats[get][value];
};
@@ -19314,13 +20715,14 @@ function longEraGetter(date, formats) {
}
var DATE_FORMATS = {
- yyyy: dateGetter('FullYear', 4),
- yy: dateGetter('FullYear', 2, 0, true),
- y: dateGetter('FullYear', 1),
+ yyyy: dateGetter('FullYear', 4, 0, false, true),
+ yy: dateGetter('FullYear', 2, 0, true, true),
+ y: dateGetter('FullYear', 1, 0, false, true),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
+ LLLL: dateStrGetter('Month', false, true),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
@@ -19346,7 +20748,7 @@ var DATE_FORMATS = {
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;
/**
@@ -19366,6 +20768,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
* * `'MMM'`: Month in year (Jan-Dec)
* * `'MM'`: Month in year, padded (01-12)
* * `'M'`: Month in year (1-12)
+ * * `'LLLL'`: Stand-alone month in year (January-December)
* * `'dd'`: Day in month, padded (01-31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
@@ -19587,21 +20990,22 @@ var uppercaseFilter = valueFn(uppercase);
* @kind function
*
* @description
- * Creates a new array or string containing only a specified number of elements. The elements
- * are taken from either the beginning or the end of the source array, string or number, as specified by
- * the value and sign (positive or negative) of `limit`. If a number is used as input, it is
- * converted to a string.
- *
- * @param {Array|string|number} input Source array, string or number to be limited.
- * @param {string|number} limit The length of the returned array or string. If the `limit` number
+ * Creates a new array or string containing only a specified number of elements. The elements are
+ * taken from either the beginning or the end of the source array, string or number, as specified by
+ * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported
+ * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input,
+ * it is converted to a string.
+ *
+ * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited.
+ * @param {string|number} limit - The length of the returned array or string. If the `limit` number
* is positive, `limit` number of items from the beginning of the source array/string are copied.
* If the number is negative, `limit` number of items from the end of the source array/string
* are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
* the input will be returned unchanged.
- * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin`
- * indicates an offset from the end of `input`. Defaults to `0`.
- * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
- * had less than `limit` elements.
+ * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index,
+ * `begin` indicates an offset from the end of `input`. Defaults to `0`.
+ * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had
+ * less than `limit` elements.
*
* @example
<example module="limitToExample">
@@ -19689,66 +21093,157 @@ function limitToFilter() {
if (isNaN(limit)) return input;
if (isNumber(input)) input = input.toString();
- if (!isArray(input) && !isString(input)) return input;
+ if (!isArrayLike(input)) return input;
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
if (limit >= 0) {
- return input.slice(begin, begin + limit);
+ return sliceFn(input, begin, begin + limit);
} else {
if (begin === 0) {
- return input.slice(limit, input.length);
+ return sliceFn(input, limit, input.length);
} else {
- return input.slice(Math.max(0, begin + limit), begin);
+ return sliceFn(input, Math.max(0, begin + limit), begin);
}
}
};
}
+function sliceFn(input, begin, end) {
+ if (isString(input)) return input.slice(begin, end);
+
+ return slice.call(input, begin, end);
+}
+
/**
* @ngdoc filter
* @name orderBy
* @kind function
*
* @description
- * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
- * for strings and numerically for numbers. Note: if you notice numbers are not being sorted
- * as expected, make sure they are actually being saved as numbers and not strings.
+ * Returns an array containing the items from the specified `collection`, ordered by a `comparator`
+ * function based on the values computed using the `expression` predicate.
+ *
+ * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in
+ * `[{id: 'bar'}, {id: 'foo'}]`.
+ *
+ * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray,
+ * String, etc).
+ *
+ * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
+ * for the preceeding one. The `expression` is evaluated against each item and the output is used
+ * for comparing with other items.
+ *
+ * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
+ * ascending order.
+ *
+ * The comparison is done using the `comparator` function. If none is specified, a default, built-in
+ * comparator is used (see below for details - in a nutshell, it compares numbers numerically and
+ * strings alphabetically).
+ *
+ * ### Under the hood
+ *
+ * Ordering the specified `collection` happens in two phases:
*
- * @param {Array} array The array to sort.
- * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
- * used by the comparator to determine the order of elements.
+ * 1. All items are passed through the predicate (or predicates), and the returned values are saved
+ * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed
+ * through a predicate that extracts the value of the `label` property, would be transformed to:
+ * ```
+ * {
+ * value: 'foo',
+ * type: 'string',
+ * index: ...
+ * }
+ * ```
+ * 2. The comparator function is used to sort the items, based on the derived values, types and
+ * indices.
+ *
+ * If you use a custom comparator, it will be called with pairs of objects of the form
+ * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal
+ * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the
+ * second, or `1` otherwise.
+ *
+ * In order to ensure that the sorting will be deterministic across platforms, if none of the
+ * specified predicates can distinguish between two items, `orderBy` will automatically introduce a
+ * dummy predicate that returns the item's index as `value`.
+ * (If you are using a custom comparator, make sure it can handle this predicate as well.)
+ *
+ * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
+ * value for an item, `orderBy` will try to convert that object to a primitive value, before passing
+ * it to the comparator. The following rules govern the conversion:
+ *
+ * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be
+ * used instead.<br />
+ * (If the object has a `valueOf()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that
+ * returns a primitive, its return value will be used instead.<br />
+ * (If the object has a `toString()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 3. No conversion; the object itself is used.
+ *
+ * ### The default comparator
+ *
+ * The default, built-in comparator should be sufficient for most usecases. In short, it compares
+ * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
+ * using their index in the original collection, and sorts values of different types by type.
+ *
+ * More specifically, it follows these steps to determine the relative order of items:
+ *
+ * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 2. If both values are of type `string`, compare them alphabetically in a case- and
+ * locale-insensitive way.
+ * 3. If both values are objects, compare their indices instead.
+ * 4. Otherwise, return:
+ * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`).
+ * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator).
+ * - `1`, otherwise.
+ *
+ * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
+ * saved as numbers and not strings.
+ *
+ * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
+ * @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
+ * predicates) to be used by the comparator to determine the order of elements.
*
* Can be one of:
*
- * - `function`: Getter function. The result of this function will be sorted using the
- * `<`, `===`, `>` operator.
- * - `string`: An Angular expression. The result of this expression is used to compare elements
- * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by
- * 3 first characters of a property called `name`). The result of a constant expression
- * is interpreted as a property name to be used in comparisons (for example `"special name"`
- * to sort object by the value of their `special name` property). An expression can be
- * optionally prefixed with `+` or `-` to control ascending or descending sort order
- * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array
- * element itself is used to compare where sorting.
- * - `Array`: An array of function or string predicates. The first predicate in the array
- * is used for sorting, but when two items are equivalent, the next predicate is used.
+ * - `Function`: A getter function. This function will be called with each item as argument and
+ * the return value will be used for sorting.
+ * - `string`: An Angular expression. This expression will be evaluated against each item and the
+ * result will be used for sorting. For example, use `'label'` to sort by a property called
+ * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
+ * property.<br />
+ * (The result of a constant expression is interpreted as a property name to be used for
+ * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a
+ * property called `special name`.)<br />
+ * An expression can be optionally prefixed with `+` or `-` to control the sorting direction,
+ * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided,
+ * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons.
+ * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the
+ * relative order of two items, the next predicate is used as a tie-breaker.
+ *
+ * **Note:** If the predicate is missing or empty then it defaults to `'+'`.
*
- * If the predicate is missing or empty then it defaults to `'+'`.
+ * @param {boolean=} reverse - If `true`, reverse the sorting order.
+ * @param {(Function)=} comparator - The comparator function used to determine the relative order of
+ * value pairs. If omitted, the built-in comparator will be used.
*
- * @param {boolean=} reverse Reverse the order of the array.
- * @returns {Array} Sorted copy of the source array.
+ * @returns {Array} - The sorted array.
*
*
* @example
- * The example below demonstrates a simple ngRepeat, where the data is sorted
- * by age in descending order (predicate is set to `'-age'`).
- * `reverse` is not set, which means it defaults to `false`.
- <example module="orderByExample">
+ * ### Ordering a table with `ngRepeat`
+ *
+ * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by
+ * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means
+ * it defaults to the built-in comparator.
+ *
+ <example name="orderBy-static" module="orderByExample1">
<file name="index.html">
<div ng-controller="ExampleController">
- <table class="friend">
+ <table class="friends">
<tr>
<th>Name</th>
<th>Phone Number</th>
@@ -19763,43 +21258,77 @@ function limitToFilter() {
</div>
</file>
<file name="script.js">
- angular.module('orderByExample', [])
+ angular.module('orderByExample1', [])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
+ $scope.friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
}]);
</file>
+ <file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var names = element.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by age in reverse order', function() {
+ expect(names.get(0).getText()).toBe('Adam');
+ expect(names.get(1).getText()).toBe('Julie');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('John');
+ });
+ </file>
</example>
+ * <hr />
*
- * The predicate and reverse parameters can be controlled dynamically through scope properties,
- * as shown in the next example.
* @example
- <example module="orderByExample">
+ * ### Changing parameters dynamically
+ *
+ * All parameters can be changed dynamically. The next example shows how you can make the columns of
+ * a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
+ *
+ <example name="orderBy-dynamic" module="orderByExample2">
<file name="index.html">
<div ng-controller="ExampleController">
- <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
+ <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
<hr/>
- <button ng-click="predicate=''">Set to unsorted</button>
- <table class="friend">
+ <button ng-click="propertyName = null; reverse = false">Set to unsorted</button>
+ <hr/>
+ <table class="friends">
<tr>
- <th>
- <button ng-click="order('name')">Name</button>
- <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <button ng-click="order('phone')">Phone Number</button>
- <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <button ng-click="order('age')">Age</button>
- <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
- </th>
+ <th>
+ <button ng-click="sortBy('name')">Name</button>
+ <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('age')">Age</button>
+ <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
+ </th>
</tr>
- <tr ng-repeat="friend in friends | orderBy:predicate:reverse">
+ <tr ng-repeat="friend in friends | orderBy:propertyName:reverse">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
@@ -19808,111 +21337,350 @@ function limitToFilter() {
</div>
</file>
<file name="script.js">
- angular.module('orderByExample', [])
+ angular.module('orderByExample2', [])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
- $scope.predicate = 'age';
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
$scope.reverse = true;
- $scope.order = function(predicate) {
- $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
- $scope.predicate = predicate;
+ $scope.friends = friends;
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
};
}]);
- </file>
+ </file>
<file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
.sortorder:after {
- content: '\25b2';
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
}
.sortorder.reverse:after {
- content: '\25bc';
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
}
</file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+ </file>
</example>
+ * <hr />
*
- * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the
- * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the
- * desired parameters.
+ * @example
+ * ### Using `orderBy` inside a controller
*
- * Example:
+ * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and
+ * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory
+ * and retrieve the `orderBy` filter with `$filter('orderBy')`.)
*
- * @example
- <example module="orderByExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
- <table class="friend">
- <tr>
- <th>
- <button ng-click="order('name')">Name</button>
- <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <button ng-click="order('phone')">Phone Number</button>
- <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <button ng-click="order('age')">Age</button>
- <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
- </th>
- </tr>
- <tr ng-repeat="friend in friends">
- <td>{{friend.name}}</td>
- <td>{{friend.phone}}</td>
- <td>{{friend.age}}</td>
- </tr>
- </table>
- </div>
- </file>
+ <example name="orderBy-call-manually" module="orderByExample3">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
+ <hr/>
+ <button ng-click="sortBy(null)">Set to unsorted</button>
+ <hr/>
+ <table class="friends">
+ <tr>
+ <th>
+ <button ng-click="sortBy('name')">Name</button>
+ <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('age')">Age</button>
+ <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
+ </th>
+ </tr>
+ <tr ng-repeat="friend in friends">
+ <td>{{friend.name}}</td>
+ <td>{{friend.phone}}</td>
+ <td>{{friend.age}}</td>
+ </tr>
+ </table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('orderByExample3', [])
+ .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
- <file name="script.js">
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) {
- var orderBy = $filter('orderBy');
- $scope.friends = [
- { name: 'John', phone: '555-1212', age: 10 },
- { name: 'Mary', phone: '555-9876', age: 19 },
- { name: 'Mike', phone: '555-4321', age: 21 },
- { name: 'Adam', phone: '555-5678', age: 35 },
- { name: 'Julie', phone: '555-8765', age: 29 }
- ];
- $scope.order = function(predicate) {
- $scope.predicate = predicate;
- $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
- $scope.friends = orderBy($scope.friends, predicate, $scope.reverse);
- };
- $scope.order('age', true);
- }]);
- </file>
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
+ ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
- <file name="style.css">
.sortorder:after {
- content: '\25b2';
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
}
.sortorder.reverse:after {
- content: '\25bc';
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
}
- </file>
-</example>
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+ </file>
+ </example>
+ * <hr />
+ *
+ * @example
+ * ### Using a custom comparator
+ *
+ * If you have very specific requirements about the way items are sorted, you can pass your own
+ * comparator function. For example, you might need to compare some strings in a locale-sensitive
+ * way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
+ * argument - passing `false` retains the default sorting order, i.e. ascending.)
+ *
+ <example name="orderBy-custom-comparator" module="orderByExample4">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <div class="friends-container custom-comparator">
+ <h3>Locale-sensitive Comparator</h3>
+ <table class="friends">
+ <tr>
+ <th>Name</th>
+ <th>Favorite Letter</th>
+ </tr>
+ <tr ng-repeat="friend in friends | orderBy:'favoriteLetter':false:localeSensitiveComparator">
+ <td>{{friend.name}}</td>
+ <td>{{friend.favoriteLetter}}</td>
+ </tr>
+ </table>
+ </div>
+ <div class="friends-container default-comparator">
+ <h3>Default Comparator</h3>
+ <table class="friends">
+ <tr>
+ <th>Name</th>
+ <th>Favorite Letter</th>
+ </tr>
+ <tr ng-repeat="friend in friends | orderBy:'favoriteLetter'">
+ <td>{{friend.name}}</td>
+ <td>{{friend.favoriteLetter}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('orderByExample4', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', favoriteLetter: 'Ä'},
+ {name: 'Mary', favoriteLetter: 'Ü'},
+ {name: 'Mike', favoriteLetter: 'Ö'},
+ {name: 'Adam', favoriteLetter: 'H'},
+ {name: 'Julie', favoriteLetter: 'Z'}
+ ];
+
+ $scope.localeSensitiveComparator = function(v1, v2) {
+ // If we don't get strings, just compare by index
+ if (v1.type !== 'string' || v2.type !== 'string') {
+ return (v1.index < v2.index) ? -1 : 1;
+ }
+
+ // Compare strings alphabetically, taking locale into account
+ return v1.value.localeCompare(v2.value);
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .friends-container {
+ display: inline-block;
+ margin: 0 30px;
+ }
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var container = element(by.css('.custom-comparator'));
+ var names = container.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by favorite letter (in correct alphabetical order)', function() {
+ expect(names.get(0).getText()).toBe('John');
+ expect(names.get(1).getText()).toBe('Adam');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('Julie');
+ });
+ </file>
+ </example>
+ *
*/
orderByFilter.$inject = ['$parse'];
function orderByFilter($parse) {
- return function(array, sortPredicate, reverseOrder) {
+ return function(array, sortPredicate, reverseOrder, compareFn) {
- if (!(isArrayLike(array))) return array;
+ if (array == null) return array;
+ if (!isArrayLike(array)) {
+ throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array);
+ }
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
- var predicates = processPredicates(sortPredicate, reverseOrder);
- // Add a predicate at the end that evaluates to the element index. This makes the
- // sort stable as it works as a tie-breaker when all the input predicates cannot
- // distinguish between two elements.
- predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1});
+ var predicates = processPredicates(sortPredicate);
+
+ var descending = reverseOrder ? -1 : 1;
+
+ // Define the `compare()` function. Use a default comparator if none is specified.
+ var compare = isFunction(compareFn) ? compareFn : defaultCompare;
// The next three lines are a version of a Swartzian Transform idiom from Perl
// (sometimes called the Decorate-Sort-Undecorate idiom)
@@ -19924,8 +21692,12 @@ function orderByFilter($parse) {
return array;
function getComparisonObject(value, index) {
+ // NOTE: We are adding an extra `tieBreaker` value based on the element's index.
+ // This will be used to keep the sort stable when none of the input predicates can
+ // distinguish between two elements.
return {
value: value,
+ tieBreaker: {value: index, type: 'number', index: index},
predicateValues: predicates.map(function(predicate) {
return getPredicateValue(predicate.get(value), index);
})
@@ -19933,18 +21705,19 @@ function orderByFilter($parse) {
}
function doComparison(v1, v2) {
- var result = 0;
- for (var index=0, length = predicates.length; index < length; ++index) {
- result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
- if (result) break;
+ for (var i = 0, ii = predicates.length; i < ii; i++) {
+ var result = compare(v1.predicateValues[i], v2.predicateValues[i]);
+ if (result) {
+ return result * predicates[i].descending * descending;
+ }
}
- return result;
+
+ return compare(v1.tieBreaker, v2.tieBreaker) * descending;
}
};
- function processPredicates(sortPredicate, reverseOrder) {
- reverseOrder = reverseOrder ? -1 : 1;
- return sortPredicate.map(function(predicate) {
+ function processPredicates(sortPredicates) {
+ return sortPredicates.map(function(predicate) {
var descending = 1, get = identity;
if (isFunction(predicate)) {
@@ -19962,7 +21735,7 @@ function orderByFilter($parse) {
}
}
}
- return { get: get, descending: descending * reverseOrder };
+ return {get: get, descending: descending};
});
}
@@ -19977,9 +21750,9 @@ function orderByFilter($parse) {
}
}
- function objectValue(value, index) {
+ function objectValue(value) {
// If `valueOf` is a valid function use that
- if (typeof value.valueOf === 'function') {
+ if (isFunction(value.valueOf)) {
value = value.valueOf();
if (isPrimitive(value)) return value;
}
@@ -19988,8 +21761,8 @@ function orderByFilter($parse) {
value = value.toString();
if (isPrimitive(value)) return value;
}
- // We have a basic object so we use the position of the object in the collection
- return index;
+
+ return value;
}
function getPredicateValue(value, index) {
@@ -19997,23 +21770,39 @@ function orderByFilter($parse) {
if (value === null) {
type = 'string';
value = 'null';
- } else if (type === 'string') {
- value = value.toLowerCase();
} else if (type === 'object') {
- value = objectValue(value, index);
+ value = objectValue(value);
}
- return { value: value, type: type };
+ return {value: value, type: type, index: index};
}
- function compare(v1, v2) {
+ function defaultCompare(v1, v2) {
var result = 0;
- if (v1.type === v2.type) {
- if (v1.value !== v2.value) {
- result = v1.value < v2.value ? -1 : 1;
+ var type1 = v1.type;
+ var type2 = v2.type;
+
+ if (type1 === type2) {
+ var value1 = v1.value;
+ var value2 = v2.value;
+
+ if (type1 === 'string') {
+ // Compare strings case-insensitively
+ value1 = value1.toLowerCase();
+ value2 = value2.toLowerCase();
+ } else if (type1 === 'object') {
+ // For basic objects, use the position of the object
+ // in the collection instead of the value
+ if (isObject(value1)) value1 = v1.index;
+ if (isObject(value2)) value2 = v2.index;
+ }
+
+ if (value1 !== value2) {
+ result = value1 < value2 ? -1 : 1;
}
} else {
- result = v1.type < v2.type ? -1 : 1;
+ result = type1 < type2 ? -1 : 1;
}
+
return result;
}
}
@@ -20293,9 +22082,11 @@ var htmlAnchorDirective = valueFn({
*
* @description
*
- * Sets the `readOnly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on
+ * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information.
*
- * A special directive is necessary because we cannot use interpolation inside the `readOnly`
+ * A special directive is necessary because we cannot use interpolation inside the `readonly`
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
@@ -20332,6 +22123,13 @@ var htmlAnchorDirective = valueFn({
* A special directive is necessary because we cannot use interpolation inside the `selected`
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
+ * <div class="alert alert-warning">
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only
+ * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you
+ * should not use `ngSelected` on the options, as `ngModel` will set the select value and
+ * selected options.
+ * </div>
+ *
* @example
<example>
<file name="index.html">
@@ -20368,6 +22166,11 @@ var htmlAnchorDirective = valueFn({
* A special directive is necessary because we cannot use interpolation inside the `open`
* attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
+ * ## A note about browser compatibility
+ *
+ * Edge, Firefox, and Internet Explorer do not support the `details` element, it is
+ * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
+ *
* @example
<example>
<file name="index.html">
@@ -20613,7 +22416,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
*
* However, if the method is used programmatically, for example by adding dynamically created controls,
* or controls that have been previously removed without destroying their corresponding DOM element,
- * it's the developers responsiblity to make sure the current state propagates to the parent form.
+ * it's the developers responsibility to make sure the current state propagates to the parent form.
*
* For example, if an input control is added that is already `$dirty` and has `$error` properties,
* calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
@@ -21043,8 +22846,8 @@ var ngFormDirective = formDirectiveFactory(true);
ngModelMinErr: false,
*/
-// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
-var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
+// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
+var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/;
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
// Note: We are being more lenient, because browsers are too.
// 1. Scheme
@@ -21058,12 +22861,14 @@ var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-
// 9. Fragment
// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999
var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
-var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
+/* jshint maxlen:220 */
+var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
+/* jshint maxlen:200 */
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
-var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
-var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
-var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
-var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
+var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/;
+var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
+var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
+var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
@@ -21096,8 +22901,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21134,11 +22939,11 @@ var inputType = {
<span class="error" ng-show="myForm.input.$error.pattern">
Single word only!</span>
</div>
- <tt>text = {{example.text}}</tt><br/>
- <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
- <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
- <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
- <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
+ <code>text = {{example.text}}</code><br/>
+ <code>myForm.input.$valid = {{myForm.input.$valid}}</code><br/>
+ <code>myForm.input.$error = {{myForm.input.$error}}</code><br/>
+ <code>myForm.$valid = {{myForm.$valid}}</code><br/>
+ <code>myForm.$error.required = {{!!myForm.$error.required}}</code><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
@@ -21384,7 +23189,7 @@ var inputType = {
*
* @description
* Input with time validation and transformation. In browsers that do not yet support
- * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
*
@@ -21427,7 +23232,7 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- <label for="exampleInput">Pick a between 8am and 5pm:</label>
+ <label for="exampleInput">Pick a time between 8am and 5pm:</label>
<input type="time" id="exampleInput" name="input" ng-model="example.value"
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
<div role="alert">
@@ -21731,8 +23536,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21829,8 +23634,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21928,8 +23733,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22148,7 +23953,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
if (!$sniffer.android) {
var composing = false;
- element.on('compositionstart', function(data) {
+ element.on('compositionstart', function() {
composing = true;
});
@@ -22409,11 +24214,7 @@ function badInputChecker(scope, element, attr, ctrl) {
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
- // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430):
- // - also sets validity.badInput (should only be validity.typeMismatch).
- // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email)
- // - can ignore this case as we can still read out the erroneous email...
- return validity.badInput && !validity.typeMismatch ? undefined : value;
+ return validity.badInput || validity.typeMismatch ? undefined : value;
});
}
}
@@ -22447,7 +24248,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
attr.$observe('min', function(val) {
if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val, 10);
+ val = parseFloat(val);
}
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
// TODO(matsko): implement validateLater to reduce number of validations
@@ -22463,7 +24264,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
attr.$observe('max', function(val) {
if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val, 10);
+ val = parseFloat(val);
}
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
// TODO(matsko): implement validateLater to reduce number of validations
@@ -22585,8 +24386,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22624,8 +24425,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * value does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -23021,8 +24822,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
- var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
- return (value || '').toString();
+ var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
+ // Unwrap the value to compare the actual inner safe value, not the wrapper object.
+ return $sce.valueOf(val);
});
$compile.$$addBindingClass(tElement);
@@ -23030,9 +24832,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
$compile.$$addBindingInfo(element, attr.ngBindHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
- // we re-evaluate the expr because we want a TrustedValueHolderType
- // for $sce, not a string
- element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
+ // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
+ var value = ngBindHtmlGetter(scope);
+ element.html($sce.getTrustedHtml(value) || '');
});
};
}
@@ -23185,7 +24987,9 @@ function classDirective(name, selector) {
}
function ngClassWatchAction(newVal) {
- if (selector === true || scope.$index % 2 === selector) {
+ // jshint bitwise: false
+ if (selector === true || (scope.$index & 1) === selector) {
+ // jshint bitwise: true
var newClasses = arrayClasses(newVal || []);
if (!oldVal) {
addClasses(newClasses);
@@ -23194,7 +24998,11 @@ function classDirective(name, selector) {
updateClasses(oldClasses, newClasses);
}
}
- oldVal = shallowCopy(newVal);
+ if (isArray(newVal)) {
+ oldVal = newVal.map(function(v) { return shallowCopy(v); });
+ } else {
+ oldVal = shallowCopy(newVal);
+ }
}
}
};
@@ -23263,10 +25071,16 @@ function classDirective(name, selector) {
* When the expression changes, the previously added classes are removed and only then are the
* new classes added.
*
- * @animations
- * **add** - happens just before the class is applied to the elements
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `class`
+ * attribute, when using the `ngClass` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
*
- * **remove** - happens just before the class is removed from the element
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
*
* @element ANY
* @param {expression} ngClass {@link guide/expression Expression} to eval. The result
@@ -23851,7 +25665,7 @@ var ngControllerDirective = [function() {
*
* * no-inline-style: this stops Angular from injecting CSS styles into the DOM
*
- * * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings
+ * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings
*
* You can use these values in the following combinations:
*
@@ -23868,7 +25682,7 @@ var ngControllerDirective = [function() {
* inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
*
* * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
- * run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
+ * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
*
* * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
* styles nor use eval, which is the same as an empty: ng-csp.
@@ -24525,8 +26339,10 @@ forEach(
* and `leave` effects.
*
* @animations
- * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container
- * leave - happens just before the `ngIf` contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
+ * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM |
*
* @element ANY
* @scope
@@ -24567,7 +26383,7 @@ forEach(
</file>
</example>
*/
-var ngIfDirective = ['$animate', function($animate) {
+var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
multiElement: true,
transclude: 'element',
@@ -24583,7 +26399,7 @@ var ngIfDirective = ['$animate', function($animate) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
- clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
+ clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
@@ -24638,8 +26454,10 @@ var ngIfDirective = ['$animate', function($animate) {
* access on some browsers.
*
* @animations
- * enter - animation is used to bring new content into the browser.
- * leave - animation is used to animate existing content away.
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
+ * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
*
* The enter and leave animation occur concurrently.
*
@@ -24900,12 +26718,12 @@ var ngIncludeFillContentDirective = ['$compile',
priority: -400,
require: 'ngInclude',
link: function(scope, $element, $attr, ctrl) {
- if (/SVG/.test($element[0].toString())) {
+ if (toString.call($element[0]).match(/SVG/)) {
// WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
// support innerHTML, so detect this here and try to generate the contents
// specially.
$element.empty();
- $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
+ $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
function namespaceAdaptedClone(clone) {
$element.append(clone);
}, {futureParentElement: $element});
@@ -25129,7 +26947,9 @@ var VALID_CLASS = 'ng-valid',
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending';
+ PENDING_CLASS = 'ng-pending',
+ EMPTY_CLASS = 'ng-empty',
+ NOT_EMPTY_CLASS = 'ng-not-empty';
var ngModelMinErr = minErr('ngModel');
@@ -25378,9 +27198,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};
ngModelSet = function($scope, newValue) {
if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
+ invokeModelSetter($scope, {$$$p: newValue});
} else {
- parsedNgModelAssign($scope, ctrl.$modelValue);
+ parsedNgModelAssign($scope, newValue);
}
};
} else if (!parsedNgModel.assign) {
@@ -25405,7 +27225,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* the `$viewValue` are different from last time.
*
* Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
- * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue`
+ * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
* or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
* invoked if you only change a property on the objects.
*/
@@ -25433,6 +27253,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
return isUndefined(value) || value === '' || value === null || value !== value;
};
+ this.$$updateEmptyClasses = function(value) {
+ if (ctrl.$isEmpty(value)) {
+ $animate.removeClass($element, NOT_EMPTY_CLASS);
+ $animate.addClass($element, EMPTY_CLASS);
+ } else {
+ $animate.removeClass($element, EMPTY_CLASS);
+ $animate.addClass($element, NOT_EMPTY_CLASS);
+ }
+ };
+
+
var currentValidationRunId = 0;
/**
@@ -25746,7 +27577,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
setValidity(name, undefined);
validatorPromises.push(promise.then(function() {
setValidity(name, true);
- }, function(error) {
+ }, function() {
allValid = false;
setValidity(name, false);
}));
@@ -25796,6 +27627,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
@@ -25894,7 +27726,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* However, custom controls might also pass objects to this method. In this case, we should make
* a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
* perform a deep watch of objects, it only looks for a change of identity. If you only change
- * the property of the object then ngModel will not realise that the object has changed and
+ * the property of the object then ngModel will not realize that the object has changed and
* will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
* not change properties of the copy once it has been passed to `$setViewValue`.
* Otherwise you may cause the model value on the scope to change incorrectly.
@@ -25978,6 +27810,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
@@ -26008,7 +27841,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* require.
* - Providing validation behavior (i.e. required, number, email, url).
* - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
+ * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
+ * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
* - Registering the control with its parent {@link ng.directive:form form}.
*
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
@@ -26065,13 +27899,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - `ng-touched`: the control has been blurred
* - `ng-untouched`: the control hasn't been blurred
* - `ng-pending`: any `$asyncValidators` are unfulfilled
+ * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
+ * by the {@link ngModel.NgModelController#$isEmpty} method
+ * - `ng-not-empty`: the view contains a non-empty value
*
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
* ## Animation Hooks
*
* Animations within models are triggered when any of the associated CSS classes are added and removed
- * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
+ * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
* `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
* The animations that are triggered within ngModel are similar to how they work in ngClass and
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
@@ -26214,7 +28051,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
});
}
- element.on('blur', function(ev) {
+ element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
@@ -26800,7 +28637,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
// jshint maxlen: 100
-var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
+var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
function parseOptionsExpression(optionsExp, selectElement, scope) {
@@ -26897,8 +28734,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];
- var locals = getLocals(optionValues[key], key);
- var selectValue = getTrackByValueFn(optionValues[key], locals);
+ var locals = getLocals(value, key);
+ var selectValue = getTrackByValueFn(value, locals);
watchedArray.push(selectValue);
// Only need to watch the displayFn if there is a specific label expression
@@ -26961,17 +28798,13 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
- var optionTemplate = document.createElement('option'),
- optGroupTemplate = document.createElement('optgroup');
-
+ var optionTemplate = window.document.createElement('option'),
+ optGroupTemplate = window.document.createElement('optgroup');
function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
- var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
-
var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
var multiple = attr.multiple;
// The emptyOption allows the application developer to provide their own custom "empty"
@@ -26991,7 +28824,10 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var options;
var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
-
+ // This stores the newly created options before they are appended to the select.
+ // Since the contents are removed from the fragment when it is appended,
+ // we only need to create it once.
+ var listFragment = $document[0].createDocumentFragment();
var renderEmptyOption = function() {
if (!providedEmptyOption) {
@@ -27026,7 +28862,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
var option = options.getOptionFromViewValue(value);
- if (option && !option.disabled) {
+ if (option) {
// Don't update the option when it is already selected.
// For example, the browser will select the first option by default. In that case,
// most properties are set automatically - except the `selected` attribute, which we
@@ -27088,7 +28924,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
if (value) {
value.forEach(function(item) {
var option = options.getOptionFromViewValue(item);
- if (option && !option.disabled) option.element.selected = true;
+ if (option) option.element.selected = true;
});
}
};
@@ -27140,6 +28976,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
emptyOption = jqLite(optionTemplate.cloneNode(false));
}
+ selectElement.empty();
+
// We need to do this here to ensure that the options object is defined
// when we first hit it in writeNgOptionsValue
updateOptions();
@@ -27149,6 +28987,12 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// ------------------------------------------------------------------ //
+ function addOptionElement(option, parent) {
+ var optionElement = optionTemplate.cloneNode(false);
+ parent.appendChild(optionElement);
+ updateOptionElement(option, optionElement);
+ }
+
function updateOptionElement(option, element) {
option.element = element;
@@ -27165,133 +29009,67 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
if (option.value !== element.value) element.value = option.selectValue;
}
- function addOrReuseElement(parent, current, type, templateElement) {
- var element;
- // Check whether we can reuse the next element
- if (current && lowercase(current.nodeName) === type) {
- // The next element is the right type so reuse it
- element = current;
- } else {
- // The next element is not the right type so create a new one
- element = templateElement.cloneNode(false);
- if (!current) {
- // There are no more elements so just append it to the select
- parent.appendChild(element);
- } else {
- // The next element is not a group so insert the new one
- parent.insertBefore(element, current);
- }
- }
- return element;
- }
-
-
- function removeExcessElements(current) {
- var next;
- while (current) {
- next = current.nextSibling;
- jqLiteRemove(current);
- current = next;
- }
- }
-
-
- function skipEmptyAndUnknownOptions(current) {
- var emptyOption_ = emptyOption && emptyOption[0];
- var unknownOption_ = unknownOption && unknownOption[0];
+ function updateOptions() {
+ var previousValue = options && selectCtrl.readValue();
- // We cannot rely on the extracted empty option being the same as the compiled empty option,
- // because the compiled empty option might have been replaced by a comment because
- // it had an "element" transclusion directive on it (such as ngIf)
- if (emptyOption_ || unknownOption_) {
- while (current &&
- (current === emptyOption_ ||
- current === unknownOption_ ||
- current.nodeType === NODE_TYPE_COMMENT ||
- (nodeName_(current) === 'option' && current.value === ''))) {
- current = current.nextSibling;
+ // We must remove all current options, but cannot simply set innerHTML = null
+ // since the providedEmptyOption might have an ngIf on it that inserts comments which we
+ // must preserve.
+ // Instead, iterate over the current option elements and remove them or their optgroup
+ // parents
+ if (options) {
+
+ for (var i = options.items.length - 1; i >= 0; i--) {
+ var option = options.items[i];
+ if (isDefined(option.group)) {
+ jqLiteRemove(option.element.parentNode);
+ } else {
+ jqLiteRemove(option.element);
+ }
}
}
- return current;
- }
-
-
- function updateOptions() {
-
- var previousValue = options && selectCtrl.readValue();
options = ngOptions.getOptions();
- var groupMap = {};
- var currentElement = selectElement[0].firstChild;
+ var groupElementMap = {};
// Ensure that the empty option is always there if it was explicitly provided
if (providedEmptyOption) {
selectElement.prepend(emptyOption);
}
- currentElement = skipEmptyAndUnknownOptions(currentElement);
-
- options.items.forEach(function updateOption(option) {
- var group;
+ options.items.forEach(function addOption(option) {
var groupElement;
- var optionElement;
- if (option.group) {
+ if (isDefined(option.group)) {
// This option is to live in a group
// See if we have already created this group
- group = groupMap[option.group];
+ groupElement = groupElementMap[option.group];
- if (!group) {
+ if (!groupElement) {
- // We have not already created this group
- groupElement = addOrReuseElement(selectElement[0],
- currentElement,
- 'optgroup',
- optGroupTemplate);
- // Move to the next element
- currentElement = groupElement.nextSibling;
+ groupElement = optGroupTemplate.cloneNode(false);
+ listFragment.appendChild(groupElement);
// Update the label on the group element
- groupElement.label = option.group;
+ // "null" is special cased because of Safari
+ groupElement.label = option.group === null ? 'null' : option.group;
// Store it for use later
- group = groupMap[option.group] = {
- groupElement: groupElement,
- currentOptionElement: groupElement.firstChild
- };
-
+ groupElementMap[option.group] = groupElement;
}
- // So now we have a group for this option we add the option to the group
- optionElement = addOrReuseElement(group.groupElement,
- group.currentOptionElement,
- 'option',
- optionTemplate);
- updateOptionElement(option, optionElement);
- // Move to the next element
- group.currentOptionElement = optionElement.nextSibling;
+ addOptionElement(option, groupElement);
} else {
// This option is not in a group
- optionElement = addOrReuseElement(selectElement[0],
- currentElement,
- 'option',
- optionTemplate);
- updateOptionElement(option, optionElement);
- // Move to the next element
- currentElement = optionElement.nextSibling;
+ addOptionElement(option, listFragment);
}
});
-
- // Now remove all excess options and group
- Object.keys(groupMap).forEach(function(key) {
- removeExcessElements(groupMap[key].currentOptionElement);
- });
- removeExcessElements(currentElement);
+ selectElement[0].appendChild(listFragment);
ngModelCtrl.$render();
@@ -27311,7 +29089,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
return {
restrict: 'A',
terminal: true,
- require: ['select', '?ngModel'],
+ require: ['select', 'ngModel'],
link: {
pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
// Deactivate the SelectController.register method to prevent
@@ -27539,7 +29317,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
// If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
- // In JS `NaN !== NaN`, so we have to exlicitly check.
+ // In JS `NaN !== NaN`, so we have to explicitly check.
if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
@@ -27599,17 +29377,23 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * You need to be aware that the JavaScript specification does not define the order of keys
- * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive
- * used to sort the keys alphabetically.)
+ * However, there are a limitations compared to array iteration:
+ *
+ * - The JavaScript specification does not define the order of keys
+ * returned for an object, so Angular relies on the order returned by the browser
+ * when running `for key in myObj`. Browsers generally follow the strategy of providing
+ * keys in the order in which they were defined, although there are exceptions when keys are deleted
+ * and reinstated. See the
+ * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
- * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
- * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
- * keys in the order in which they were defined, although there are exceptions when keys are deleted
- * and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
*
- * If this is not desired, the recommended workaround is to convert your object into an array
- * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw an error if used with one.
+ *
+ * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
* do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
* or implement a `$watch` on the object yourself.
*
@@ -27656,7 +29440,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
* will not have to rebuild the DOM elements for items it has already rendered, even if the
* JavaScript objects in the collection have been substituted for new ones. For large collections,
- * this signifincantly improves rendering performance. If you don't have a unique identifier,
+ * this significantly improves rendering performance. If you don't have a unique identifier,
* `track by $index` can also provide a performance boost.
* </div>
* ```html
@@ -27727,11 +29511,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
*
* @animations
- * **.enter** - when a new item is added to the list or when an item is revealed after a filter
- *
- * **.leave** - when an item is removed from the list or when an item is filtered out
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter |
+ * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out |
+ * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
*
- * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
+ * See the example below for defining CSS animations with ngRepeat.
*
* @element ANY
* @scope
@@ -27785,22 +29571,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
*
* @example
- * This example initializes the scope to a list of names and
- * then uses `ngRepeat` to display every person:
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
+ * results by name. New (entering) and removed (leaving) items are animated.
+ <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true">
<file name="index.html">
- <div ng-init="friends = [
- {name:'John', age:25, gender:'boy'},
- {name:'Jessie', age:30, gender:'girl'},
- {name:'Johanna', age:28, gender:'girl'},
- {name:'Joy', age:15, gender:'girl'},
- {name:'Mary', age:28, gender:'girl'},
- {name:'Peter', age:95, gender:'boy'},
- {name:'Sebastian', age:50, gender:'boy'},
- {name:'Erika', age:27, gender:'girl'},
- {name:'Patrick', age:40, gender:'boy'},
- {name:'Samantha', age:60, gender:'girl'}
- ]">
+ <div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
@@ -27813,6 +29588,22 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
</ul>
</div>
</file>
+ <file name="script.js">
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ $scope.friends = [
+ {name:'John', age:25, gender:'boy'},
+ {name:'Jessie', age:30, gender:'girl'},
+ {name:'Johanna', age:28, gender:'girl'},
+ {name:'Joy', age:15, gender:'girl'},
+ {name:'Mary', age:28, gender:'girl'},
+ {name:'Peter', age:95, gender:'boy'},
+ {name:'Sebastian', age:50, gender:'boy'},
+ {name:'Erika', age:27, gender:'girl'},
+ {name:'Patrick', age:40, gender:'boy'},
+ {name:'Samantha', age:60, gender:'girl'}
+ ];
+ });
+ </file>
<file name="animations.css">
.example-animate-container {
background:white;
@@ -27823,7 +29614,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
.animate-repeat {
- line-height:40px;
+ line-height:30px;
list-style:none;
box-sizing:border-box;
}
@@ -27845,7 +29636,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
- max-height:40px;
+ max-height:30px;
}
</file>
<file name="protractor.js" type="protractor">
@@ -27872,7 +29663,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
</file>
</example>
*/
-var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
+var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
@@ -27907,7 +29698,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
var expression = $attr.ngRepeat;
- var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
+ var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
@@ -28186,12 +29977,14 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
* property to block during animation states--ngAnimate will handle the style toggling automatically for you.
*
* @animations
- * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible
- * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngShow` expression evaluates to a truthy value and just before contents are set to visible |
*
* @element ANY
* @param {expression} ngShow If the {@link guide/expression expression} is truthy
@@ -28350,12 +30143,15 @@ var ngShowDirective = ['$animate', function($animate) {
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
* property to block during animation states--ngAnimate will handle the style toggling automatically for you.
*
* @animations
- * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden
- * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible |
+ *
*
* @element ANY
* @param {expression} ngHide If the {@link guide/expression expression} is truthy then
@@ -28443,6 +30239,11 @@ var ngHideDirective = ['$animate', function($animate) {
* @description
* The `ngStyle` directive allows you to set CSS style on an HTML element conditionally.
*
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `style`
+ * attribute, when using the `ngStyle` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
+ *
* @element ANY
* @param {expression} ngStyle
*
@@ -28517,8 +30318,10 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
* </div>
* @animations
- * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container
- * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container |
+ * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM |
*
* @usage
*
@@ -28617,7 +30420,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
</file>
</example>
*/
-var ngSwitchDirective = ['$animate', function($animate) {
+var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
return {
require: 'ngSwitch',
@@ -28658,7 +30461,7 @@ var ngSwitchDirective = ['$animate', function($animate) {
selectedTransclude.transclude(function(caseElement, selectedScope) {
selectedScopes.push(selectedScope);
var anchor = selectedTransclude.element;
- caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: ');
+ caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
var block = { clone: caseElement };
selectedElements.push(block);
@@ -28701,69 +30504,214 @@ var ngSwitchDefaultDirective = ngDirective({
* @description
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
*
- * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
+ * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
+ * as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
+ *
+ * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
+ * content of this element will be removed before the transcluded content is inserted.
+ * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case
+ * that no transcluded content is provided.
*
* @element ANY
*
+ * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty
+ * or its value is the same as the name of the attribute then the default slot is used.
+ *
* @example
- <example module="transcludeExample">
- <file name="index.html">
- <script>
- angular.module('transcludeExample', [])
- .directive('pane', function(){
- return {
- restrict: 'E',
- transclude: true,
- scope: { title:'@' },
- template: '<div style="border: 1px solid black;">' +
- '<div style="background-color: gray">{{title}}</div>' +
- '<ng-transclude></ng-transclude>' +
- '</div>'
- };
- })
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.title = 'Lorem Ipsum';
- $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <input ng-model="title" aria-label="title"> <br/>
- <textarea ng-model="text" aria-label="text"></textarea> <br/>
- <pane title="{{title}}">{{text}}</pane>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should have transcluded', function() {
- var titleElement = element(by.model('title'));
- titleElement.clear();
- titleElement.sendKeys('TITLE');
- var textElement = element(by.model('text'));
- textElement.clear();
- textElement.sendKeys('TEXT');
- expect(element(by.binding('title')).getText()).toEqual('TITLE');
- expect(element(by.binding('text')).getText()).toEqual('TEXT');
- });
- </file>
- </example>
+ * ### Basic transclusion
+ * This example demonstrates basic transclusion of content into a component directive.
+ * <example name="simpleTranscludeExample" module="transcludeExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeExample', [])
+ * .directive('pane', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: { title:'@' },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div style="background-color: gray">{{title}}</div>' +
+ * '<ng-transclude></ng-transclude>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane title="{{title}}">{{text}}</pane>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.binding('title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * });
+ * </file>
+ * </example>
+ *
+ * @example
+ * ### Transclude fallback content
+ * This example shows how to use `NgTransclude` with fallback content, that
+ * is displayed if no transcluded content is provided.
*
+ * <example module="transcludeFallbackContentExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeFallbackContentExample', [])
+ * .directive('myButton', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: true,
+ * template: '<button style="cursor: pointer;">' +
+ * '<ng-transclude>' +
+ * '<b style="color: red;">Button1</b>' +
+ * '</ng-transclude>' +
+ * '</button>'
+ * };
+ * });
+ * </script>
+ * <!-- fallback button content -->
+ * <my-button id="fallback"></my-button>
+ * <!-- modified button content -->
+ * <my-button id="modified">
+ * <i style="color: green;">Button2</i>
+ * </my-button>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have different transclude element content', function() {
+ * expect(element(by.id('fallback')).getText()).toBe('Button1');
+ * expect(element(by.id('modified')).getText()).toBe('Button2');
+ * });
+ * </file>
+ * </example>
+ *
+ * @example
+ * ### Multi-slot transclusion
+ * This example demonstrates using multi-slot transclusion in a component directive.
+ * <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
+ * <file name="index.html">
+ * <style>
+ * .title, .footer {
+ * background-color: gray
+ * }
+ * </style>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane>
+ * <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
+ * <pane-body><p>{{text}}</p></pane-body>
+ * </pane>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('multiSlotTranscludeExample', [])
+ * .directive('pane', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: {
+ * 'title': '?paneTitle',
+ * 'body': 'paneBody',
+ * 'footer': '?paneFooter'
+ * },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div class="title" ng-transclude="title">Fallback Title</div>' +
+ * '<div ng-transclude="body"></div>' +
+ * '<div class="footer" ng-transclude="footer">Fallback Footer</div>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.link = "https://google.com";
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded the title and the body', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.css('.title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer');
+ * });
+ * </file>
+ * </example>
*/
-var ngTranscludeDirective = ngDirective({
- restrict: 'EAC',
- link: function($scope, $element, $attrs, controller, $transclude) {
- if (!$transclude) {
- throw minErr('ngTransclude')('orphan',
- 'Illegal use of ngTransclude directive in the template! ' +
- 'No parent directive that requires a transclusion found. ' +
- 'Element: {0}',
- startingTag($element));
- }
-
- $transclude(function(clone) {
- $element.empty();
- $element.append(clone);
- });
- }
-});
+var ngTranscludeMinErr = minErr('ngTransclude');
+var ngTranscludeDirective = ['$compile', function($compile) {
+ return {
+ restrict: 'EAC',
+ terminal: true,
+ compile: function ngTranscludeCompile(tElement) {
+
+ // Remove and cache any original content to act as a fallback
+ var fallbackLinkFn = $compile(tElement.contents());
+ tElement.empty();
+
+ return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
+
+ if (!$transclude) {
+ throw ngTranscludeMinErr('orphan',
+ 'Illegal use of ngTransclude directive in the template! ' +
+ 'No parent directive that requires a transclusion found. ' +
+ 'Element: {0}',
+ startingTag($element));
+ }
+
+
+ // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ $attrs.ngTransclude = '';
+ }
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
+
+ // If the slot is required and no transclusion content is provided then this call will throw an error
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
+
+ // If the slot is optional and no transclusion content is provided then use the fallback content
+ if (slotName && !$transclude.isSlotFilled(slotName)) {
+ useFallbackContent();
+ }
+
+ function ngTranscludeCloneAttachFn(clone, transcludedScope) {
+ if (clone.length) {
+ $element.append(clone);
+ } else {
+ useFallbackContent();
+ // There is nothing linked against the transcluded scope since no content was available,
+ // so it should be safe to clean up the generated scope.
+ transcludedScope.$destroy();
+ }
+ }
+
+ function useFallbackContent() {
+ // Since this is the fallback content rather than the transcluded content,
+ // we link against the scope of this directive rather than the transcluded scope
+ fallbackLinkFn($scope, function(clone) {
+ $element.append(clone);
+ });
+ }
+ };
+ }
+ };
+}];
/**
* @ngdoc directive
@@ -28833,7 +30781,7 @@ function chromeHack(optionElement) {
* added `<option>` elements, perhaps by an `ngRepeat` directive.
*/
var SelectController =
- ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+ ['$element', '$scope', function($element, $scope) {
var self = this,
optionsMap = new HashMap();
@@ -28847,7 +30795,7 @@ var SelectController =
//
// We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
- self.unknownOption = jqLite(document.createElement('option'));
+ self.unknownOption = jqLite(window.document.createElement('option'));
self.renderUnknownOption = function(val) {
var unknownVal = '? ' + hashKey(val) + ' ?';
self.unknownOption.val(unknownVal);
@@ -28980,7 +30928,7 @@ var SelectController =
*
* <div class="alert alert-warning">
* Note that the value of a `select` directive used without `ngOptions` is always a string.
- * When the model needs to be bound to a non-string value, you must either explictly convert it
+ * When the model needs to be bound to a non-string value, you must either explicitly convert it
* using a directive (see example below) or use `ngOptions` to specify the set of options.
* This is because an option element can only be bound to string values at present.
* </div>
@@ -29304,6 +31252,7 @@ var styleDirective = valueFn({
/**
* @ngdoc directive
* @name ngRequired
+ * @restrict A
*
* @description
*
@@ -29810,10 +31759,10 @@ $provide.value("$locale", {
});
}]);
- jqLite(document).ready(function() {
- angularInit(document, bootstrap);
+ jqLite(window.document).ready(function() {
+ angularInit(window.document, bootstrap);
});
-})(window, document);
+})(window);
!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'); \ No newline at end of file