+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ *
*
* @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.
+ * `$sceDelegate.trustAs`} call.
* @returns {*} The value the was originally provided to {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} if valid in this context. Otherwise, throws an exception.
*/
function getTrusted(type, maybeTrusted) {
- if (maybeTrusted === null || maybeTrusted === undefined || maybeTrusted === '') {
+ if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') {
return maybeTrusted;
}
var constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
@@ -11381,7 +18548,8 @@ function $SceDelegateProvider() {
return maybeTrusted;
} else {
throw $sceMinErr('insecurl',
- 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', maybeTrusted.toString());
+ 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}',
+ maybeTrusted.toString());
}
} else if (type === SCE_CONTEXTS.HTML) {
return htmlSanitizer(maybeTrusted);
@@ -11397,8 +18565,8 @@ function $SceDelegateProvider() {
/**
- * @ngdoc object
- * @name ng.$sceProvider
+ * @ngdoc provider
+ * @name $sceProvider
* @description
*
* The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service.
@@ -11408,10 +18576,12 @@ function $SceDelegateProvider() {
* Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
*/
+/* jshint maxlen: false*/
+
/**
* @ngdoc service
- * @name ng.$sce
- * @function
+ * @name $sce
+ * @kind function
*
* @description
*
@@ -11426,23 +18596,23 @@ function $SceDelegateProvider() {
*
* As of version 1.2, Angular ships with SCE enabled by default.
*
- * Note: When enabled (the default), IE8 in quirks mode is not supported. In this mode, IE8 allows
+ * Note: When enabled (the default), IE<11 in quirks mode is not supported. In this mode, IE<11 allow
* one to execute arbitrary javascript by the use of the expression() syntax. Refer
* to learn more about them.
* You can ensure your document is in standards mode and not quirks mode by adding ``
* to the top of your HTML document.
*
- * 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:
*
- *
- *
- *
- *
+ * ```
+ *
+ *
+ * ```
*
- * Notice that `ng-bind-html` is bound to `{{userHtml}}` controlled by the user. With SCE
+ * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE
* disabled, this application allows the user to render arbitrary HTML into the DIV.
* In a more realistic example, one may be rendering user comments, blog articles, etc. via
* bindings. (HTML is just one example of a context where rendering user controlled input creates
@@ -11464,31 +18634,31 @@ function $SceDelegateProvider() {
* allowing only the files in a specific directory to do this. Ensuring that the internal API
* exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task.
*
- * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs} (and shorthand
- * methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to obtain values that will be
- * accepted by SCE / privileged contexts.
+ * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs}
+ * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to
+ * obtain values that will be accepted by SCE / privileged contexts.
*
*
* ## How does it work?
*
* In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
* $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
- * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
+ * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
* {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
*
* As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
* ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
* simplified):
*
- *
- * var ngBindHtmlDirective = ['$sce', function($sce) {
- * return function(scope, element, attr) {
- * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
- * element.html(value || '');
- * });
- * };
- * }];
- *
+ * ```
+ * var ngBindHtmlDirective = ['$sce', function($sce) {
+ * return function(scope, element, attr) {
+ * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
+ * element.html(value || '');
+ * });
+ * };
+ * }];
+ * ```
*
* ## Impact on loading templates
*
@@ -11498,25 +18668,25 @@ function $SceDelegateProvider() {
* By default, Angular only loads templates from the same domain and protocol as the application
* document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
* $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
- * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
+ * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
* them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
*
* *Please note*:
* The browser's
- * {@link https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest
- * Same Origin Policy} and {@link http://www.w3.org/TR/cors/ Cross-Origin Resource Sharing (CORS)}
+ * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
+ * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
* policy apply in addition to this and may further restrict whether the template is successfully
* loaded. This means that without the right CORS policy, loading templates from a different domain
* won't work on all browsers. Also, loading templates from `file://` URL does not work on some
* browsers.
*
- * ## This feels like too much overhead for the developer?
+ * ## This feels like too much overhead
*
* It's important to remember that SCE only applies to interpolation expressions.
*
* If your expressions are constant literals, they're automatically trusted and you don't need to
- * call `$sce.trustAs` on them. (e.g.
- * `
`) just works.
+ * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g.
+ * `
`) just works.
*
* Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
* through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here.
@@ -11532,14 +18702,15 @@ function $SceDelegateProvider() {
* application that's secure and can be audited to verify that with much more ease than bolting
* security onto an application later.
*
- * ## What trusted context types are supported?
+ *
+ * ## What trusted context types are supported?
*
* | Context | Notes |
* |---------------------|----------------|
- * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. |
+ * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. |
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
- * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
+ * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (` Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
*
* ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
@@ -11554,98 +18725,102 @@ function $SceDelegateProvider() {
* being tested (substring matches are not good enough.)
* - There are exactly **two wildcard sequences** - `*` and `**`. All other characters
* match themselves.
- * - `*`: matches zero or more occurances of any character other than one of the following 6
- * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use
+ * - `*`: matches zero or more occurrences of any character other than one of the following 6
+ * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use
* in a whitelist.
- * - `**`: matches zero or more occurances of *any* character. As such, it's not
- * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g.
+ * - `**`: matches zero or more occurrences of *any* character. As such, it's not
+ * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g.
* http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
- * not have been the intention.) It's usage at the very end of the path is ok. (e.g.
+ * not have been the intention.) Its usage at the very end of the path is ok. (e.g.
* http://foo.example.com/templates/**).
* - **RegExp** (*see caveat below*)
* - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax
* (and all the inevitable escaping) makes them *harder to maintain*. It's easy to
* accidentally introduce a bug when one updates a complex expression (imho, all regexes should
- * have good test coverage.). For instance, the use of `.` in the regex is correct only in a
+ * have good test coverage). For instance, the use of `.` in the regex is correct only in a
* small number of cases. A `.` character in the regex used when matching the scheme or a
* subdomain could be matched against a `:` or literal `.` that was likely not intended. It
* is highly recommended to use the string patterns and only fall back to regular expressions
- * if they as a last resort.
+ * as a last resort.
* - The regular expression must be an instance of RegExp (i.e. not a string.) It is
* matched against the **entire** *normalized / absolute URL* of the resource being tested
* (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags
* present on the RegExp (such as multiline, global, ignoreCase) are ignored.
- * - If you are generating your Javascript from some other templating engine (not
+ * - If you are generating your JavaScript from some other templating engine (not
* recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)),
* remember to escape your regular expression (and be aware that you might need more than
* one level of escaping depending on your templating engine and the way you interpolated
* the value.) Do make use of your platform's escaping mechanism as it might be good
- * enough before coding your own. e.g. Ruby has
+ * enough before coding your own. E.g. Ruby has
* [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape)
* and Python has [re.escape](http://docs.python.org/library/re.html#re.escape).
* Javascript lacks a similar built in function for escaping. Take a look at Google
* Closure library's [goog.string.regExpEscape(s)](
* http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962).
*
- * Refer {@link ng.$sceDelegateProvider#example $sceDelegateProvider} for an example.
+ * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example.
*
* ## Show me an example using SCE.
*
- * @example
-
-
-
-
-
User comments
- By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when $sanitize is available. If $sanitize isn't available, this results in an error instead of an exploit.
-
-
- {{userComment.name}} :
-
-
-
-
-
-
-
-
- var mySceApp = angular.module('mySceApp', ['ngSanitize']);
-
- mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) {
- var self = this;
- $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
- self.userComments = userComments;
- });
- self.explicitlyTrustedHtml = $sce.trustAsHtml(
- 'Hover over this text. ');
- });
-
-
-
- [
- { "name": "Alice",
- "htmlComment": "Is anyone reading this? "
- },
- { "name": "Bob",
- "htmlComment": "Yes! Am I the only other one?"
- }
- ]
-
-
-
- describe('SCE doc demo', function() {
- it('should sanitize untrusted values', function() {
- expect(element('.htmlComment').html()).toBe('Is anyone reading this? ');
- });
- it('should NOT sanitize explicitly trusted values', function() {
- expect(element('#explicitlyTrustedHtml').html()).toBe(
- 'Hover over this text. ');
- });
- });
-
-
+ *
+ *
+ *
+ *
+ *
User comments
+ * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when
+ * $sanitize is available. If $sanitize isn't available, this results in an error instead of an
+ * exploit.
+ *
+ *
+ * {{userComment.name}} :
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * angular.module('mySceApp', ['ngSanitize'])
+ * .controller('AppController', ['$http', '$templateCache', '$sce',
+ * function($http, $templateCache, $sce) {
+ * var self = this;
+ * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
+ * self.userComments = userComments;
+ * });
+ * self.explicitlyTrustedHtml = $sce.trustAsHtml(
+ * 'Hover over this text. ');
+ * }]);
+ *
+ *
+ *
+ * [
+ * { "name": "Alice",
+ * "htmlComment":
+ * "Is anyone reading this? "
+ * },
+ * { "name": "Bob",
+ * "htmlComment": "Yes! Am I the only other one?"
+ * }
+ * ]
+ *
+ *
+ *
+ * describe('SCE doc demo', function() {
+ * it('should sanitize untrusted values', function() {
+ * expect(element.all(by.css('.htmlComment')).first().getInnerHtml())
+ * .toBe('Is anyone reading this? ');
+ * });
+ *
+ * it('should NOT sanitize explicitly trusted values', function() {
+ * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe(
+ * 'Hover over this text. ');
+ * });
+ * });
+ *
+ *
*
*
*
@@ -11659,24 +18834,24 @@ function $SceDelegateProvider() {
*
* That said, here's how you can completely disable SCE:
*
- *
- * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
- * // Completely disable SCE. For demonstration purposes only!
- * // Do not use in new projects.
- * $sceProvider.enabled(false);
- * });
- *
+ * ```
+ * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
+ * // Completely disable SCE. For demonstration purposes only!
+ * // Do not use in new projects.
+ * $sceProvider.enabled(false);
+ * });
+ * ```
*
*/
+/* jshint maxlen: 100 */
function $SceProvider() {
var enabled = true;
/**
- * @ngdoc function
- * @name ng.sceProvider#enabled
- * @methodOf ng.$sceProvider
- * @function
+ * @ngdoc method
+ * @name $sceProvider#enabled
+ * @kind function
*
* @param {boolean=} value If provided, then enables/disables SCE.
* @return {boolean} true if SCE is enabled, false otherwise.
@@ -11684,7 +18859,7 @@ function $SceProvider() {
* @description
* Enables/disables SCE and returns the current value.
*/
- this.enabled = function (value) {
+ this.enabled = function(value) {
if (arguments.length) {
enabled = !!value;
}
@@ -11713,13 +18888,13 @@ function $SceProvider() {
* This function should return the a value that is safe to use in the context specified by
* contextEnum or throw and exception otherwise.
*
- * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be opaque
- * or wrapped in some holder object. That happens to be an implementation detail. For instance,
- * an implementation could maintain a registry of all trusted objects by context. In such a case,
- * trustAs() would return the same object that was passed in. getTrusted() would return the same
- * object passed in if it was found in the registry under a compatible context or throw an
- * exception otherwise. An implementation might only wrap values some of the time based on
- * some criteria. getTrusted() might return a value and not throw an exception for special
+ * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
+ * opaque or wrapped in some holder object. That happens to be an implementation detail. For
+ * instance, an implementation could maintain a registry of all trusted objects by context. In
+ * such a case, trustAs() would return the same object that was passed in. getTrusted() would
+ * return the same object passed in if it was found in the registry under a compatible context or
+ * throw an exception otherwise. An implementation might only wrap values some of the time based
+ * on some criteria. getTrusted() might return a value and not throw an exception for special
* constants or objects even if not wrapped. All such implementations fulfill this contract.
*
*
@@ -11738,27 +18913,23 @@ function $SceProvider() {
* sce.js and sceSpecs.js would need to be aware of this detail.
*/
- this.$get = ['$parse', '$document', '$sceDelegate', function(
- $parse, $document, $sceDelegate) {
- // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows
+ this.$get = ['$parse', '$sceDelegate', function(
+ $parse, $sceDelegate) {
+ // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
// the "expression(javascript expression)" syntax which is insecure.
- if (enabled && msie) {
- var documentMode = $document[0].documentMode;
- if (documentMode !== undefined && documentMode < 8) {
- throw $sceMinErr('iequirks',
- 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' +
- 'mode. You can fix this by adding the text to the top of your HTML ' +
- 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
- }
+ if (enabled && msie < 8) {
+ throw $sceMinErr('iequirks',
+ 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' +
+ 'mode. You can fix this by adding the text to the top of your HTML ' +
+ 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
}
- var sce = copy(SCE_CONTEXTS);
+ var sce = shallowCopy(SCE_CONTEXTS);
/**
- * @ngdoc function
- * @name ng.sce#isEnabled
- * @methodOf ng.$sce
- * @function
+ * @ngdoc method
+ * @name $sce#isEnabled
+ * @kind function
*
* @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you
* have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
@@ -11766,7 +18937,7 @@ function $SceProvider() {
* @description
* Returns a boolean indicating if SCE is enabled.
*/
- sce.isEnabled = function () {
+ sce.isEnabled = function() {
return enabled;
};
sce.trustAs = $sceDelegate.trustAs;
@@ -11774,14 +18945,13 @@ function $SceProvider() {
sce.valueOf = $sceDelegate.valueOf;
if (!enabled) {
- sce.trustAs = sce.getTrusted = function(type, value) { return value; },
- sce.valueOf = identity
+ sce.trustAs = sce.getTrusted = function(type, value) { return value; };
+ sce.valueOf = identity;
}
/**
* @ngdoc method
- * @name ng.$sce#parse
- * @methodOf ng.$sce
+ * @name $sce#parseAs
*
* @description
* Converts Angular {@link guide/expression expression} into a function. This is like {@link
@@ -11803,26 +18973,26 @@ function $SceProvider() {
if (parsed.literal && parsed.constant) {
return parsed;
} else {
- return function sceParseAsTrusted(self, locals) {
- return sce.getTrusted(type, parsed(self, locals));
- }
+ return $parse(expr, function(value) {
+ return sce.getTrusted(type, value);
+ });
}
};
/**
* @ngdoc method
- * @name ng.$sce#trustAs
- * @methodOf ng.$sce
+ * @name $sce#trustAs
*
* @description
- * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns an object
- * that is trusted by angular for use in specified strict contextual escaping contexts (such as
- * ng-html-bind-unsafe, ng-include, any src attribute interpolation, any dom event binding
- * attribute interpolation such as for onclick, etc.) that uses the provided value. See *
- * {@link ng.$sce $sce} for enabling strict contextual escaping.
+ * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such,
+ * returns an object that is trusted by angular for use in specified strict contextual
+ * escaping contexts (such as ng-bind-html, ng-include, any src attribute
+ * interpolation, any dom event binding attribute interpolation such as for onclick, etc.)
+ * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual
+ * escaping.
*
* @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resource_url, html, js and css.
+ * resourceUrl, html, js and css.
* @param {*} value The value that that should be considered trusted/safe.
* @returns {*} A value that can be used to stand in for the provided `value` in places
* where Angular expects a $sce.trustAs() return value.
@@ -11830,11 +19000,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#trustAsHtml
- * @methodOf ng.$sce
+ * @name $sce#trustAsHtml
*
* @description
- * Shorthand method. `$sce.trustAsHtml(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
+ * Shorthand method. `$sce.trustAsHtml(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
*
* @param {*} value The value to trustAs.
* @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml
@@ -11845,11 +19015,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#trustAsUrl
- * @methodOf ng.$sce
+ * @name $sce#trustAsUrl
*
* @description
- * Shorthand method. `$sce.trustAsUrl(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
+ * Shorthand method. `$sce.trustAsUrl(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
*
* @param {*} value The value to trustAs.
* @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl
@@ -11860,11 +19030,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#trustAsResourceUrl
- * @methodOf ng.$sce
+ * @name $sce#trustAsResourceUrl
*
* @description
- * Shorthand method. `$sce.trustAsResourceUrl(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
+ * Shorthand method. `$sce.trustAsResourceUrl(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
*
* @param {*} value The value to trustAs.
* @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl
@@ -11875,11 +19045,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#trustAsJs
- * @methodOf ng.$sce
+ * @name $sce#trustAsJs
*
* @description
- * Shorthand method. `$sce.trustAsJs(value)` → {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
+ * Shorthand method. `$sce.trustAsJs(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
*
* @param {*} value The value to trustAs.
* @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs
@@ -11890,28 +19060,29 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#getTrusted
- * @methodOf ng.$sce
+ * @name $sce#getTrusted
*
* @description
- * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such, takes
- * the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the originally supplied
- * value if the queried context type is a supertype of the created type. If this condition
- * isn't satisfied, throws an exception.
+ * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such,
+ * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and returns the
+ * originally supplied value if the queried context type is a supertype of the created type.
+ * If this condition isn't satisfied, throws an exception.
*
* @param {string} type The kind of context in which this value is to be used.
- * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`} call.
- * @returns {*} The value the was originally provided to {@link ng.$sce#trustAs `$sce.trustAs`} if
- * valid in this context. Otherwise, throws an exception.
+ * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`}
+ * call.
+ * @returns {*} The value the was originally provided to
+ * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context.
+ * Otherwise, throws an exception.
*/
/**
* @ngdoc method
- * @name ng.$sce#getTrustedHtml
- * @methodOf ng.$sce
+ * @name $sce#getTrustedHtml
*
* @description
- * Shorthand method. `$sce.getTrustedHtml(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
+ * Shorthand method. `$sce.getTrustedHtml(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
* @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)`
@@ -11919,11 +19090,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#getTrustedCss
- * @methodOf ng.$sce
+ * @name $sce#getTrustedCss
*
* @description
- * Shorthand method. `$sce.getTrustedCss(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
+ * Shorthand method. `$sce.getTrustedCss(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
* @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)`
@@ -11931,11 +19102,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#getTrustedUrl
- * @methodOf ng.$sce
+ * @name $sce#getTrustedUrl
*
* @description
- * Shorthand method. `$sce.getTrustedUrl(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
+ * Shorthand method. `$sce.getTrustedUrl(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
* @returns {*} The return value of `$sce.getTrusted($sce.URL, value)`
@@ -11943,11 +19114,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#getTrustedResourceUrl
- * @methodOf ng.$sce
+ * @name $sce#getTrustedResourceUrl
*
* @description
- * Shorthand method. `$sce.getTrustedResourceUrl(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
+ * Shorthand method. `$sce.getTrustedResourceUrl(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
*
* @param {*} value The value to pass to `$sceDelegate.getTrusted`.
* @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
@@ -11955,11 +19126,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#getTrustedJs
- * @methodOf ng.$sce
+ * @name $sce#getTrustedJs
*
* @description
- * Shorthand method. `$sce.getTrustedJs(value)` → {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
+ * Shorthand method. `$sce.getTrustedJs(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
*
* @param {*} value The value to pass to `$sce.getTrusted`.
* @returns {*} The return value of `$sce.getTrusted($sce.JS, value)`
@@ -11967,11 +19138,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#parseAsHtml
- * @methodOf ng.$sce
+ * @name $sce#parseAsHtml
*
* @description
- * Shorthand method. `$sce.parseAsHtml(expression string)` → {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`}
+ * Shorthand method. `$sce.parseAsHtml(expression string)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -11984,11 +19155,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#parseAsCss
- * @methodOf ng.$sce
+ * @name $sce#parseAsCss
*
* @description
- * Shorthand method. `$sce.parseAsCss(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`}
+ * Shorthand method. `$sce.parseAsCss(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -12001,11 +19172,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#parseAsUrl
- * @methodOf ng.$sce
+ * @name $sce#parseAsUrl
*
* @description
- * Shorthand method. `$sce.parseAsUrl(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`}
+ * Shorthand method. `$sce.parseAsUrl(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -12018,11 +19189,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#parseAsResourceUrl
- * @methodOf ng.$sce
+ * @name $sce#parseAsResourceUrl
*
* @description
- * Shorthand method. `$sce.parseAsResourceUrl(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`}
+ * Shorthand method. `$sce.parseAsResourceUrl(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -12035,11 +19206,11 @@ function $SceProvider() {
/**
* @ngdoc method
- * @name ng.$sce#parseAsJs
- * @methodOf ng.$sce
+ * @name $sce#parseAsJs
*
* @description
- * Shorthand method. `$sce.parseAsJs(value)` → {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`}
+ * Shorthand method. `$sce.parseAsJs(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
*
* @param {string} expression String expression to compile.
* @returns {function(context, locals)} a function which represents the compiled expression:
@@ -12055,17 +19226,17 @@ function $SceProvider() {
getTrusted = sce.getTrusted,
trustAs = sce.trustAs;
- forEach(SCE_CONTEXTS, function (enumValue, name) {
+ forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function (expr) {
+ sce[camelCase("parse_as_" + lName)] = function(expr) {
return parse(enumValue, expr);
- }
- sce[camelCase("get_trusted_" + lName)] = function (value) {
+ };
+ sce[camelCase("get_trusted_" + lName)] = function(value) {
return getTrusted(enumValue, value);
- }
- sce[camelCase("trust_as_" + lName)] = function (value) {
+ };
+ sce[camelCase("trust_as_" + lName)] = function(value) {
return trustAs(enumValue, value);
- }
+ };
});
return sce;
@@ -12075,12 +19246,11 @@ function $SceProvider() {
/**
* !!! This is an undocumented "private" service !!!
*
- * @name ng.$sniffer
+ * @name $sniffer
* @requires $window
* @requires $document
*
* @property {boolean} history Does the browser support html5 history api ?
- * @property {boolean} hashchange Does the browser support hashchange event ?
* @property {boolean} transitions Does the browser support CSS transition events ?
* @property {boolean} animations Does the browser support CSS animation events ?
*
@@ -12090,35 +19260,40 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
- android = int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
+ // 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),
document = $document[0] || {},
vendorPrefix,
- vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
+ vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style,
transitions = false,
animations = false,
match;
if (bodyStyle) {
- for(var prop in bodyStyle) {
- if(match = vendorRegex.exec(prop)) {
+ for (var prop in bodyStyle) {
+ if (match = vendorRegex.exec(prop)) {
vendorPrefix = match[0];
- vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
+ vendorPrefix = vendorPrefix[0].toUpperCase() + vendorPrefix.substr(1);
break;
}
}
- if(!vendorPrefix) {
+ if (!vendorPrefix) {
vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
}
transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
- if (android && (!transitions||!animations)) {
- transitions = isString(document.body.style.webkitTransition);
- animations = isString(document.body.style.webkitAnimation);
+ if (android && (!transitions || !animations)) {
+ transitions = isString(bodyStyle.webkitTransition);
+ animations = isString(bodyStyle.webkitAnimation);
}
}
@@ -12129,17 +19304,19 @@ function $SnifferProvider() {
// http://code.google.com/p/android/issues/detail?id=17471
// https://github.com/angular/angular.js/issues/904
- // older webit browser (533.9) on Boxee box has exactly the same problem as Android has
+ // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
// so let's not use the history API also
- history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
- hashchange: 'onhashchange' in $window &&
- // IE8 compatible mode lies
- (!document.documentMode || document.documentMode > 7),
+ // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
+ // jshint -W018
+ 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
// it. In particular the event is not fired when backspace or delete key are pressed or
// when cut operation is performed.
- if (event == 'input' && msie == 9) return false;
+ // IE10+ implements 'input' event but it erroneously fires under various situations,
+ // e.g. when placeholder changes, or a form is focused.
+ if (event === 'input' && msie <= 11) return false;
if (isUndefined(eventSupport[event])) {
var divElm = document.createElement('div');
@@ -12148,143 +19325,292 @@ function $SnifferProvider() {
return eventSupport[event];
},
- csp: document.securityPolicy ? document.securityPolicy.isActive : false,
+ csp: csp(),
vendorPrefix: vendorPrefix,
- transitions : transitions,
- animations : animations
+ transitions: transitions,
+ animations: animations,
+ android: android
+ };
+ }];
+}
+
+var $templateRequestMinErr = minErr('$compile');
+
+/**
+ * @ngdoc provider
+ * @name $templateRequestProvider
+ * @description
+ * Used to configure the options passed to the {@link $http} service when making a template request.
+ *
+ * For example, it can be used for specifying the "Accept" header that is sent to the server, when
+ * requesting a template.
+ */
+function $TemplateRequestProvider() {
+
+ 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++;
+
+ // We consider the template cache holds only trusted templates, so
+ // there's no need to go through whitelisting again for keys that already
+ // are included in there. This also makes Angular accept any script
+ // directive, no matter its name. However, we still need to unwrap trusted
+ // types.
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
+ tpl = $sce.getTrustedResourceUrl(tpl);
+ }
+
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
+
+ if (isArray(transformResponse)) {
+ transformResponse = transformResponse.filter(function(transformer) {
+ return transformer !== defaultHttpResponseTransform;
+ });
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
+
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
+ ['finally'](function() {
+ handleRequestFn.totalPendingRequests--;
+ })
+ .then(function(response) {
+ $templateCache.put(tpl, response.data);
+ return response.data;
+ }, handleError);
+
+ function handleError(resp) {
+ if (!ignoreRequestError) {
+ throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
+ }
+ return $q.reject(resp);
+ }
+ }
+
+ handleRequestFn.totalPendingRequests = 0;
+
+ return handleRequestFn;
+ }];
+}
+
+function $$TestabilityProvider() {
+ this.$get = ['$rootScope', '$browser', '$location',
+ function($rootScope, $browser, $location) {
+
+ /**
+ * @name $testability
+ *
+ * @description
+ * The private $$testability service provides a collection of methods for use when debugging
+ * or by automated test and debugging tools.
+ */
+ var testability = {};
+
+ /**
+ * @name $$testability#findBindings
+ *
+ * @description
+ * Returns an array of elements that are bound (via ng-bind or {{}})
+ * to expressions matching the input.
+ *
+ * @param {Element} element The element root to search from.
+ * @param {string} expression The binding expression to match.
+ * @param {boolean} opt_exactMatch If true, only returns exact matches
+ * for the expression. Filters and whitespace are ignored.
+ */
+ testability.findBindings = function(element, expression, opt_exactMatch) {
+ var bindings = element.getElementsByClassName('ng-binding');
+ var matches = [];
+ forEach(bindings, function(binding) {
+ var dataBinding = angular.element(binding).data('$binding');
+ if (dataBinding) {
+ forEach(dataBinding, function(bindingName) {
+ if (opt_exactMatch) {
+ var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
+ if (matcher.test(bindingName)) {
+ matches.push(binding);
+ }
+ } else {
+ if (bindingName.indexOf(expression) != -1) {
+ matches.push(binding);
+ }
+ }
+ });
+ }
+ });
+ return matches;
+ };
+
+ /**
+ * @name $$testability#findModels
+ *
+ * @description
+ * Returns an array of elements that are two-way found via ng-model to
+ * expressions matching the input.
+ *
+ * @param {Element} element The element root to search from.
+ * @param {string} expression The model expression to match.
+ * @param {boolean} opt_exactMatch If true, only returns exact matches
+ * for the expression.
+ */
+ testability.findModels = function(element, expression, opt_exactMatch) {
+ var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
+ for (var p = 0; p < prefixes.length; ++p) {
+ var attributeEquals = opt_exactMatch ? '=' : '*=';
+ var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
+ var elements = element.querySelectorAll(selector);
+ if (elements.length) {
+ return elements;
+ }
+ }
+ };
+
+ /**
+ * @name $$testability#getLocation
+ *
+ * @description
+ * Shortcut for getting the location in a browser agnostic way. Returns
+ * the path, search, and hash. (e.g. /path?a=b#hash)
+ */
+ testability.getLocation = function() {
+ return $location.url();
+ };
+
+ /**
+ * @name $$testability#setLocation
+ *
+ * @description
+ * Shortcut for navigating to a location without doing a full page reload.
+ *
+ * @param {string} url The location url (path, search and hash,
+ * e.g. /path?a=b#hash) to go to.
+ */
+ testability.setLocation = function(url) {
+ if (url !== $location.url()) {
+ $location.url(url);
+ $rootScope.$digest();
+ }
+ };
+
+ /**
+ * @name $$testability#whenStable
+ *
+ * @description
+ * Calls the callback when $timeout and $http requests are completed.
+ *
+ * @param {function} callback
+ */
+ testability.whenStable = function(callback) {
+ $browser.notifyWhenNoOutstandingRequests(callback);
};
+
+ return testability;
}];
}
function $TimeoutProvider() {
- this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
- function($rootScope, $browser, $q, $exceptionHandler) {
+ this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
+ function($rootScope, $browser, $q, $$q, $exceptionHandler) {
+
var deferreds = {};
/**
- * @ngdoc function
- * @name ng.$timeout
- * @requires $browser
+ * @ngdoc service
+ * @name $timeout
*
* @description
* Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
* block and delegates any exceptions to
* {@link ng.$exceptionHandler $exceptionHandler} service.
*
- * The return value of registering a timeout function is a promise, which will be resolved when
- * the timeout is reached and the timeout function is executed.
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
*
* To cancel a timeout request, call `$timeout.cancel(promise)`.
*
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
* synchronously flush the queue of deferred functions.
*
- * @param {function()} fn A function, whose execution should be delayed.
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
* @param {number=} [delay=0] Delay in milliseconds.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
- * promise will be resolved with is the return value of the `fn` function.
- *
- * @example
-
-
-
-
-
-
- Date format:
- Current time is:
-
- Blood 1 : {{blood_1}}
- Blood 2 : {{blood_2}}
- Fight
- StopFight
- resetFight
-
-
-
-
-
- */
- function timeout(fn, delay, invokeApply) {
- var deferred = $q.defer(),
- promise = deferred.promise,
- skipApply = (isDefined(invokeApply) && !invokeApply),
- timeoutId;
+ var args = sliceArgs(arguments, 3),
+ skipApply = (isDefined(invokeApply) && !invokeApply),
+ deferred = (skipApply ? $$q : $q).defer(),
+ promise = deferred.promise,
+ timeoutId;
timeoutId = $browser.defer(function() {
try {
- deferred.resolve(fn());
- } catch(e) {
+ deferred.resolve(fn.apply(null, args));
+ } catch (e) {
deferred.reject(e);
$exceptionHandler(e);
}
@@ -12303,9 +19629,8 @@ function $TimeoutProvider() {
/**
- * @ngdoc function
- * @name ng.$timeout#cancel
- * @methodOf ng.$timeout
+ * @ngdoc method
+ * @name $timeout#cancel
*
* @description
* Cancels a task associated with the `promise`. As a result of this, the promise will be
@@ -12335,8 +19660,9 @@ 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 originUrl = urlResolve(window.location.href, true);
+var urlParsingNode = window.document.createElement("a");
+var originUrl = urlResolve(window.location.href);
+
/**
*
@@ -12352,20 +19678,13 @@ var originUrl = urlResolve(window.location.href, true);
*
* Implementation Notes for IE
* ---------------------------
- * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other
+ * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other
* browsers. However, the parsed components will not be set if the URL assigned did not specify
* them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We
* work around that by performing the parsing in a 2nd step by taking a previously normalized
- * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the
+ * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the
* properties such as protocol, hostname, port, etc.
*
- * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one
- * uses the inner HTML approach to assign the URL as part of an HTML snippet -
- * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL.
- * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception.
- * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that
- * method and IE < 8 is unsupported.
- *
* References:
* http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
@@ -12373,7 +19692,7 @@ var originUrl = urlResolve(window.location.href, true);
* https://github.com/angular/angular.js/pull/2902
* http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
*
- * @function
+ * @kind function
* @param {string} url The URL to be parsed.
* @description Normalizes and parses a URL.
* @returns {object} Returns the normalized URL as a dictionary.
@@ -12392,6 +19711,7 @@ var originUrl = urlResolve(window.location.href, true);
*/
function urlResolve(url) {
var href = url;
+
if (msie) {
// Normalize before parse. Refer Implementation Notes on why this is
// done in two steps on IE.
@@ -12401,7 +19721,7 @@ function urlResolve(url) {
urlParsingNode.setAttribute('href', href);
- // $$urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
+ // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
@@ -12410,11 +19730,12 @@ function urlResolve(url) {
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
hostname: urlParsingNode.hostname,
port: urlParsingNode.port,
- pathname: urlParsingNode.pathname && urlParsingNode.pathname.charAt(0) === '/' ? urlParsingNode.pathname : '/' + urlParsingNode.pathname
+ pathname: (urlParsingNode.pathname.charAt(0) === '/')
+ ? urlParsingNode.pathname
+ : '/' + urlParsingNode.pathname
};
}
-
/**
* Parse a request URL and determine whether this is a same-origin request as the application document.
*
@@ -12429,8 +19750,8 @@ function urlIsSameOrigin(requestUrl) {
}
/**
- * @ngdoc object
- * @name ng.$window
+ * @ngdoc service
+ * @name $window
*
* @description
* A reference to the browser's `window` object. While `window`
@@ -12444,42 +19765,118 @@ function urlIsSameOrigin(requestUrl) {
* expression.
*
* @example
-
-
+
+
-
-
-
ALERT
+
+
+ ALERT
-
-
+
+
it('should display the greeting in the input box', function() {
- input('greeting').enter('Hello, E2E Tests');
+ element(by.model('greeting')).sendKeys('Hello, E2E Tests');
// If we click the button it will block the test runner
// element(':button').click();
});
-
-
+
+
*/
-function $WindowProvider(){
+function $WindowProvider() {
this.$get = valueFn(window);
}
/**
- * @ngdoc object
- * @name ng.$filterProvider
+ * @name $$cookieReader
+ * @requires $document
+ *
* @description
+ * This is a private service for reading cookies used by $http and ngCookies
*
- * Filters are just functions which transform input to an output. However filters need to be Dependency Injected. To
- * achieve this a filter definition consists of a factory function which is annotated with dependencies and is
- * responsible for creating a filter function.
+ * @return {Object} a key/value map of the current cookies
+ */
+function $$CookieReader($document) {
+ var rawDocument = $document[0] || {};
+ var lastCookies = {};
+ var lastCookieString = '';
+
+ function safeDecodeURIComponent(str) {
+ try {
+ return decodeURIComponent(str);
+ } catch (e) {
+ return str;
+ }
+ }
+
+ return function() {
+ var cookieArray, cookie, i, index, name;
+ var currentCookieString = rawDocument.cookie || '';
+
+ if (currentCookieString !== lastCookieString) {
+ lastCookieString = currentCookieString;
+ cookieArray = lastCookieString.split('; ');
+ lastCookies = {};
+
+ for (i = 0; i < cookieArray.length; i++) {
+ cookie = cookieArray[i];
+ index = cookie.indexOf('=');
+ if (index > 0) { //ignore nameless cookies
+ name = safeDecodeURIComponent(cookie.substring(0, index));
+ // the first value that is seen for a cookie is the most
+ // specific one. values for the same cookie name that
+ // follow are for less specific paths.
+ if (isUndefined(lastCookies[name])) {
+ lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
+ }
+ }
+ }
+ }
+ return lastCookies;
+ };
+}
+
+$$CookieReader.$inject = ['$document'];
+
+function $$CookieReaderProvider() {
+ this.$get = $$CookieReader;
+}
+
+/* global currencyFilter: true,
+ dateFilter: true,
+ filterFilter: true,
+ jsonFilter: true,
+ limitToFilter: true,
+ lowercaseFilter: true,
+ numberFilter: true,
+ orderByFilter: true,
+ uppercaseFilter: true,
+ */
+
+/**
+ * @ngdoc provider
+ * @name $filterProvider
+ * @description
*
- *
+ * Filters are just functions which transform input to an output. However filters need to be
+ * Dependency Injected. To achieve this a filter definition consists of a factory function which is
+ * annotated with dependencies and is responsible for creating a filter function.
+ *
+ *
+ * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
+ * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
+ * (`myapp_subsection_filterx`).
+ *
+ *
+ * ```js
* // Filter registration
* function MyModule($provide, $filterProvider) {
* // create a service to demonstrate injection (not always needed)
@@ -12498,10 +19895,12 @@ function $WindowProvider(){
* };
* });
* }
- *
+ * ```
*
- * The filter function is registered with the `$injector` under the filter name suffix with `Filter`.
- *
+ * The filter function is registered with the `$injector` under the filter name suffix with
+ * `Filter`.
+ *
+ * ```js
* it('should be the same instance', inject(
* function($filterProvider) {
* $filterProvider.register('reverse', function(){
@@ -12511,29 +19910,17 @@ function $WindowProvider(){
* function($filter, reverseFilter) {
* expect($filter('reverse')).toBe(reverseFilter);
* });
- *
+ * ```
*
*
* For more information about how angular filters work, and how to create your own filters, see
- * {@link guide/dev_guide.templates.filters Understanding Angular Filters} in the angular Developer
- * Guide.
- */
-/**
- * @ngdoc method
- * @name ng.$filterProvider#register
- * @methodOf ng.$filterProvider
- * @description
- * Register filter factory function.
- *
- * @param {String} name Name of the filter.
- * @param {function} fn The filter factory function which is injectable.
+ * {@link guide/filter Filters} in the Angular Developer Guide.
*/
-
/**
- * @ngdoc function
- * @name ng.$filter
- * @function
+ * @ngdoc service
+ * @name $filter
+ * @kind function
* @description
* Filters are used for formatting data displayed to the user.
*
@@ -12543,22 +19930,46 @@ function $WindowProvider(){
*
* @param {String} name Name of the filter function to retrieve
* @return {Function} the filter function
- */
+ * @example
+
+
+
+
{{ originalText }}
+ {{ filteredText }}
+
+
+
+
+ angular.module('filterExample', [])
+ .controller('MainCtrl', function($scope, $filter) {
+ $scope.originalText = 'hello';
+ $scope.filteredText = $filter('uppercase')($scope.originalText);
+ });
+
+
+ */
$FilterProvider.$inject = ['$provide'];
function $FilterProvider($provide) {
var suffix = 'Filter';
/**
- * @ngdoc function
- * @name ng.$controllerProvider#register
- * @methodOf ng.$controllerProvider
+ * @ngdoc method
+ * @name $filterProvider#register
* @param {string|Object} name Name of the filter function, or an object map of filters where
* the keys are the filter names and the values are the filter factories.
+ *
+ *
+ * **Note:** Filter names must be valid angular {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
+ * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
+ * (`myapp_subsection_filterx`).
+ *
+ * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered.
* @returns {Object} Registered filter instance, or if a map of filters was provided then a map
* of the registered filter instances.
*/
function register(name, factory) {
- if(isObject(name)) {
+ if (isObject(name)) {
var filters = {};
forEach(name, function(filter, key) {
filters[key] = register(key, filter);
@@ -12578,6 +19989,18 @@ function $FilterProvider($provide) {
////////////////////////////////////////
+ /* global
+ currencyFilter: false,
+ dateFilter: false,
+ filterFilter: false,
+ jsonFilter: false,
+ limitToFilter: false,
+ lowercaseFilter: false,
+ numberFilter: false,
+ orderByFilter: false,
+ uppercaseFilter: false,
+ */
+
register('currency', currencyFilter);
register('date', dateFilter);
register('filter', filterFilter);
@@ -12591,55 +20014,70 @@ function $FilterProvider($provide) {
/**
* @ngdoc filter
- * @name ng.filter:filter
- * @function
+ * @name filter
+ * @kind function
*
* @description
* Selects a subset of items from `array` and returns it as a new array.
*
- * Note: This function is used to augment the `Array` type in Angular expressions. See
- * {@link ng.$filter} for more information about Angular arrays.
- *
* @param {Array} array The source array.
* @param {string|Object|function()} expression The predicate to be used for selecting items from
* `array`.
*
* Can be one of:
*
- * - `string`: Predicate that results in a substring match using the value of `expression`
- * string. All strings or objects with string properties in `array` that contain this string
- * will be returned. The predicate can be negated by prefixing the string with `!`.
+ * - `string`: The string is used for matching against the contents of the `array`. All strings or
+ * objects with string properties in `array` that match this string will be returned. This also
+ * applies to nested object properties.
+ * The predicate can be negated by prefixing the string with `!`.
*
* - `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. That's equivalent to the simple substring match with a `string`
- * as described above.
- *
- * - `function`: A predicate function can be used to write arbitrary filters. The function is
- * called for each element of `array`. The final result is an array of those elements that
- * the predicate returned true for.
- *
- * @param {function(expected, actual)|true|undefined} comparator Comparator which is used in
+ * 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".
+ *
+ * Note that a named property will match properties on the same level only, while the special
+ * `$` property will match properties on the same level or deeper. E.g. an array item like
+ * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but
+ * **will** be matched by `{$: 'John'}`.
+ *
+ * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters.
+ * The function is called for each element of the array, with the element, its index, and
+ * the entire array itself as arguments.
+ *
+ * The final result is an array of those elements that the predicate returned true for.
+ *
+ * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
* determining if the expected value (from the filter expression) and actual value (from
* the object in the array) should be considered a match.
*
* Can be one of:
*
- * - `function(expected, actual)`:
- * The function will be given the object value and the predicate value to compare and
- * should return true if the item should be included in filtered result.
+ * - `function(actual, expected)`:
+ * The function will be given the object value and the predicate value to compare and
+ * should return true if both values should be considered equal.
*
- * - `true`: A shorthand for `function(expected, actual) { return angular.equals(expected, actual)}`.
- * this is essentially strict comparison of expected and actual.
+ * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
+ * This is essentially strict comparison of expected and actual.
*
- * - `false|undefined`: A short hand for a function which will look for a substring match in case
- * insensitive way.
+ * - `false|undefined`: A short hand for a function which will look for a substring match in case
+ * insensitive way.
+ *
+ * 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
-
-
+
+
- Search:
+ Search:
Name Phone
@@ -12656,152 +20094,193 @@ function $FilterProvider($provide) {
- Any:
- Name only
- Phone only
- Equality
+ Any:
+ Name only
+ Phone only
+ Equality
Name Phone
-
- {{friend.name}}
- {{friend.phone}}
+
+ {{friendObj.name}}
+ {{friendObj.phone}}
-
-
- it('should search across all fields when filtering with a string', function() {
- input('searchText').enter('m');
- expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')).
- toEqual(['Mary', 'Mike', 'Adam']);
+
+
+ var expectFriendNames = function(expectedNames, key) {
+ element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) {
+ arr.forEach(function(wd, i) {
+ expect(wd.getText()).toMatch(expectedNames[i]);
+ });
+ });
+ };
- input('searchText').enter('76');
- expect(repeater('#searchTextResults tr', 'friend in friends').column('friend.name')).
- toEqual(['John', 'Julie']);
+ it('should search across all fields when filtering with a string', function() {
+ var searchText = element(by.model('searchText'));
+ searchText.clear();
+ searchText.sendKeys('m');
+ expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend');
+
+ searchText.clear();
+ searchText.sendKeys('76');
+ expectFriendNames(['John', 'Julie'], 'friend');
});
it('should search in specific fields when filtering with a predicate object', function() {
- input('search.$').enter('i');
- expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')).
- toEqual(['Mary', 'Mike', 'Julie', 'Juliette']);
+ var searchAny = element(by.model('search.$'));
+ searchAny.clear();
+ searchAny.sendKeys('i');
+ expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj');
});
it('should use a equal comparison when comparator is true', function() {
- input('search.name').enter('Julie');
- input('strict').check();
- expect(repeater('#searchObjResults tr', 'friend in friends').column('friend.name')).
- toEqual(['Julie']);
+ var searchName = element(by.model('search.name'));
+ var strict = element(by.model('strict'));
+ searchName.clear();
+ searchName.sendKeys('Julie');
+ strict.click();
+ expectFriendNames(['Julie'], 'friendObj');
});
-
-
+
+
*/
+
function filterFilter() {
- return function(array, expression, comperator) {
- if (!isArray(array)) return array;
- var predicates = [];
- predicates.check = function(value) {
- for (var j = 0; j < predicates.length; j++) {
- if(!predicates[j](value)) {
- return false;
- }
+ return function(array, expression, comparator, anyPropertyKey) {
+ if (!isArrayLike(array)) {
+ if (array == null) {
+ return array;
+ } else {
+ throw minErr('filter')('notarray', 'Expected array but received: {0}', array);
}
- return true;
- };
- switch(typeof comperator) {
- case "function":
- break;
- case "boolean":
- if(comperator == true) {
- comperator = function(obj, text) {
- return angular.equals(obj, text);
- }
- break;
- }
- default:
- comperator = function(obj, text) {
- text = (''+text).toLowerCase();
- return (''+obj).toLowerCase().indexOf(text) > -1;
- };
}
- var search = function(obj, text){
- if (typeof text == 'string' && text.charAt(0) === '!') {
- return !search(obj, text.substr(1));
- }
- switch (typeof obj) {
- case "boolean":
- case "number":
- case "string":
- return comperator(obj, text);
- case "object":
- switch (typeof text) {
- case "object":
- return comperator(obj, text);
- break;
- default:
- for ( var objKey in obj) {
- if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
- return true;
- }
- }
- break;
- }
- return false;
- case "array":
- for ( var i = 0; i < obj.length; i++) {
- if (search(obj[i], text)) {
- return true;
- }
- }
- return false;
- default:
- return false;
- }
- };
- switch (typeof expression) {
- case "boolean":
- case "number":
- case "string":
- expression = {$:expression};
- case "object":
- for (var key in expression) {
- if (key == '$') {
- (function() {
- if (!expression[key]) return;
- var path = key
- predicates.push(function(value) {
- return search(value, expression[path]);
- });
- })();
- } else {
- (function() {
- if (typeof(expression[key]) == 'undefined') { return; }
- var path = key;
- predicates.push(function(value) {
- return search(getter(value,path), expression[path]);
- });
- })();
- }
- }
- break;
+
+ anyPropertyKey = anyPropertyKey || '$';
+ var expressionType = getTypeForFilter(expression);
+ var predicateFn;
+ var matchAgainstAnyProp;
+
+ switch (expressionType) {
case 'function':
- predicates.push(expression);
+ predicateFn = expression;
+ break;
+ case 'boolean':
+ case 'null':
+ case 'number':
+ case 'string':
+ matchAgainstAnyProp = true;
+ //jshint -W086
+ case 'object':
+ //jshint +W086
+ predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
return array;
}
- var filtered = [];
- for ( var j = 0; j < array.length; j++) {
- var value = array[j];
- if (predicates.check(value)) {
- filtered.push(value);
+
+ return Array.prototype.filter.call(array, predicateFn);
+ };
+}
+
+// Helper functions for `filterFilter`
+function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) {
+ var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression);
+ var predicateFn;
+
+ if (comparator === true) {
+ comparator = equals;
+ } else if (!isFunction(comparator)) {
+ comparator = function(actual, expected) {
+ if (isUndefined(actual)) {
+ // No substring matching against `undefined`
+ return false;
+ }
+ if ((actual === null) || (expected === null)) {
+ // No substring matching against `null`; only match against `null`
+ return actual === expected;
+ }
+ if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) {
+ // Should not compare primitives against objects, unless they have custom `toString` method
+ return false;
}
+
+ actual = lowercase('' + actual);
+ expected = lowercase('' + expected);
+ return actual.indexOf(expected) !== -1;
+ };
+ }
+
+ predicateFn = function(item) {
+ if (shouldMatchPrimitives && !isObject(item)) {
+ return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false);
}
- return filtered;
+ return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp);
+ };
+
+ return predicateFn;
+}
+
+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, 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, anyPropertyKey, matchAgainstAnyProp);
+ });
+ }
+
+ switch (actualType) {
+ case 'object':
+ var key;
+ if (matchAgainstAnyProp) {
+ for (key in actual) {
+ if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
+ return true;
+ }
+ }
+ return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false);
+ } else if (expectedType === 'object') {
+ for (key in expected) {
+ var expectedVal = expected[key];
+ if (isFunction(expectedVal) || isUndefined(expectedVal)) {
+ continue;
+ }
+
+ var matchAnyProperty = key === anyPropertyKey;
+ var actualVal = matchAnyProperty ? actual : actual[key];
+ if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return comparator(actual, expected);
+ }
+ break;
+ case 'function':
+ return false;
+ default:
+ return comparator(actual, expected);
}
}
+// Used for easily differentiating between `null` and actual `object`
+function getTypeForFilter(val) {
+ return (val === null) ? 'null' : typeof val;
+}
+
+var MAX_DIGITS = 22;
+var DECIMAL_SEP = '.';
+var ZERO_CHAR = '0';
+
/**
* @ngdoc filter
- * @name ng.filter:currency
- * @function
+ * @name currency
+ * @kind function
*
* @description
* Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default
@@ -12809,215 +20288,386 @@ function filterFilter() {
*
* @param {number} amount Input to filter.
* @param {string=} symbol Currency symbol or identifier to be displayed.
+ * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale
* @returns {string} Formatted number.
*
*
* @example
-
-
+
+
-
-
- default currency symbol ($): {{amount | currency}}
- custom currency identifier (USD$): {{amount | currency:"USD$"}}
+
+
+ default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
+ no fractions (0): {{amount | currency:"USD$":0}}
-
-
+
+
it('should init with 1234.56', function() {
- expect(binding('amount | currency')).toBe('$1,234.56');
- expect(binding('amount | currency:"USD$"')).toBe('USD$1,234.56');
+ expect(element(by.id('currency-default')).getText()).toBe('$1,234.56');
+ expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56');
+ expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
});
it('should update', function() {
- input('amount').enter('-1234');
- expect(binding('amount | currency')).toBe('($1,234.00)');
- expect(binding('amount | currency:"USD$"')).toBe('(USD$1,234.00)');
+ if (browser.params.browser == 'safari') {
+ // Safari does not understand the minus key. See
+ // https://github.com/angular/protractor/issues/481
+ return;
+ }
+ element(by.model('amount')).clear();
+ element(by.model('amount')).sendKeys('-1234');
+ expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00');
+ expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00');
+ expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234');
});
-
-
+
+
*/
currencyFilter.$inject = ['$locale'];
function currencyFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
- return function(amount, currencySymbol){
- if (isUndefined(currencySymbol)) currencySymbol = formats.CURRENCY_SYM;
- return formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, 2).
- replace(/\u00A4/g, currencySymbol);
+ return function(amount, currencySymbol, fractionSize) {
+ if (isUndefined(currencySymbol)) {
+ currencySymbol = formats.CURRENCY_SYM;
+ }
+
+ if (isUndefined(fractionSize)) {
+ fractionSize = formats.PATTERNS[1].maxFrac;
+ }
+
+ // if null or undefined pass it through
+ return (amount == null)
+ ? amount
+ : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
+ replace(/\u00A4/g, currencySymbol);
};
}
/**
* @ngdoc filter
- * @name ng.filter:number
- * @function
+ * @name number
+ * @kind function
*
* @description
* Formats a number as text.
*
+ * If the input is null or undefined, it will just be returned.
+ * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively.
* If the input is not a number an empty string is returned.
*
+ *
* @param {number|string} number Number to format.
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
- * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
+ * @returns {string} Number rounded to `fractionSize` 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
-
-
+
+
-
- Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}}
+
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}}
-
-
+
+
it('should format numbers', function() {
- expect(binding('val | number')).toBe('1,234.568');
- expect(binding('val | number:0')).toBe('1,235');
- expect(binding('-val | number:4')).toBe('-1,234.5679');
+ expect(element(by.id('number-default')).getText()).toBe('1,234.568');
+ expect(element(by.binding('val | number:0')).getText()).toBe('1,235');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679');
});
it('should update', function() {
- input('val').enter('3374.333');
- expect(binding('val | number')).toBe('3,374.333');
- expect(binding('val | number:0')).toBe('3,374');
- expect(binding('-val | number:4')).toBe('-3,374.3330');
- });
-
-
+ element(by.model('val')).clear();
+ element(by.model('val')).sendKeys('3374.333');
+ expect(element(by.id('number-default')).getText()).toBe('3,374.333');
+ expect(element(by.binding('val | number:0')).getText()).toBe('3,374');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330');
+ });
+
+
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
return function(number, fractionSize) {
- return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
- fractionSize);
+
+ // if null or undefined pass it through
+ return (number == null)
+ ? number
+ : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
+ fractionSize);
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (isNaN(number) || !isFinite(number)) return '';
-
- var isNegative = number < 0;
- number = Math.abs(number);
- var numStr = number + '',
- formatedText = '',
- parts = [];
-
- var hasExponent = false;
- if (numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- numStr = '0';
- } else {
- formatedText = numStr;
- hasExponent = true;
- }
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
+
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
+ }
+
+ // Exponential form?
+ if ((i = numStr.search(/e/i)) > 0) {
+ // Work out the exponent.
+ if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
+ numberOfIntegerDigits += +numStr.slice(i + 1);
+ numStr = numStr.substring(0, i);
+ } else if (numberOfIntegerDigits < 0) {
+ // There was no decimal point or exponent so it is an integer.
+ numberOfIntegerDigits = numStr.length;
}
- if (!hasExponent) {
- var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */}
- // determine fractionSize if it is not specified
- if (isUndefined(fractionSize)) {
- fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
+ if (i == (zeros = numStr.length)) {
+ // The digits are all zero.
+ digits = [0];
+ numberOfIntegerDigits = 1;
+ } else {
+ // Count the number of trailing zeros
+ zeros--;
+ while (numStr.charAt(zeros) == ZERO_CHAR) zeros--;
+
+ // Trailing zeros are insignificant so ignore them
+ numberOfIntegerDigits -= i;
+ digits = [];
+ // Convert string to array of digits without leading/trailing zeros.
+ for (j = 0; i <= zeros; i++, j++) {
+ digits[j] = +numStr.charAt(i);
}
+ }
+
+ // If the number overflows the maximum allowed digits then use an exponent.
+ if (numberOfIntegerDigits > MAX_DIGITS) {
+ digits = digits.splice(0, MAX_DIGITS - 1);
+ exponent = numberOfIntegerDigits - 1;
+ numberOfIntegerDigits = 1;
+ }
- var pow = Math.pow(10, fractionSize);
- number = Math.round(number * pow) / pow;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
+}
- var pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
+/**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (var i = 0; i < pos; i++) {
- if ((pos - i)%group === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
+
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
+
+ if (roundAt > 0) {
+ // 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 = Math.max(1, roundAt = fractionSize + 1);
+ digits[0] = 0;
+ for (var i = 1; i < roundAt; i++) digits[i] = 0;
}
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i)%lgroup === 0 && i !== 0) {
- formatedText += groupSep;
+ 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]++;
}
- formatedText += whole.charAt(i);
}
- // format fraction part.
- while(fraction.length < fractionSize) {
- fraction += '0';
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
+
+
+ // Do any carrying, e.g. a digit was rounded up to 10
+ var carry = digits.reduceRight(function(carry, d, i, digits) {
+ d = d + carry;
+ digits[i] = d % 10;
+ return Math.floor(d / 10);
+ }, 0);
+ if (carry) {
+ digits.unshift(carry);
+ parsedNumber.i++;
}
+}
+
+/**
+ * Format a number into a string
+ * @param {number} number The number to format
+ * @param {{
+ * minFrac, // the minimum number of digits required in the fraction part of the number
+ * maxFrac, // the maximum number of digits required in the fraction part of the number
+ * gSize, // number of digits in each group of separated digits
+ * lgSize, // number of digits in the last group of digits before the decimal separator
+ * negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
+ * posPre, // the string to go in front of a positive number
+ * negSuf, // the string to go after a negative number (e.g. `)`)
+ * posSuf // the string to go after a positive number
+ * }} pattern
+ * @param {string} groupSep The string to separate groups of number (e.g. `,`)
+ * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
+ * @param {[type]} fractionSize The size of the fractional part of the number
+ * @return {string} The number formatted as a string
+ */
+function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
+
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
} else {
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
- if (fractionSize > 0 && number > -1 && number < 1) {
- formatedText = number.toFixed(fractionSize);
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
+ }
+
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen, digits.length);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
}
- }
- parts.push(isNegative ? pattern.negPre : pattern.posPre);
- parts.push(formatedText);
- parts.push(isNegative ? pattern.negSuf : pattern.posSuf);
- return parts.join('');
+ if (exponent) {
+ formattedText += 'e+' + exponent;
+ }
+ }
+ if (number < 0 && !isZero) {
+ return pattern.negPre + formattedText + pattern.negSuf;
+ } else {
+ return pattern.posPre + formattedText + pattern.posSuf;
+ }
}
-function padNumber(num, digits, trim) {
+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 = '0' + num;
- if (trim)
+ while (num.length < digits) num = ZERO_CHAR + num;
+ if (trim) {
num = num.substr(num.length - digits);
+ }
return neg + num;
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
- if (offset > 0 || value > -offset)
+ if (offset > 0 || value > -offset) {
value += offset;
- if (value === 0 && offset == -12 ) value = 12;
- return padNumber(value, size, trim);
+ }
+ if (value === 0 && offset == -12) value = 12;
+ 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];
};
}
-function timeZoneGetter(date) {
- var zone = -1 * date.getTimezoneOffset();
+function timeZoneGetter(date, formats, offset) {
+ var zone = -1 * offset;
var paddedZone = (zone >= 0) ? "+" : "";
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
@@ -13026,18 +20676,53 @@ function timeZoneGetter(date) {
return paddedZone;
}
-function ampmGetter(date, formats) {
- return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
+function getFirstThursdayOfYear(year) {
+ // 0 = index of January
+ var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay();
+ // 4 = index of Thursday (+1 to account for 1st = 5)
+ // 11 = index of *next* Thursday (+1 account for 1st = 12)
+ return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst);
}
-var DATE_FORMATS = {
- yyyy: dateGetter('FullYear', 4),
- yy: dateGetter('FullYear', 2, 0, true),
- y: dateGetter('FullYear', 1),
- MMMM: dateStrGetter('Month'),
- MMM: dateStrGetter('Month', true),
+function getThursdayThisWeek(datetime) {
+ return new Date(datetime.getFullYear(), datetime.getMonth(),
+ // 4 = index of Thursday
+ datetime.getDate() + (4 - datetime.getDay()));
+}
+
+function weekGetter(size) {
+ return function(date) {
+ var firstThurs = getFirstThursdayOfYear(date.getFullYear()),
+ thisThurs = getThursdayThisWeek(date);
+
+ var diff = +thisThurs - +firstThurs,
+ result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
+
+ return padNumber(result, size);
+ };
+}
+
+function ampmGetter(date, formats) {
+ return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
+}
+
+function eraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1];
+}
+
+function longEraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1];
+}
+
+var DATE_FORMATS = {
+ 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),
@@ -13054,16 +20739,22 @@ var DATE_FORMATS = {
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true),
a: ampmGetter,
- Z: timeZoneGetter
+ Z: timeZoneGetter,
+ ww: weekGetter(2),
+ w: weekGetter(1),
+ G: eraGetter,
+ GG: eraGetter,
+ GGG: eraGetter,
+ GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;
/**
* @ngdoc filter
- * @name ng.filter:date
- * @function
+ * @name date
+ * @kind function
*
* @description
* Formats `date` to a string based on the requested `format`.
@@ -13077,69 +20768,82 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
* * `'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)
* * `'EEE'`: Day in Week, (Sun-Sat)
* * `'HH'`: Hour in day, padded (00-23)
* * `'H'`: Hour in day (0-23)
- * * `'hh'`: Hour in am/pm, padded (01-12)
- * * `'h'`: Hour in am/pm, (1-12)
+ * * `'hh'`: Hour in AM/PM, padded (01-12)
+ * * `'h'`: Hour in AM/PM, (1-12)
* * `'mm'`: Minute in hour, padded (00-59)
* * `'m'`: Minute in hour (0-59)
* * `'ss'`: Second in minute, padded (00-59)
* * `'s'`: Second in minute (0-59)
- * * `'.sss' or ',sss'`: Millisecond in second, padded (000-999)
- * * `'a'`: am/pm marker
+ * * `'sss'`: Millisecond in second, padded (000-999)
+ * * `'a'`: AM/PM marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
+ * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year
+ * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year
+ * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD')
+ * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini')
*
* `format` string can also be one of the following predefined
* {@link guide/i18n localizable formats}:
*
* * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
- * (e.g. Sep 3, 2010 12:05:08 pm)
- * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm)
- * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale
+ * (e.g. Sep 3, 2010 12:05:08 PM)
+ * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM)
+ * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale
* (e.g. Friday, September 3, 2010)
* * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010)
* * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
* * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
- * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm)
- * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm)
+ * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM)
+ * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM)
*
- * `format` string can contain literal values. These need to be quoted with single quotes (e.g.
- * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence
+ * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g.
+ * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
* (e.g. `"h 'o''clock'"`).
*
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
- * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its
+ * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
* specified in the string input, the time is considered to be in the local timezone.
* @param {string=} format Formatting rules (see Description). If not specified,
* `mediumDate` is used.
+ * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
-
-
+
+
{{1288323623006 | date:'medium'}} :
- {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'medium'}}
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}} :
- {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}} :
- {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
-
+ {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+ {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}} :
+ {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
+
+
it('should format date', function() {
- expect(binding("1288323623006 | date:'medium'")).
+ expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
- expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).
+ expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
- expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).
+ expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
+ expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
+ toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/);
});
-
-
+
+
*/
dateFilter.$inject = ['$locale'];
function dateFilter($locale) {
@@ -13157,14 +20861,14 @@ function dateFilter($locale) {
timeSetter = match[8] ? date.setUTCHours : date.setHours;
if (match[9]) {
- tzHour = int(match[9] + match[10]);
- tzMin = int(match[9] + match[11]);
- }
- dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3]));
- var h = int(match[4]||0) - tzHour;
- var m = int(match[5]||0) - tzMin
- var s = int(match[6]||0);
- var ms = Math.round(parseFloat('0.' + (match[7]||0)) * 1000);
+ tzHour = toInt(match[9] + match[10]);
+ tzMin = toInt(match[9] + match[11]);
+ }
+ dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
+ var h = toInt(match[4] || 0) - tzHour;
+ var m = toInt(match[5] || 0) - tzMin;
+ var s = toInt(match[6] || 0);
+ var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
}
@@ -13172,7 +20876,7 @@ function dateFilter($locale) {
}
- return function(date, format) {
+ return function(date, format, timezone) {
var text = '',
parts = [],
fn, match;
@@ -13180,22 +20884,18 @@ function dateFilter($locale) {
format = format || 'mediumDate';
format = $locale.DATETIME_FORMATS[format] || format;
if (isString(date)) {
- if (NUMBER_STRING.test(date)) {
- date = int(date);
- } else {
- date = jsonStringToDate(date);
- }
+ date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
}
if (isNumber(date)) {
date = new Date(date);
}
- if (!isDate(date)) {
+ if (!isDate(date) || !isFinite(date.getTime())) {
return date;
}
- while(format) {
+ while (format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
parts = concat(parts, match, 1);
@@ -13206,10 +20906,15 @@ function dateFilter($locale) {
}
}
- forEach(parts, function(value){
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ if (timezone) {
+ dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ date = convertTimezoneToLocal(date, timezone, true);
+ }
+ forEach(parts, function(value) {
fn = DATE_FORMATS[value];
- text += fn ? fn(date, $locale.DATETIME_FORMATS)
- : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
+ : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
});
return text;
@@ -13219,8 +20924,8 @@ function dateFilter($locale) {
/**
* @ngdoc filter
- * @name ng.filter:json
- * @function
+ * @name json
+ * @kind function
*
* @description
* Allows you to convert a JavaScript object into JSON string.
@@ -13229,33 +20934,39 @@ function dateFilter($locale) {
* the binding is automatically converted to JSON.
*
* @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
+ * @param {number=} spacing The number of spaces to use per indentation, defaults to 2.
* @returns {string} JSON string.
*
*
- * @example:
-
-
- {{ {'name':'value'} | json }}
-
-
+ * @example
+
+
+ {{ {'name':'value'} | json }}
+ {{ {'name':'value'} | json:4 }}
+
+
it('should jsonify filtered objects', function() {
- expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
});
-
-
+
+
*
*/
function jsonFilter() {
- return function(object) {
- return toJson(object, true);
+ return function(object, spacing) {
+ if (isUndefined(spacing)) {
+ spacing = 2;
+ }
+ return toJson(object, spacing);
};
}
/**
* @ngdoc filter
- * @name ng.filter:lowercase
- * @function
+ * @name lowercase
+ * @kind function
* @description
* Converts string to lowercase.
* @see angular.lowercase
@@ -13265,8 +20976,8 @@ var lowercaseFilter = valueFn(lowercase);
/**
* @ngdoc filter
- * @name ng.filter:uppercase
- * @function
+ * @name uppercase
+ * @kind function
* @description
* Converts string to uppercase.
* @see angular.uppercase
@@ -13274,243 +20985,825 @@ var lowercaseFilter = valueFn(lowercase);
var uppercaseFilter = valueFn(uppercase);
/**
- * @ngdoc function
- * @name ng.filter:limitTo
- * @function
+ * @ngdoc filter
+ * @name limitTo
+ * @kind function
*
* @description
- * Creates a new array or string containing only a specified number of elements. The elements
- * are taken from either the beginning or the end of the source array or string, as specified by
- * the value and sign (positive or negative) of `limit`.
- *
- * Note: This function is used to augment the `Array` type in Angular expressions. See
- * {@link ng.$filter} for more information about Angular arrays.
- *
- * @param {Array|string} input Source array or string 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`
- * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
- * had less than `limit` elements.
+ * 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 the input had
+ * less than `limit` elements.
*
* @example
-
-
+
+
-
- Limit {{numbers}} to:
+
-
-
+
+
+ var numLimitInput = element(by.model('numLimit'));
+ var letterLimitInput = element(by.model('letterLimit'));
+ var longNumberLimitInput = element(by.model('longNumberLimit'));
+ var limitedNumbers = element(by.binding('numbers | limitTo:numLimit'));
+ var limitedLetters = element(by.binding('letters | limitTo:letterLimit'));
+ var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit'));
+
it('should limit the number array to first three items', function() {
- expect(element('.doc-example-live input[ng-model=numLimit]').val()).toBe('3');
- expect(element('.doc-example-live input[ng-model=letterLimit]').val()).toBe('3');
- expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3]');
- expect(binding('letters | limitTo:letterLimit')).toEqual('abc');
+ expect(numLimitInput.getAttribute('value')).toBe('3');
+ expect(letterLimitInput.getAttribute('value')).toBe('3');
+ expect(longNumberLimitInput.getAttribute('value')).toBe('3');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abc');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 234');
});
- it('should update the output when -3 is entered', function() {
- input('numLimit').enter(-3);
- input('letterLimit').enter(-3);
- expect(binding('numbers | limitTo:numLimit')).toEqual('[7,8,9]');
- expect(binding('letters | limitTo:letterLimit')).toEqual('ghi');
- });
+ // There is a bug in safari and protractor that doesn't like the minus key
+ // it('should update the output when -3 is entered', function() {
+ // numLimitInput.clear();
+ // numLimitInput.sendKeys('-3');
+ // letterLimitInput.clear();
+ // letterLimitInput.sendKeys('-3');
+ // longNumberLimitInput.clear();
+ // longNumberLimitInput.sendKeys('-3');
+ // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
+ // expect(limitedLetters.getText()).toEqual('Output letters: ghi');
+ // expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
+ // });
it('should not exceed the maximum size of input array', function() {
- input('numLimit').enter(100);
- input('letterLimit').enter(100);
- expect(binding('numbers | limitTo:numLimit')).toEqual('[1,2,3,4,5,6,7,8,9]');
- expect(binding('letters | limitTo:letterLimit')).toEqual('abcdefghi');
+ numLimitInput.clear();
+ numLimitInput.sendKeys('100');
+ letterLimitInput.clear();
+ letterLimitInput.sendKeys('100');
+ longNumberLimitInput.clear();
+ longNumberLimitInput.sendKeys('100');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342');
});
-
-
- */
-function limitToFilter(){
- return function(input, limit) {
- if (!isArray(input) && !isString(input)) return input;
-
- limit = int(limit);
-
- if (isString(input)) {
- //NaN check on limit
- if (limit) {
- return limit >= 0 ? input.slice(0, limit) : input.slice(limit, input.length);
- } else {
- return "";
- }
+
+
+*/
+function limitToFilter() {
+ return function(input, limit, begin) {
+ if (Math.abs(Number(limit)) === Infinity) {
+ limit = Number(limit);
+ } else {
+ limit = toInt(limit);
}
+ if (isNaN(limit)) return input;
- var out = [],
- i, n;
+ if (isNumber(input)) input = input.toString();
+ if (!isArrayLike(input)) return input;
- // if abs(limit) exceeds maximum length, trim it
- if (limit > input.length)
- limit = input.length;
- else if (limit < -input.length)
- limit = -input.length;
+ begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
+ begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
- if (limit > 0) {
- i = 0;
- n = limit;
+ if (limit >= 0) {
+ return sliceFn(input, begin, begin + limit);
} else {
- i = input.length + limit;
- n = input.length;
+ if (begin === 0) {
+ return sliceFn(input, limit, input.length);
+ } else {
+ return sliceFn(input, Math.max(0, begin + limit), begin);
+ }
}
+ };
+}
- for (; i
+ * (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.
+ * (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.
)=} expression - A predicate (or list of
+ * predicates) to be used by the comparator to determine the order of elements.
*
- * Note: this function is used to augment the `Array` type in Angular expressions. See
- * {@link ng.$filter} for more information about Angular arrays.
+ * Can be one of:
*
- * @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.
+ * - `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.
+ * (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`.)
+ * 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.
*
- * Can be one of:
+ * **Note:** If the predicate is missing or empty then it defaults to `'+'`.
*
- * - `function`: Getter function. The result of this function will be sorted using the
- * `<`, `=`, `>` operator.
- * - `string`: An Angular expression which evaluates to an object to order by, such as 'name'
- * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control
- * ascending or descending sort order (for example, +name or -name).
- * - `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.
+ * @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.
+ *
+ * @returns {Array} - The sorted array.
*
- * @param {boolean=} reverse Reverse the order the array.
- * @returns {Array} Sorted copy of the source array.
*
* @example
-
-
-
-
-
Sorting predicate = {{predicate}}; reverse = {{reverse}}
+ * ### 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.
+ *
+
+
+
+
+
+ Name
+ Phone Number
+ Age
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample1', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var names = element.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by age in reverse order', function() {
+ expect(names.get(0).getText()).toBe('Adam');
+ expect(names.get(1).getText()).toBe('Julie');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('John');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Changing parameters dynamically
+ *
+ * All parameters can be changed dynamically. The next example shows how you can make the columns of
+ * a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
+
+
+
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample2', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = friends;
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Using `orderBy` inside a controller
+ *
+ * It 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')`.)
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
- [
unsorted ]
-
+
- Name
- (^ )
- Phone Number
- Age
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
-
+
{{friend.name}}
{{friend.phone}}
{{friend.age}}
-
-
- it('should be reverse ordered by aged', function() {
- expect(binding('predicate')).toBe('-age');
- expect(repeater('table.friend', 'friend in friends').column('friend.age')).
- toEqual(['35', '29', '21', '19', '10']);
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
+
+
+ angular.module('orderByExample3', [])
+ .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
+ ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
});
+
+
+ *
+ *
+ * @example
+ * ### Using a custom comparator
+ *
+ * If you have very specific requirements about the way items are sorted, you can pass your own
+ * comparator function. For example, you might need to compare some strings in a locale-sensitive
+ * way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
+ * argument - passing `false` retains the default sorting order, i.e. ascending.)
+ *
+
+
+
+
+
Locale-sensitive Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
Default Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
+
+ angular.module('orderByExample4', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', favoriteLetter: 'Ä'},
+ {name: 'Mary', favoriteLetter: 'Ü'},
+ {name: 'Mike', favoriteLetter: 'Ö'},
+ {name: 'Adam', favoriteLetter: 'H'},
+ {name: 'Julie', favoriteLetter: 'Z'}
+ ];
+
+ $scope.localeSensitiveComparator = function(v1, v2) {
+ // If we don't get strings, just compare by index
+ if (v1.type !== 'string' || v2.type !== 'string') {
+ return (v1.index < v2.index) ? -1 : 1;
+ }
+
+ // Compare strings alphabetically, taking locale into account
+ return v1.value.localeCompare(v2.value);
+ };
+ }]);
+
+
+ .friends-container {
+ display: inline-block;
+ margin: 0 30px;
+ }
+
+ .friends {
+ border-collapse: collapse;
+ }
- it('should reorder the table when user selects different predicate', function() {
- element('.doc-example-live a:contains("Name")').click();
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
- expect(repeater('table.friend', 'friend in friends').column('friend.age')).
- toEqual(['35', '10', '29', '19', '21']);
-
- element('.doc-example-live a:contains("Phone")').click();
- expect(repeater('table.friend', 'friend in friends').column('friend.phone')).
- toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var container = element(by.css('.custom-comparator'));
+ var names = container.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by favorite letter (in correct alphabetical order)', function() {
+ expect(names.get(0).getText()).toBe('John');
+ expect(names.get(1).getText()).toBe('Adam');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('Julie');
});
-
-
+
+
+ *
*/
orderByFilter.$inject = ['$parse'];
-function orderByFilter($parse){
- return function(array, sortPredicate, reverseOrder) {
- if (!isArray(array)) return array;
- if (!sortPredicate) return array;
- sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate];
- sortPredicate = map(sortPredicate, function(predicate){
- var descending = false, get = predicate || identity;
- if (isString(predicate)) {
+function orderByFilter($parse) {
+ return function(array, sortPredicate, reverseOrder, compareFn) {
+
+ 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);
+
+ 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)
+ // See https://en.wikipedia.org/wiki/Schwartzian_transform
+ var compareValues = Array.prototype.map.call(array, getComparisonObject);
+ compareValues.sort(doComparison);
+ array = compareValues.map(function(item) { return item.value; });
+
+ return array;
+
+ function getComparisonObject(value, index) {
+ // 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);
+ })
+ };
+ }
+
+ function doComparison(v1, v2) {
+ 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 compare(v1.tieBreaker, v2.tieBreaker) * descending;
+ }
+ };
+
+ function processPredicates(sortPredicates) {
+ return sortPredicates.map(function(predicate) {
+ var descending = 1, get = identity;
+
+ if (isFunction(predicate)) {
+ get = predicate;
+ } else if (isString(predicate)) {
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-';
+ descending = predicate.charAt(0) == '-' ? -1 : 1;
predicate = predicate.substring(1);
}
- get = $parse(predicate);
+ if (predicate !== '') {
+ get = $parse(predicate);
+ if (get.constant) {
+ var key = get();
+ get = function(value) { return value[key]; };
+ }
+ }
}
- return reverseComparator(function(a,b){
- return compare(get(a),get(b));
- }, descending);
+ return {get: get, descending: descending};
});
- var arrayCopy = [];
- for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); }
- return arrayCopy.sort(reverseComparator(comparator, reverseOrder));
-
- function comparator(o1, o2){
- for ( var i = 0; i < sortPredicate.length; i++) {
- var comp = sortPredicate[i](o1, o2);
- if (comp !== 0) return comp;
- }
- return 0;
- }
- function reverseComparator(comp, descending) {
- return toBoolean(descending)
- ? function(a,b){return comp(b,a);}
- : comp;
- }
- function compare(v1, v2){
- var t1 = typeof v1;
- var t2 = typeof v2;
- if (t1 == t2) {
- if (t1 == "string") {
- v1 = v1.toLowerCase();
- v2 = v2.toLowerCase();
- }
- if (v1 === v2) return 0;
- return v1 < v2 ? -1 : 1;
- } else {
- return t1 < t2 ? -1 : 1;
+ }
+
+ function isPrimitive(value) {
+ switch (typeof value) {
+ case 'number': /* falls through */
+ case 'boolean': /* falls through */
+ case 'string':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ function objectValue(value) {
+ // If `valueOf` is a valid function use that
+ if (isFunction(value.valueOf)) {
+ value = value.valueOf();
+ if (isPrimitive(value)) return value;
+ }
+ // If `toString` is a valid function and not the one from `Object.prototype` use that
+ if (hasCustomToString(value)) {
+ value = value.toString();
+ if (isPrimitive(value)) return value;
+ }
+
+ return value;
+ }
+
+ function getPredicateValue(value, index) {
+ var type = typeof value;
+ if (value === null) {
+ type = 'string';
+ value = 'null';
+ } else if (type === 'object') {
+ value = objectValue(value);
+ }
+ return {value: value, type: type, index: index};
+ }
+
+ function defaultCompare(v1, v2) {
+ var result = 0;
+ 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 = type1 < type2 ? -1 : 1;
}
+
+ return result;
}
}
@@ -13518,7 +21811,7 @@ function ngDirective(directive) {
if (isFunction(directive)) {
directive = {
link: directive
- }
+ };
}
directive.restrict = directive.restrict || 'AC';
return valueFn(directive);
@@ -13526,7 +21819,7 @@ function ngDirective(directive) {
/**
* @ngdoc directive
- * @name ng.directive:a
+ * @name a
* @restrict E
*
* @description
@@ -13540,56 +21833,48 @@ function ngDirective(directive) {
var htmlAnchorDirective = valueFn({
restrict: 'E',
compile: function(element, attr) {
-
- if (msie <= 8) {
-
- // turn link into a stylable link in IE
- // but only if it doesn't have name attribute, in which case it's an anchor
- if (!attr.href && !attr.name) {
- attr.$set('href', '');
- }
-
- // add a comment node to anchors to workaround IE bug that causes element content to be reset
- // to new attribute content if attribute is updated with value containing @ and element also
- // contains value with @
- // see issue #1949
- element.append(document.createComment('IE fix'));
- }
-
- return function(scope, element) {
- element.on('click', function(event){
- // if we have no href url, then don't navigate anywhere.
- if (!element.attr('href')) {
- event.preventDefault();
- }
- });
+ if (!attr.href && !attr.xlinkHref) {
+ return function(scope, element) {
+ // If the linked element is not an anchor tag anymore, do nothing
+ if (element[0].nodeName.toLowerCase() !== 'a') return;
+
+ // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
+ var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
+ 'xlink:href' : 'href';
+ element.on('click', function(event) {
+ // if we have no href url, then don't navigate anywhere.
+ if (!element.attr(href)) {
+ event.preventDefault();
+ }
+ });
+ };
}
}
});
/**
* @ngdoc directive
- * @name ng.directive:ngHref
+ * @name ngHref
* @restrict A
+ * @priority 99
*
* @description
* Using Angular markup like `{{hash}}` in an href attribute will
* make the link go to the wrong URL if the user clicks it before
* Angular has a chance to replace the `{{hash}}` markup with its
* value. Until Angular replaces the markup the link will be broken
- * and will most likely return a 404 error.
- *
- * The `ngHref` directive solves this problem.
+ * and will most likely return a 404 error. The `ngHref` directive
+ * solves this problem.
*
* The wrong way to write it:
- *
- *
- *
+ * ```html
+ * link1
+ * ```
*
* The correct way to write it:
- *
- *
- *
+ * ```html
+ * link1
+ * ```
*
* @element A
* @param {template} ngHref any string which can contain `{{}}` markup.
@@ -13597,8 +21882,8 @@ var htmlAnchorDirective = valueFn({
* @example
* This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
* in links and their different behaviors:
-
-
+
+
link 1 (link, don't reload)
link 2 (link, don't reload)
@@ -13606,54 +21891,71 @@ var htmlAnchorDirective = valueFn({
anchor (link, don't reload)
anchor (no link)
link (link, change location)
-
-
+
+
it('should execute ng-click but not reload when href without value', function() {
- element('#link-1').click();
- expect(input('value').val()).toEqual('1');
- expect(element('#link-1').attr('href')).toBe("");
+ element(by.id('link-1')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('1');
+ expect(element(by.id('link-1')).getAttribute('href')).toBe('');
});
it('should execute ng-click but not reload when href empty string', function() {
- element('#link-2').click();
- expect(input('value').val()).toEqual('2');
- expect(element('#link-2').attr('href')).toBe("");
+ element(by.id('link-2')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('2');
+ expect(element(by.id('link-2')).getAttribute('href')).toBe('');
});
it('should execute ng-click and change url when ng-href specified', function() {
- expect(element('#link-3').attr('href')).toBe("/123");
+ expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/);
+
+ element(by.id('link-3')).click();
- element('#link-3').click();
- expect(browser().window().path()).toEqual('/123');
+ // At this point, we navigate away from an Angular page, so we need
+ // to use browser.driver to get the base webdriver.
+
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/123$/);
+ });
+ }, 5000, 'page should navigate to /123');
});
it('should execute ng-click but not reload when href empty string and name specified', function() {
- element('#link-4').click();
- expect(input('value').val()).toEqual('4');
- expect(element('#link-4').attr('href')).toBe('');
+ element(by.id('link-4')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('4');
+ expect(element(by.id('link-4')).getAttribute('href')).toBe('');
});
it('should execute ng-click but not reload when no href but name specified', function() {
- element('#link-5').click();
- expect(input('value').val()).toEqual('5');
- expect(element('#link-5').attr('href')).toBe(undefined);
+ element(by.id('link-5')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('5');
+ expect(element(by.id('link-5')).getAttribute('href')).toBe(null);
});
it('should only change url when only ng-href', function() {
- input('value').enter('6');
- expect(element('#link-6').attr('href')).toBe('6');
+ element(by.model('value')).clear();
+ element(by.model('value')).sendKeys('6');
+ expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/);
- element('#link-6').click();
- expect(browser().location().url()).toEqual('/6');
+ element(by.id('link-6')).click();
+
+ // At this point, we navigate away from an Angular page, so we need
+ // to use browser.driver to get the base webdriver.
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/6$/);
+ });
+ }, 5000, 'page should navigate to /6');
});
-
-
+
+
*/
/**
* @ngdoc directive
- * @name ng.directive:ngSrc
+ * @name ngSrc
* @restrict A
+ * @priority 99
*
* @description
* Using Angular markup like `{{hash}}` in a `src` attribute doesn't
@@ -13662,14 +21964,14 @@ var htmlAnchorDirective = valueFn({
* `{{hash}}`. The `ngSrc` directive solves this problem.
*
* The buggy way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* The correct way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* @element IMG
* @param {template} ngSrc any string which can contain `{{}}` markup.
@@ -13677,8 +21979,9 @@ var htmlAnchorDirective = valueFn({
/**
* @ngdoc directive
- * @name ng.directive:ngSrcset
+ * @name ngSrcset
* @restrict A
+ * @priority 99
*
* @description
* Using Angular markup like `{{hash}}` in a `srcset` attribute doesn't
@@ -13687,14 +21990,14 @@ var htmlAnchorDirective = valueFn({
* `{{hash}}`. The `ngSrcset` directive solves this problem.
*
* The buggy way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* The correct way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* @element IMG
* @param {template} ngSrcset any string which can contain `{{}}` markup.
@@ -13702,195 +22005,251 @@ var htmlAnchorDirective = valueFn({
/**
* @ngdoc directive
- * @name ng.directive:ngDisabled
+ * @name ngDisabled
* @restrict A
+ * @priority 100
*
* @description
*
- * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
- *
- *
- * Disabled
- *
- *
+ * This directive sets the `disabled` attribute on the element if the
+ * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as disabled. (Their presence means true and their absence means false.)
- * This prevents the Angular compiler from retrieving the binding expression.
- * The `ngDisabled` directive solves this problem for the `disabled` attribute.
+ * A special directive is necessary because we cannot use interpolation inside the `disabled`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
-
-
- Click me to toggle:
+
+
+ Click me to toggle:
Button
-
-
+
+
it('should toggle button', function() {
- expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy();
- input('checked').check();
- expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy();
+ expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy();
});
-
-
+
+
*
* @element INPUT
- * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
- * then special attribute "disabled" will be set on the element
+ * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
+ * then the `disabled` attribute will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngChecked
+ * @name ngChecked
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as checked. (Their presence means true and their absence means false.)
- * This prevents the Angular compiler from retrieving the binding expression.
- * The `ngChecked` directive solves this problem for the `checked` attribute.
+ * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy.
+ *
+ * Note that this directive should not be used together with {@link ngModel `ngModel`},
+ * as this can lead to unexpected behavior.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
* @example
-
-
- Check me to check both:
-
-
-
+
+
+ Check me to check both:
+
+
+
it('should check both checkBoxes', function() {
- expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy();
- input('master').check();
- expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy();
+ expect(element(by.id('checkSlave')).getAttribute('checked')).toBeFalsy();
+ element(by.model('master')).click();
+ expect(element(by.id('checkSlave')).getAttribute('checked')).toBeTruthy();
});
-
-
+
+
*
* @element INPUT
- * @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
- * then special attribute "checked" will be set on the element
+ * @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
+ * then the `checked` attribute will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngReadonly
+ * @name ngReadonly
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as readonly. (Their presence means true and their absence means false.)
- * This prevents the Angular compiler from retrieving the binding expression.
- * The `ngReadonly` directive solves this problem for the `readonly` attribute.
+ *
+ * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on
+ * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `readonly`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
* @example
-
-
- Check me to make text readonly:
-
-
-
+
+
+ Check me to make text readonly:
+
+
+
it('should toggle readonly attr', function() {
- expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy();
- input('checked').check();
- expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy();
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy();
});
-
-
+
+
*
* @element INPUT
- * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
+ * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
* then special attribute "readonly" will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngSelected
+ * @name ngSelected
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as selected. (Their presence means true and their absence means false.)
- * This prevents the Angular compiler from retrieving the binding expression.
- * The `ngSelected` directive solves this problem for the `selected` atttribute.
+ *
+ * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `selected`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ *
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only
+ * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you
+ * should not use `ngSelected` on the options, as `ngModel` will set the select value and
+ * selected options.
+ *
+ *
* @example
-
-
- Check me to select:
-
+
+
+ Check me to select:
+
Hello!
Greetings!
-
-
+
+
it('should select Greetings!', function() {
- expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy();
- input('selected').check();
- expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy();
+ expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy();
+ element(by.model('selected')).click();
+ expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy();
});
-
-
+
+
*
* @element OPTION
- * @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
+ * @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
* then special attribute "selected" will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngOpen
+ * @name ngOpen
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as open. (Their presence means true and their absence means false.)
- * This prevents the Angular compiler from retrieving the binding expression.
- * The `ngOpen` directive solves this problem for the `open` attribute.
+ *
+ * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `open`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ * ## 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
-
-
- Check me check multiple:
+
+
+ Check me check multiple:
Show/Hide me
-
-
+
+
it('should toggle open', function() {
- expect(element('#details').prop('open')).toBeFalsy();
- input('open').check();
- expect(element('#details').prop('open')).toBeTruthy();
+ expect(element(by.id('details')).getAttribute('open')).toBeFalsy();
+ element(by.model('open')).click();
+ expect(element(by.id('details')).getAttribute('open')).toBeTruthy();
});
-
-
+
+
*
* @element DETAILS
- * @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
+ * @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
* then special attribute "open" will be set on the element
*/
var ngAttributeAliasDirectives = {};
-
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
// binding to multiple is not supported
if (propName == "multiple") return;
+ function defaultLinkFn(scope, element, attr) {
+ scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
+ attr.$set(attrName, !!value);
+ });
+ }
+
var normalized = directiveNormalize('ng-' + attrName);
+ var linkFn = defaultLinkFn;
+
+ if (propName === 'checked') {
+ linkFn = function(scope, element, attr) {
+ // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
+ if (attr.ngModel !== attr[normalized]) {
+ defaultLinkFn(scope, element, attr);
+ }
+ };
+ }
+
ngAttributeAliasDirectives[normalized] = function() {
return {
+ restrict: 'A',
priority: 100,
- compile: function() {
- return function(scope, element, attr) {
- scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
- attr.$set(attrName, !!value);
- });
- };
- }
+ link: linkFn
};
};
});
+// aliased input attrs are evaluated
+forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
+ ngAttributeAliasDirectives[ngAttr] = function() {
+ return {
+ priority: 100,
+ link: function(scope, element, attr) {
+ //special case ngPattern when a literal regular expression value
+ //is used as the expression (this way we don't have to watch anything).
+ if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
+ var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
+ if (match) {
+ attr.$set("ngPattern", new RegExp(match[1], match[2]));
+ return;
+ }
+ }
+
+ scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
+ attr.$set(ngAttr, value);
+ });
+ }
+ };
+ };
+});
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
@@ -13899,48 +22258,90 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
return {
priority: 99, // it needs to run after the attributes are interpolated
link: function(scope, element, attr) {
+ var propName = attrName,
+ name = attrName;
+
+ if (attrName === 'href' &&
+ toString.call(element.prop('href')) === '[object SVGAnimatedString]') {
+ name = 'xlinkHref';
+ attr.$attr[name] = 'xlink:href';
+ propName = null;
+ }
+
attr.$observe(normalized, function(value) {
- if (!value)
- return;
+ if (!value) {
+ if (attrName === 'href') {
+ attr.$set(name, null);
+ }
+ return;
+ }
- attr.$set(attrName, value);
+ attr.$set(name, value);
// on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
// we use attr[attrName] value since $set can sanitize the url.
- if (msie) element.prop(attrName, attr[attrName]);
+ if (msie && propName) element.prop(propName, attr[name]);
});
}
};
};
});
+/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
+ */
var nullFormCtrl = {
$addControl: noop,
+ $$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
$setDirty: noop,
- $setPristine: noop
-};
+ $setPristine: noop,
+ $setSubmitted: noop
+},
+SUBMITTED_CLASS = 'ng-submitted';
+
+function nullFormRenameControl(control, name) {
+ control.$name = name;
+}
/**
- * @ngdoc object
- * @name ng.directive:form.FormController
+ * @ngdoc type
+ * @name form.FormController
*
* @property {boolean} $pristine True if user has not interacted with the form yet.
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
- *
- * @property {Object} $error Is an object hash, containing references to all invalid controls or
- * forms, where:
- *
- * - keys are validation tokens (error names) — such as `required`, `url` or `email`),
- * - values are arrays of controls or forms that are invalid with given error.
+ * @property {boolean} $pending True if at least one containing control or form is pending.
+ * @property {boolean} $submitted True if user has submitted the form even if its invalid.
+ *
+ * @property {Object} $error Is an object hash, containing references to controls or
+ * forms with failing validators, where:
+ *
+ * - keys are validation tokens (error names),
+ * - values are arrays of controls or forms that have a failing validator for given error name.
+ *
+ * Built-in validation tokens:
+ *
+ * - `email`
+ * - `max`
+ * - `maxlength`
+ * - `min`
+ * - `minlength`
+ * - `number`
+ * - `pattern`
+ * - `required`
+ * - `url`
+ * - `date`
+ * - `datetimelocal`
+ * - `time`
+ * - `week`
+ * - `month`
*
* @description
- * `FormController` keeps track of all its controls and nested forms as well as state of them,
+ * `FormController` keeps track of all its controls and nested forms as well as the state of them,
* such as being valid/invalid or dirty/pristine.
*
* Each {@link ng.directive:form form} directive creates an instance
@@ -13948,44 +22349,77 @@ var nullFormCtrl = {
*
*/
//asks for $scope to fool the BC controller module
-FormController.$inject = ['$element', '$attrs', '$scope'];
-function FormController(element, attrs) {
+FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
+function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
- parentForm = element.parent().controller('form') || nullFormCtrl,
- invalidCount = 0, // used to easily determine if we are valid
- errors = form.$error = {},
controls = [];
// init state
- form.$name = attrs.name || attrs.ngForm;
+ form.$error = {};
+ form.$$success = {};
+ form.$pending = undefined;
+ form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
form.$dirty = false;
form.$pristine = true;
form.$valid = true;
form.$invalid = false;
+ form.$submitted = false;
+ form.$$parentForm = nullFormCtrl;
- parentForm.$addControl(form);
-
- // Setup initial state of the control
- element.addClass(PRISTINE_CLASS);
- toggleValidCss(true);
+ /**
+ * @ngdoc method
+ * @name form.FormController#$rollbackViewValue
+ *
+ * @description
+ * Rollback all form controls pending updates to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is typically needed by the reset button of
+ * a form that uses `ng-model-options` to pend updates.
+ */
+ form.$rollbackViewValue = function() {
+ forEach(controls, function(control) {
+ control.$rollbackViewValue();
+ });
+ };
- // convenience method for easy toggling of classes
- function toggleValidCss(isValid, validationErrorKey) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- element.
- removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
- addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
- }
+ /**
+ * @ngdoc method
+ * @name form.FormController#$commitViewValue
+ *
+ * @description
+ * Commit all form controls pending updates to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
+ * usually handles calling this in response to input events.
+ */
+ form.$commitViewValue = function() {
+ forEach(controls, function(control) {
+ control.$commitViewValue();
+ });
+ };
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$addControl
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$addControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
- * Register a control with the form.
+ * Register a control with the form. Input elements using ngModelController do this automatically
+ * when they are linked.
+ *
+ * Note that the current state of the control will not be reflected on the new parent form. This
+ * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine`
+ * state.
+ *
+ * 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 responsibility to make sure the current state propagates to the parent form.
*
- * Input elements using ngModelController do this automatically when they are linked.
+ * For example, if an input control is added that is already `$dirty` and has `$error` properties,
+ * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
form.$addControl = function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
@@ -13996,81 +22430,95 @@ function FormController(element, attrs) {
if (control.$name) {
form[control.$name] = control;
}
+
+ control.$$parentForm = form;
+ };
+
+ // Private API: rename a form control
+ form.$$renameControl = function(control, newName) {
+ var oldName = control.$name;
+
+ if (form[oldName] === control) {
+ delete form[oldName];
+ }
+ form[newName] = control;
+ control.$name = newName;
};
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$removeControl
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$removeControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
* Deregister a control from the form.
*
* Input elements using ngModelController do this automatically when they are destroyed.
+ *
+ * Note that only the removed control's validation state (`$errors`etc.) will be removed from the
+ * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be
+ * different from case to case. For example, removing the only `$dirty` control from a form may or
+ * may not mean that the form is still `$dirty`.
*/
form.$removeControl = function(control) {
if (control.$name && form[control.$name] === control) {
delete form[control.$name];
}
- forEach(errors, function(queue, validationToken) {
- form.$setValidity(validationToken, true, control);
+ forEach(form.$pending, function(value, name) {
+ form.$setValidity(name, null, control);
+ });
+ forEach(form.$error, function(value, name) {
+ form.$setValidity(name, null, control);
+ });
+ forEach(form.$$success, function(value, name) {
+ form.$setValidity(name, null, control);
});
arrayRemove(controls, control);
+ control.$$parentForm = nullFormCtrl;
};
+
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$setValidity
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$setValidity
*
* @description
* Sets the validity of a form control.
*
* This method will also propagate to parent forms.
*/
- form.$setValidity = function(validationToken, isValid, control) {
- var queue = errors[validationToken];
-
- if (isValid) {
- if (queue) {
- arrayRemove(queue, control);
- if (!queue.length) {
- invalidCount--;
- if (!invalidCount) {
- toggleValidCss(isValid);
- form.$valid = true;
- form.$invalid = false;
- }
- errors[validationToken] = false;
- toggleValidCss(true, validationToken);
- parentForm.$setValidity(validationToken, true, form);
+ addSetValidityMethod({
+ ctrl: this,
+ $element: element,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
}
}
-
- } else {
- if (!invalidCount) {
- toggleValidCss(isValid);
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
}
- if (queue) {
- if (includes(queue, control)) return;
- } else {
- errors[validationToken] = queue = [];
- invalidCount++;
- toggleValidCss(false, validationToken);
- parentForm.$setValidity(validationToken, false, form);
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
}
- queue.push(control);
-
- form.$valid = false;
- form.$invalid = true;
- }
- };
+ },
+ $animate: $animate
+ });
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$setDirty
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$setDirty
*
* @description
* Sets the form to a dirty state.
@@ -14079,16 +22527,16 @@ function FormController(element, attrs) {
* state (ng-dirty class). This method will also propagate to parent forms.
*/
form.$setDirty = function() {
- element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
+ $animate.removeClass(element, PRISTINE_CLASS);
+ $animate.addClass(element, DIRTY_CLASS);
form.$dirty = true;
form.$pristine = false;
- parentForm.$setDirty();
+ form.$$parentForm.$setDirty();
};
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$setPristine
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$setPristine
*
* @description
* Sets the form to its pristine state.
@@ -14100,20 +22548,52 @@ function FormController(element, attrs) {
* Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
* saving or resetting it.
*/
- form.$setPristine = function () {
- element.removeClass(DIRTY_CLASS).addClass(PRISTINE_CLASS);
+ form.$setPristine = function() {
+ $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
form.$dirty = false;
form.$pristine = true;
+ form.$submitted = false;
forEach(controls, function(control) {
control.$setPristine();
});
};
-}
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setUntouched
+ *
+ * @description
+ * Sets the form to its untouched state.
+ *
+ * This method can be called to remove the 'ng-touched' class and set the form controls to their
+ * untouched state (ng-untouched class).
+ *
+ * Setting a form controls back to their untouched state is often useful when setting the form
+ * back to its pristine state.
+ */
+ form.$setUntouched = function() {
+ forEach(controls, function(control) {
+ control.$setUntouched();
+ });
+ };
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setSubmitted
+ *
+ * @description
+ * Sets the form to its submitted state.
+ */
+ form.$setSubmitted = function() {
+ $animate.addClass(element, SUBMITTED_CLASS);
+ form.$submitted = true;
+ form.$$parentForm.$setSubmitted();
+ };
+}
/**
* @ngdoc directive
- * @name ng.directive:ngForm
+ * @name ngForm
* @restrict EAC
*
* @description
@@ -14121,40 +22601,44 @@ function FormController(element, attrs) {
* does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
* sub-group of controls needs to be determined.
*
- * @param {string=} name|ngForm Name of the form. If specified, the form controller will be published into
+ * Note: the purpose of `ngForm` is to group controls,
+ * but not to be a replacement for the `