summaryrefslogtreecommitdiff
path: root/www/lib/angular/angular.js
diff options
context:
space:
mode:
authorpliablepixels <pliablepixels@gmail.com>2016-04-10 10:47:35 -0400
committerpliablepixels <pliablepixels@gmail.com>2016-04-10 10:47:35 -0400
commita29f9a676a6ea3bad56ede05cd1a1c82ffbbe8e9 (patch)
tree7e6e6bfb23390a64344fdfe2272a5da04c2dd97c /www/lib/angular/angular.js
parent42bc21f7d0b4acfeefa5f4c2708203be78f57778 (diff)
#219 - everything upgraded
Former-commit-id: 15f58d10df83feda8199a1b904433e625ef36b44
Diffstat (limited to 'www/lib/angular/angular.js')
-rw-r--r--www/lib/angular/angular.js6006
1 files changed, 4178 insertions, 1828 deletions
diff --git a/www/lib/angular/angular.js b/www/lib/angular/angular.js
index f7442c0b..5e6fb2c3 100644
--- a/www/lib/angular/angular.js
+++ b/www/lib/angular/angular.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.4.3
- * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.5.3
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, document, undefined) {'use strict';
@@ -57,7 +57,7 @@ function minErr(module, ErrorConstructor) {
return match;
});
- message += '\nhttp://errors.angularjs.org/1.4.3/' +
+ message += '\nhttp://errors.angularjs.org/1.5.3/' +
(module ? module + '/' : '') + code;
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
@@ -188,29 +188,9 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
-/**
- * @ngdoc function
- * @name angular.lowercase
- * @module ng
- * @kind function
- *
- * @description Converts the specified string to lowercase.
- * @param {string} string String to be converted to lowercase.
- * @returns {string} Lowercased string.
- */
-var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
var hasOwnProperty = Object.prototype.hasOwnProperty;
-/**
- * @ngdoc function
- * @name angular.uppercase
- * @module ng
- * @kind function
- *
- * @description Converts the specified string to uppercase.
- * @param {string} string String to be converted to uppercase.
- * @returns {string} Uppercased string.
- */
+var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
@@ -230,7 +210,7 @@ var manualUppercase = function(s) {
// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods
-// with correct but slower alternatives.
+// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387
if ('i' !== 'I'.toLowerCase()) {
lowercase = manualLowercase;
uppercase = manualUppercase;
@@ -267,20 +247,25 @@ msie = document.documentMode;
* String ...)
*/
function isArrayLike(obj) {
- if (obj == null || isWindow(obj)) {
- return false;
- }
+
+ // `null`, `undefined` and `window` are not array-like
+ if (obj == null || isWindow(obj)) return false;
+
+ // arrays, strings and jQuery/jqLite objects are array like
+ // * jqLite is either the jQuery or jqLite constructor function
+ // * we have to check the existence of jqLite first as this method is called
+ // via the forEach method when constructing the jqLite object in the first place
+ if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true;
// Support: iOS 8.2 (not reproducible in simulator)
// "length" in obj used to prevent JIT error (gh-11508)
var length = "length" in Object(obj) && obj.length;
- if (obj.nodeType === NODE_TYPE_ELEMENT && length) {
- return true;
- }
+ // NodeList objects (with `item` method) and
+ // other objects with suitable length characteristics are array-like
+ return isNumber(length) &&
+ (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item == 'function');
- return isString(obj) || isArray(obj) || length === 0 ||
- typeof length === 'number' && length > 0 && (length - 1) in obj;
}
/**
@@ -300,7 +285,7 @@ function isArrayLike(obj) {
*
* Unlike ES262's
* [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18),
- * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just
+ * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just
* return the value provided.
*
```js
@@ -377,7 +362,7 @@ function forEachSorted(obj, iterator, context) {
* @returns {function(*, string)}
*/
function reverseParams(iteratorFn) {
- return function(value, key) { iteratorFn(key, value); };
+ return function(value, key) {iteratorFn(key, value);};
}
/**
@@ -423,6 +408,12 @@ function baseExtend(dst, objs, deep) {
if (deep && isObject(src)) {
if (isDate(src)) {
dst[key] = new Date(src.valueOf());
+ } else if (isRegExp(src)) {
+ dst[key] = new RegExp(src);
+ } else if (src.nodeName) {
+ dst[key] = src.cloneNode(true);
+ } else if (isElement(src)) {
+ dst[key] = src.clone();
} else {
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
baseExtend(dst[key], [src], true);
@@ -535,10 +526,10 @@ function identity($) {return $;}
identity.$inject = [];
-function valueFn(value) {return function() {return value;};}
+function valueFn(value) {return function valueRef() {return value;};}
function hasCustomToString(obj) {
- return isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
+ return isFunction(obj.toString) && obj.toString !== toString;
}
@@ -737,9 +728,13 @@ function isPromiseLike(obj) {
}
-var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/;
+var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
function isTypedArray(value) {
- return TYPED_ARRAY_REGEXP.test(toString.call(value));
+ return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
+}
+
+function isArrayBuffer(obj) {
+ return toString.call(obj) === '[object ArrayBuffer]';
}
@@ -779,7 +774,7 @@ function isElement(node) {
* @returns {object} in the form of {key1:true, key2:true, ...}
*/
function makeMap(str) {
- var obj = {}, items = str.split(","), i;
+ var obj = {}, items = str.split(','), i;
for (i = 0; i < items.length; i++) {
obj[items[i]] = true;
}
@@ -861,98 +856,141 @@ function arrayRemove(array, value) {
</file>
</example>
*/
-function copy(source, destination, stackSource, stackDest) {
- if (isWindow(source) || isScope(source)) {
- throw ngMinErr('cpws',
- "Can't copy! Making copies of Window or Scope instances is not supported.");
- }
- if (isTypedArray(destination)) {
- throw ngMinErr('cpta',
- "Can't copy! TypedArray destination cannot be mutated.");
- }
-
- if (!destination) {
- destination = source;
- if (isObject(source)) {
- var index;
- if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
- return stackDest[index];
- }
-
- // TypedArray, Date and RegExp have specific copy functionality and must be
- // pushed onto the stack before returning.
- // Array and other objects create the base object and recurse to copy child
- // objects. The array/object will be pushed onto the stack when recursed.
- if (isArray(source)) {
- return copy(source, [], stackSource, stackDest);
- } else if (isTypedArray(source)) {
- destination = new source.constructor(source);
- } else if (isDate(source)) {
- destination = new Date(source.getTime());
- } else if (isRegExp(source)) {
- destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
- destination.lastIndex = source.lastIndex;
- } else {
- var emptyObject = Object.create(getPrototypeOf(source));
- return copy(source, emptyObject, stackSource, stackDest);
- }
+function copy(source, destination) {
+ var stackSource = [];
+ var stackDest = [];
- if (stackDest) {
- stackSource.push(source);
- stackDest.push(destination);
- }
+ if (destination) {
+ if (isTypedArray(destination) || isArrayBuffer(destination)) {
+ throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
}
- } else {
- if (source === destination) throw ngMinErr('cpi',
- "Can't copy! Source and destination are identical.");
-
- stackSource = stackSource || [];
- stackDest = stackDest || [];
-
- if (isObject(source)) {
- stackSource.push(source);
- stackDest.push(destination);
+ if (source === destination) {
+ throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
}
- var result, key;
- if (isArray(source)) {
+ // Empty the destination object
+ if (isArray(destination)) {
destination.length = 0;
- for (var i = 0; i < source.length; i++) {
- destination.push(copy(source[i], null, stackSource, stackDest));
- }
} else {
- var h = destination.$$hashKey;
- if (isArray(destination)) {
- destination.length = 0;
- } else {
- forEach(destination, function(value, key) {
+ forEach(destination, function(value, key) {
+ if (key !== '$$hashKey') {
delete destination[key];
- });
- }
- if (isBlankObject(source)) {
- // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
- for (key in source) {
- destination[key] = copy(source[key], null, stackSource, stackDest);
}
- } else if (source && typeof source.hasOwnProperty === 'function') {
- // Slow path, which must rely on hasOwnProperty
- for (key in source) {
- if (source.hasOwnProperty(key)) {
- destination[key] = copy(source[key], null, stackSource, stackDest);
- }
+ });
+ }
+
+ stackSource.push(source);
+ stackDest.push(destination);
+ return copyRecurse(source, destination);
+ }
+
+ return copyElement(source);
+
+ function copyRecurse(source, destination) {
+ var h = destination.$$hashKey;
+ var key;
+ if (isArray(source)) {
+ for (var i = 0, ii = source.length; i < ii; i++) {
+ destination.push(copyElement(source[i]));
+ }
+ } else if (isBlankObject(source)) {
+ // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
+ for (key in source) {
+ destination[key] = copyElement(source[key]);
+ }
+ } else if (source && typeof source.hasOwnProperty === 'function') {
+ // Slow path, which must rely on hasOwnProperty
+ for (key in source) {
+ if (source.hasOwnProperty(key)) {
+ destination[key] = copyElement(source[key]);
}
- } else {
- // Slowest path --- hasOwnProperty can't be called as a method
- for (key in source) {
- if (hasOwnProperty.call(source, key)) {
- destination[key] = copy(source[key], null, stackSource, stackDest);
- }
+ }
+ } else {
+ // Slowest path --- hasOwnProperty can't be called as a method
+ for (key in source) {
+ if (hasOwnProperty.call(source, key)) {
+ destination[key] = copyElement(source[key]);
}
}
- setHashKey(destination,h);
+ }
+ setHashKey(destination, h);
+ return destination;
+ }
+
+ function copyElement(source) {
+ // Simple values
+ if (!isObject(source)) {
+ return source;
+ }
+
+ // Already copied values
+ var index = stackSource.indexOf(source);
+ if (index !== -1) {
+ return stackDest[index];
+ }
+
+ if (isWindow(source) || isScope(source)) {
+ throw ngMinErr('cpws',
+ "Can't copy! Making copies of Window or Scope instances is not supported.");
+ }
+
+ var needsRecurse = false;
+ var destination = copyType(source);
+
+ if (destination === undefined) {
+ destination = isArray(source) ? [] : Object.create(getPrototypeOf(source));
+ needsRecurse = true;
+ }
+
+ stackSource.push(source);
+ stackDest.push(destination);
+
+ return needsRecurse
+ ? copyRecurse(source, destination)
+ : destination;
+ }
+
+ function copyType(source) {
+ switch (toString.call(source)) {
+ case '[object Int8Array]':
+ case '[object Int16Array]':
+ case '[object Int32Array]':
+ case '[object Float32Array]':
+ case '[object Float64Array]':
+ case '[object Uint8Array]':
+ case '[object Uint8ClampedArray]':
+ case '[object Uint16Array]':
+ case '[object Uint32Array]':
+ return new source.constructor(copyElement(source.buffer));
+
+ case '[object ArrayBuffer]':
+ //Support: IE10
+ if (!source.slice) {
+ var copied = new ArrayBuffer(source.byteLength);
+ new Uint8Array(copied).set(new Uint8Array(source));
+ return copied;
+ }
+ return source.slice(0);
+
+ case '[object Boolean]':
+ case '[object Number]':
+ case '[object String]':
+ case '[object Date]':
+ return new source.constructor(source.valueOf());
+
+ case '[object RegExp]':
+ var re = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
+ re.lastIndex = source.lastIndex;
+ return re;
+
+ case '[object Blob]':
+ return new source.constructor([source], {type: source.type});
+ }
+
+ if (isFunction(source.cloneNode)) {
+ return source.cloneNode(true);
}
}
- return destination;
}
/**
@@ -1015,60 +1053,76 @@ function equals(o1, o2) {
if (o1 === null || o2 === null) return false;
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
- if (t1 == t2) {
- if (t1 == 'object') {
- if (isArray(o1)) {
- if (!isArray(o2)) return false;
- if ((length = o1.length) == o2.length) {
- for (key = 0; key < length; key++) {
- if (!equals(o1[key], o2[key])) return false;
- }
- return true;
- }
- } else if (isDate(o1)) {
- if (!isDate(o2)) return false;
- return equals(o1.getTime(), o2.getTime());
- } else if (isRegExp(o1)) {
- return isRegExp(o2) ? o1.toString() == o2.toString() : false;
- } else {
- if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
- isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
- keySet = createMap();
- for (key in o1) {
- if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (t1 == t2 && t1 == 'object') {
+ if (isArray(o1)) {
+ if (!isArray(o2)) return false;
+ if ((length = o1.length) == o2.length) {
+ for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
- keySet[key] = true;
- }
- for (key in o2) {
- if (!(key in keySet) &&
- key.charAt(0) !== '$' &&
- o2[key] !== undefined &&
- !isFunction(o2[key])) return false;
}
return true;
}
+ } else if (isDate(o1)) {
+ if (!isDate(o2)) return false;
+ return equals(o1.getTime(), o2.getTime());
+ } else if (isRegExp(o1)) {
+ if (!isRegExp(o2)) return false;
+ return o1.toString() == o2.toString();
+ } else {
+ if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
+ isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
+ keySet = createMap();
+ for (key in o1) {
+ if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (!equals(o1[key], o2[key])) return false;
+ keySet[key] = true;
+ }
+ for (key in o2) {
+ if (!(key in keySet) &&
+ key.charAt(0) !== '$' &&
+ isDefined(o2[key]) &&
+ !isFunction(o2[key])) return false;
+ }
+ return true;
}
}
return false;
}
var csp = function() {
- if (isDefined(csp.isActive_)) return csp.isActive_;
+ if (!isDefined(csp.rules)) {
+
- var active = !!(document.querySelector('[ng-csp]') ||
- document.querySelector('[data-ng-csp]'));
+ var ngCspElement = (document.querySelector('[ng-csp]') ||
+ document.querySelector('[data-ng-csp]'));
- if (!active) {
+ if (ngCspElement) {
+ var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
+ ngCspElement.getAttribute('data-ng-csp');
+ csp.rules = {
+ noUnsafeEval: !ngCspAttribute || (ngCspAttribute.indexOf('no-unsafe-eval') !== -1),
+ noInlineStyle: !ngCspAttribute || (ngCspAttribute.indexOf('no-inline-style') !== -1)
+ };
+ } else {
+ csp.rules = {
+ noUnsafeEval: noUnsafeEval(),
+ noInlineStyle: false
+ };
+ }
+ }
+
+ return csp.rules;
+
+ function noUnsafeEval() {
try {
/* jshint -W031, -W054 */
new Function('');
/* jshint +W031, +W054 */
+ return false;
} catch (e) {
- active = true;
+ return true;
}
}
-
- return (csp.isActive_ = active);
};
/**
@@ -1206,7 +1260,7 @@ function toJsonReplacer(key, value) {
* @returns {string|undefined} JSON-ified string representing `obj`.
*/
function toJson(obj, pretty) {
- if (typeof obj === 'undefined') return undefined;
+ if (isUndefined(obj)) return undefined;
if (!isNumber(pretty)) {
pretty = pretty ? 2 : null;
}
@@ -1233,7 +1287,10 @@ function fromJson(json) {
}
+var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
+ // IE/Edge do not "understand" colon (`:`) in timezone
+ timezone = timezone.replace(ALL_COLONS, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
@@ -1248,8 +1305,9 @@ function addDateMinutes(date, minutes) {
function convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
- var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
- return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
}
@@ -1268,7 +1326,7 @@ function startingTag(element) {
return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) :
elemHtml.
match(/^(<[^>]+>)/)[1].
- replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); });
+ replace(/^<([\w\-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
} catch (e) {
return lowercase(elemHtml);
}
@@ -1300,13 +1358,19 @@ function tryDecodeURIComponent(value) {
* @returns {Object.<string,boolean|Array>}
*/
function parseKeyValue(/**string*/keyValue) {
- var obj = {}, key_value, key;
+ var obj = {};
forEach((keyValue || "").split('&'), function(keyValue) {
+ var splitPoint, key, val;
if (keyValue) {
- key_value = keyValue.replace(/\+/g,'%20').split('=');
- key = tryDecodeURIComponent(key_value[0]);
+ key = keyValue = keyValue.replace(/\+/g,'%20');
+ splitPoint = keyValue.indexOf('=');
+ if (splitPoint !== -1) {
+ key = keyValue.substring(0, splitPoint);
+ val = keyValue.substring(splitPoint + 1);
+ }
+ key = tryDecodeURIComponent(key);
if (isDefined(key)) {
- var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
+ val = isDefined(val) ? tryDecodeURIComponent(val) : true;
if (!hasOwnProperty.call(obj, key)) {
obj[key] = val;
} else if (isArray(obj[key])) {
@@ -1410,10 +1474,17 @@ function getNgAttribute(element, ngAttr) {
* designates the **root element** of the application and is typically placed near the root element
* of the page - e.g. on the `<body>` or `<html>` tags.
*
- * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
- * found in the document will be used to define the root element to auto-bootstrap as an
- * application. To run multiple applications in an HTML document you must manually bootstrap them using
- * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other.
+ * There are a few things to keep in mind when using `ngApp`:
+ * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
+ * found in the document will be used to define the root element to auto-bootstrap as an
+ * application. To run multiple applications in an HTML document you must manually bootstrap them using
+ * {@link angular.bootstrap} instead.
+ * - AngularJS applications cannot be nested within each other.
+ * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`.
+ * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and
+ * {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
*
* You can specify an **AngularJS module** to be used as the root module for the application. This
* module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It
@@ -1553,16 +1624,25 @@ function angularInit(element, bootstrap) {
* @description
* Use this function to manually start up angular application.
*
- * See: {@link guide/bootstrap Bootstrap}
- *
- * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually.
- * They must use {@link ng.directive:ngApp ngApp}.
+ * For more information, see the {@link guide/bootstrap Bootstrap guide}.
*
* Angular will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
* multiple instances of Angular try to work on the DOM.
*
+ * <div class="alert alert-warning">
+ * **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually.
+ * They must use {@link ng.directive:ngApp ngApp}.
+ * </div>
+ *
+ * <div class="alert alert-warning">
+ * **Note:** Do not bootstrap the app on an element with a directive that uses {@link ng.$compile#transclusion transclusion},
+ * such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
+ * </div>
+ *
* ```html
* <!doctype html>
* <html>
@@ -1705,7 +1785,6 @@ function snake_case(name, separator) {
}
var bindJQueryFired = false;
-var skipDestroyOnNextJQueryCleanData;
function bindJQuery() {
var originalCleanData;
@@ -1715,10 +1794,9 @@ function bindJQuery() {
// bind to jQuery if present;
var jqName = jq();
- jQuery = window.jQuery; // use default jQuery.
- if (isDefined(jqName)) { // `ngJq` present
- jQuery = jqName === null ? undefined : window[jqName]; // if empty; use jqLite. if not empty, use jQuery specified by `ngJq`.
- }
+ jQuery = isUndefined(jqName) ? window.jQuery : // use jQuery (if present)
+ !jqName ? undefined : // use jqLite
+ window[jqName]; // use jQuery specified by `ngJq`
// Use jQuery if it exists with proper functionality, otherwise default to us.
// Angular 1.2+ requires jQuery 1.7+ for on()/off() support.
@@ -1740,15 +1818,11 @@ function bindJQuery() {
originalCleanData = jQuery.cleanData;
jQuery.cleanData = function(elems) {
var events;
- if (!skipDestroyOnNextJQueryCleanData) {
- for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
- if (events && events.$destroy) {
- jQuery(elem).triggerHandler('$destroy');
- }
+ for (var i = 0, elem; (elem = elems[i]) != null; i++) {
+ events = jQuery._data(elem, "events");
+ if (events && events.$destroy) {
+ jQuery(elem).triggerHandler('$destroy');
}
- } else {
- skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
@@ -1823,22 +1897,24 @@ function getter(obj, path, bindFnToScope) {
/**
* Return the DOM siblings between the first and last node in the given array.
* @param {Array} array like object
- * @returns {jqLite} jqLite collection containing the nodes
+ * @returns {Array} the inputted object or a jqLite collection containing the nodes
*/
function getBlockNodes(nodes) {
- // TODO(perf): just check if all items in `nodes` are siblings and if they are return the original
- // collection, otherwise update the original collection.
+ // TODO(perf): update `nodes` instead of creating a new object?
var node = nodes[0];
var endNode = nodes[nodes.length - 1];
- var blockNodes = [node];
+ var blockNodes;
- do {
- node = node.nextSibling;
- if (!node) break;
- blockNodes.push(node);
- } while (node !== endNode);
+ for (var i = 1; node !== endNode && (node = node.nextSibling); i++) {
+ if (blockNodes || nodes[i] !== node) {
+ if (!blockNodes) {
+ blockNodes = jqLite(slice.call(nodes, 0, i));
+ }
+ blockNodes.push(node);
+ }
+ }
- return jqLite(blockNodes);
+ return blockNodes || nodes;
}
@@ -1902,8 +1978,8 @@ function setupModuleLoader(window) {
* All modules (angular core or 3rd party) that should be available to an application must be
* registered using this mechanism.
*
- * When passed two or more arguments, a new module is created. If passed only one argument, an
- * existing module (the name passed as the first argument to `module`) is retrieved.
+ * Passing one argument retrieves an existing {@link angular.Module},
+ * whereas passing more than one argument creates a new {@link angular.Module}
*
*
* # Module
@@ -1940,7 +2016,7 @@ function setupModuleLoader(window) {
* unspecified then the module is being retrieved for further configuration.
* @param {Function=} configFn Optional configuration function for the module. Same as
* {@link angular.Module#config Module#config()}.
- * @returns {module} new module with the {@link angular.Module} api.
+ * @returns {angular.Module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
var assertNotHasOwnProperty = function(name, context) {
@@ -2052,7 +2128,7 @@ function setupModuleLoader(window) {
* @param {string} name constant name
* @param {*} object Constant value.
* @description
- * Because the constant are fixed, they get applied before other provide methods.
+ * Because the constants are fixed, they get applied before other provide methods.
* See {@link auto.$provide#constant $provide.constant()}.
*/
constant: invokeLater('$provide', 'constant', 'unshift'),
@@ -2148,6 +2224,19 @@ function setupModuleLoader(window) {
/**
* @ngdoc method
+ * @name angular.Module#component
+ * @module ng
+ * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
+ * @param {Object} options Component definition object (a simplified
+ * {@link ng.$compile#directive-definition-object directive definition object})
+ *
+ * @description
+ * See {@link ng.$compileProvider#component $compileProvider.component()}.
+ */
+ component: invokeLaterAndSetModuleName('$compileProvider', 'component'),
+
+ /**
+ * @ngdoc method
* @name angular.Module#config
* @module ng
* @param {Function} configFn Execute this function on module load. Useful for service
@@ -2222,7 +2311,7 @@ function serializeObject(obj) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
- if (seen.indexOf(val) >= 0) return '<<already seen>>';
+ if (seen.indexOf(val) >= 0) return '...';
seen.push(val);
}
@@ -2233,7 +2322,7 @@ function serializeObject(obj) {
function toDebugString(obj) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
- } else if (typeof obj === 'undefined') {
+ } else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
return serializeObject(obj);
@@ -2244,7 +2333,6 @@ function toDebugString(obj) {
/* global angularModule: true,
version: true,
- $LocaleProvider,
$CompileProvider,
htmlAnchorDirective,
@@ -2261,7 +2349,6 @@ function toDebugString(obj) {
ngClassDirective,
ngClassEvenDirective,
ngClassOddDirective,
- ngCspDirective,
ngCloakDirective,
ngControllerDirective,
ngFormDirective,
@@ -2298,14 +2385,19 @@ function toDebugString(obj) {
$AnchorScrollProvider,
$AnimateProvider,
+ $CoreAnimateCssProvider,
+ $$CoreAnimateJsProvider,
$$CoreAnimateQueueProvider,
- $$CoreAnimateRunnerProvider,
+ $$AnimateRunnerFactoryProvider,
+ $$AnimateAsyncRunFactoryProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
+ $DateProvider,
$DocumentProvider,
$ExceptionHandlerProvider,
$FilterProvider,
+ $$ForceReflowProvider,
$InterpolateProvider,
$IntervalProvider,
$$HashMapProvider,
@@ -2313,6 +2405,7 @@ function toDebugString(obj) {
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
+ $xhrFactoryProvider,
$LocationProvider,
$LogProvider,
$ParseProvider,
@@ -2339,8 +2432,9 @@ function toDebugString(obj) {
* @name angular.version
* @module ng
* @description
- * An object that contains information about the current AngularJS version. This object has the
- * following properties:
+ * An object that contains information about the current AngularJS version.
+ *
+ * This object has the following properties:
*
* - `full` – `{string}` – Full version string, such as "0.9.18".
* - `major` – `{number}` – Major version number, such as "0".
@@ -2349,11 +2443,11 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.4.3', // all of these placeholder strings will be replaced by grunt's
+ full: '1.5.3', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task
- minor: 4,
+ minor: 5,
dot: 3,
- codeName: 'foam-acceleration'
+ codeName: 'diplohaplontic-meiosis'
};
@@ -2392,11 +2486,6 @@ function publishExternalAPI(angular) {
});
angularModule = setupModuleLoader(window);
- try {
- angularModule('ngLocale');
- } catch (e) {
- angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
- }
angularModule('ng', ['ngLocale'], ['$provide',
function ngModule($provide) {
@@ -2459,20 +2548,25 @@ function publishExternalAPI(angular) {
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
+ $animateCss: $CoreAnimateCssProvider,
+ $$animateJs: $$CoreAnimateJsProvider,
$$animateQueue: $$CoreAnimateQueueProvider,
- $$AnimateRunner: $$CoreAnimateRunnerProvider,
+ $$AnimateRunner: $$AnimateRunnerFactoryProvider,
+ $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
$document: $DocumentProvider,
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
+ $$forceReflow: $$ForceReflowProvider,
$interpolate: $InterpolateProvider,
$interval: $IntervalProvider,
$http: $HttpProvider,
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
+ $xhrFactory: $xhrFactoryProvider,
$location: $LocationProvider,
$log: $LogProvider,
$parse: $ParseProvider,
@@ -2529,16 +2623,22 @@ function publishExternalAPI(angular) {
*
* If jQuery is available, `angular.element` is an alias for the
* [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element`
- * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite."
+ * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
+ *
+ * jqLite is a tiny, API-compatible subset of jQuery that allows
+ * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
+ * commonly needed functionality with the goal of having a very small footprint.
*
- * <div class="alert alert-success">jqLite is a tiny, API-compatible subset of jQuery that allows
- * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most
- * commonly needed functionality with the goal of having a very small footprint.</div>
+ * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the
+ * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a
+ * specific version of jQuery if multiple versions exist on the page.
*
- * To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
+ * <div class="alert alert-info">**Note:** All element references in Angular are always wrapped with jQuery or
+ * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.</div>
*
- * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or
- * jqLite; they are never raw DOM references.</div>
+ * <div class="alert alert-warning">**Note:** Keep in mind that this function will not find elements
+ * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)`
+ * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.</div>
*
* ## Angular's jqLite
* jqLite provides only the following jQuery methods:
@@ -2551,7 +2651,8 @@ function publishExternalAPI(angular) {
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
- * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'.
+ * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`.
+ * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing.
* - [`data()`](http://api.jquery.com/data/)
* - [`detach()`](http://api.jquery.com/detach/)
* - [`empty()`](http://api.jquery.com/empty/)
@@ -2561,7 +2662,7 @@ function publishExternalAPI(angular) {
* - [`html()`](http://api.jquery.com/html/)
* - [`next()`](http://api.jquery.com/next/) - Does not support selectors
* - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData
- * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors
+ * - [`off()`](http://api.jquery.com/off/) - Does not support namespaces, selectors or event object as parameter
* - [`one()`](http://api.jquery.com/one/) - Does not support namespaces or selectors
* - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors
* - [`prepend()`](http://api.jquery.com/prepend/)
@@ -2575,7 +2676,7 @@ function publishExternalAPI(angular) {
* - [`text()`](http://api.jquery.com/text/)
* - [`toggleClass()`](http://api.jquery.com/toggleClass/)
* - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers.
- * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces
+ * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter
* - [`val()`](http://api.jquery.com/val/)
* - [`wrap()`](http://api.jquery.com/wrap/)
*
@@ -2647,10 +2748,10 @@ function camelCase(name) {
replace(MOZ_HACK_REGEXP, 'Moz$1');
}
-var SINGLE_TAG_REGEXP = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;
+var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
var HTML_REGEXP = /<|&#?\w+;/;
-var TAG_NAME_REGEXP = /<([\w:]+)/;
-var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi;
+var TAG_NAME_REGEXP = /<([\w:-]+)/;
+var XHTML_TAG_REGEXP = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi;
var wrapMap = {
'option': [1, '<select multiple="multiple">', '</select>'],
@@ -2685,6 +2786,12 @@ function jqLiteHasData(node) {
return false;
}
+function jqLiteCleanData(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ jqLiteRemoveData(nodes[i]);
+ }
+}
+
function jqLiteBuildFragment(html, context) {
var tmp, tag, wrap,
fragment = context.createDocumentFragment(),
@@ -2737,6 +2844,24 @@ function jqLiteParseHTML(html, context) {
return [];
}
+function jqLiteWrapNode(node, wrapper) {
+ var parent = node.parentNode;
+
+ if (parent) {
+ parent.replaceChild(wrapper, node);
+ }
+
+ wrapper.appendChild(node);
+}
+
+
+// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
+var jqLiteContains = Node.prototype.contains || function(arg) {
+ // jshint bitwise: false
+ return !!(this.compareDocumentPosition(arg) & 16);
+ // jshint bitwise: true
+};
+
/////////////////////////////////////////////
function JQLite(element) {
if (element instanceof JQLite) {
@@ -2795,17 +2920,23 @@ function jqLiteOff(element, type, fn, unsupported) {
delete events[type];
}
} else {
- forEach(type.split(' '), function(type) {
+
+ var removeHandler = function(type) {
+ var listenerFns = events[type];
if (isDefined(fn)) {
- var listenerFns = events[type];
arrayRemove(listenerFns || [], fn);
- if (listenerFns && listenerFns.length > 0) {
- return;
- }
}
+ if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
+ removeEventListenerFn(element, type, handle);
+ delete events[type];
+ }
+ };
- removeEventListenerFn(element, type, handle);
- delete events[type];
+ forEach(type.split(' '), function(type) {
+ removeHandler(type);
+ if (MOUSE_EVENT_MAP[type]) {
+ removeHandler(MOUSE_EVENT_MAP[type]);
+ }
});
}
}
@@ -2946,7 +3077,7 @@ function jqLiteInheritedData(element, name, value) {
while (element) {
for (var i = 0, ii = names.length; i < ii; i++) {
- if ((value = jqLite.data(element, names[i])) !== undefined) return value;
+ if (isDefined(value = jqLite.data(element, names[i]))) return value;
}
// If dealing with a document fragment node with a host element, and no parent, use the host
@@ -2973,7 +3104,7 @@ function jqLiteRemove(element, keepData) {
function jqLiteDocumentLoaded(action, win) {
win = win || window;
if (win.document.readyState === 'complete') {
- // Force the action to be run async for consistent behaviour
+ // Force the action to be run async for consistent behavior
// from the action's point of view
// i.e. it will definitely not be in a $apply
win.setTimeout(action);
@@ -3052,15 +3183,15 @@ function getBooleanAttrName(element, name) {
return booleanAttr && BOOLEAN_ELEMENTS[nodeName_(element)] && booleanAttr;
}
-function getAliasedAttrName(element, name) {
- var nodeName = element.nodeName;
- return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name];
+function getAliasedAttrName(name) {
+ return ALIASED_ATTR[name];
}
forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
- hasData: jqLiteHasData
+ hasData: jqLiteHasData,
+ cleanData: jqLiteCleanData
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -3191,7 +3322,7 @@ forEach({
// in a way that survives minification.
// jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
- (((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2) === undefined)) {
+ (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
@@ -3212,7 +3343,7 @@ forEach({
// TODO: do we still need this?
var value = fn.$dv;
// Only if we have $dv do we iterate over all, otherwise it is just the first element.
- var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount;
+ var jj = (isUndefined(value)) ? Math.min(nodeCount, 1) : nodeCount;
for (var j = 0; j < jj; j++) {
var nodeValue = fn(this[j], arg1, arg2);
value = value ? value + nodeValue : nodeValue;
@@ -3261,6 +3392,9 @@ function createEventHandler(element, events) {
return event.immediatePropagationStopped === true;
};
+ // Some events have special handlers that wrap the real handler
+ var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper;
+
// Copy event handlers in case event handlers array is modified during execution.
if ((eventFnsLength > 1)) {
eventFns = shallowCopy(eventFns);
@@ -3268,7 +3402,7 @@ function createEventHandler(element, events) {
for (var i = 0; i < eventFnsLength; i++) {
if (!event.isImmediatePropagationStopped()) {
- eventFns[i].call(element, event);
+ handlerWrapper(element, event, eventFns[i]);
}
}
};
@@ -3279,6 +3413,22 @@ function createEventHandler(element, events) {
return eventHandler;
}
+function defaultHandlerWrapper(element, event, handler) {
+ handler.call(element, event);
+}
+
+function specialMouseHandlerWrapper(target, event, handler) {
+ // Refer to jQuery's implementation of mouseenter & mouseleave
+ // Read about mouseenter and mouseleave:
+ // http://www.quirksmode.org/js/events_mouse.html#link8
+ var related = event.relatedTarget;
+ // For mousenter/leave call the handler if related is outside the target.
+ // NB: No relatedTarget if the mouse left/entered the browser window
+ if (!related || (related !== target && !jqLiteContains.call(target, related))) {
+ handler.call(target, event);
+ }
+}
+
//////////////////////////////////////////
// Functions iterating traversal.
// These functions chain results into a single
@@ -3307,35 +3457,28 @@ forEach({
var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
var i = types.length;
- while (i--) {
- type = types[i];
+ var addHandler = function(type, specialHandlerWrapper, noEventListener) {
var eventFns = events[type];
if (!eventFns) {
- events[type] = [];
-
- if (type === 'mouseenter' || type === 'mouseleave') {
- // Refer to jQuery's implementation of mouseenter & mouseleave
- // Read about mouseenter and mouseleave:
- // http://www.quirksmode.org/js/events_mouse.html#link8
-
- jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
- var target = this, related = event.relatedTarget;
- // For mousenter/leave call the handler if related is outside the target.
- // NB: No relatedTarget if the mouse left/entered the browser window
- if (!related || (related !== target && !target.contains(related))) {
- handle(event, type);
- }
- });
-
- } else {
- if (type !== '$destroy') {
- addEventListenerFn(element, type, handle);
- }
+ eventFns = events[type] = [];
+ eventFns.specialHandlerWrapper = specialHandlerWrapper;
+ if (type !== '$destroy' && !noEventListener) {
+ addEventListenerFn(element, type, handle);
}
- eventFns = events[type];
}
+
eventFns.push(fn);
+ };
+
+ while (i--) {
+ type = types[i];
+ if (MOUSE_EVENT_MAP[type]) {
+ addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper);
+ addHandler(type, undefined, true);
+ } else {
+ addHandler(type);
+ }
}
},
@@ -3403,12 +3546,7 @@ forEach({
},
wrap: function(element, wrapNode) {
- wrapNode = jqLite(wrapNode).eq(0).clone()[0];
- var parent = element.parentNode;
- if (parent) {
- parent.replaceChild(wrapNode, element);
- }
- wrapNode.appendChild(element);
+ jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]);
},
remove: jqLiteRemove,
@@ -3686,17 +3824,23 @@ var $$HashMapProvider = [function() {
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
+var ARROW_ARG = /^([^\(]+?)=>/;
+var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
+function extractArgs(fn) {
+ var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
+ args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
+ return args;
+}
+
function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
- var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
- args = fnText.match(FN_ARGS);
+ var args = extractArgs(fn);
if (args) {
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
}
@@ -3705,7 +3849,6 @@ function anonFn(fn) {
function annotate(fn, strictDi, name) {
var $inject,
- fnText,
argDecl,
last;
@@ -3720,8 +3863,7 @@ function annotate(fn, strictDi, name) {
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
- fnText = fn.toString().replace(STRIP_COMMENTS, '');
- argDecl = fnText.match(FN_ARGS);
+ argDecl = extractArgs(fn);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
@@ -4111,8 +4253,20 @@ function annotate(fn, strictDi, name) {
*
* Register a **service constructor**, which will be invoked with `new` to create the service
* instance.
- * This is short for registering a service where its provider's `$get` property is the service
- * constructor function that will be used to instantiate the service instance.
+ * This is short for registering a service where its provider's `$get` property is a factory
+ * function that returns an instance instantiated by the injector from the service constructor
+ * function.
+ *
+ * Internally it looks a bit like this:
+ *
+ * ```
+ * {
+ * $get: function() {
+ * return $injector.instantiate(constructor);
+ * }
+ * }
+ * ```
+ *
*
* You should use {@link auto.$provide#service $provide.service(class)} if you define your service
* as a type/class.
@@ -4152,14 +4306,13 @@ function annotate(fn, strictDi, name) {
* @description
*
* Register a **value service** with the {@link auto.$injector $injector}, such as a string, a
- * number, an array, an object or a function. This is short for registering a service where its
+ * number, an array, an object or a function. This is short for registering a service where its
* provider's `$get` property is a factory function that takes no arguments and returns the **value
- * service**.
+ * service**. That also means it is not possible to inject other services into a value service.
*
* Value services are similar to constant services, except that they cannot be injected into a
* module configuration function (see {@link angular.Module#config}) but they can be overridden by
- * an Angular
- * {@link auto.$provide#decorator decorator}.
+ * an Angular {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the instance.
* @param {*} value The value.
@@ -4184,8 +4337,11 @@ function annotate(fn, strictDi, name) {
* @name $provide#constant
* @description
*
- * Register a **constant service**, such as a string, a number, an array, an object or a function,
- * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be
+ * Register a **constant service** with the {@link auto.$injector $injector}, such as a string,
+ * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not
+ * possible to inject other services into a constant.
+ *
+ * But unlike {@link auto.$provide#value value}, a constant can be
* injected into a module configuration function (see {@link angular.Module#config}) and it cannot
* be overridden by an Angular {@link auto.$provide#decorator decorator}.
*
@@ -4213,7 +4369,7 @@ function annotate(fn, strictDi, name) {
* @description
*
* Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator
- * intercepts the creation of a service, allowing it to override or modify the behaviour of the
+ * intercepts the creation of a service, allowing it to override or modify the behavior of the
* service. The object returned by the decorator may be the original service, or a new service
* object which replaces or wraps and delegates to the original service.
*
@@ -4262,14 +4418,19 @@ function createInjector(modulesToLoad, strictDi) {
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),
instanceCache = {},
- instanceInjector = (instanceCache.$injector =
+ protoInstanceInjector =
createInternalInjector(instanceCache, function(serviceName, caller) {
var provider = providerInjector.get(serviceName + providerSuffix, caller);
- return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
- }));
-
+ return instanceInjector.invoke(
+ provider.$get, provider, undefined, serviceName);
+ }),
+ instanceInjector = protoInstanceInjector;
- forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
+ providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ var runBlocks = loadModules(modulesToLoad);
+ instanceInjector = protoInstanceInjector.get('$injector');
+ instanceInjector.strictDi = strictDi;
+ forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
return instanceInjector;
@@ -4342,6 +4503,7 @@ function createInjector(modulesToLoad, strictDi) {
// Module Loading
////////////////////////////////////
function loadModules(modulesToLoad) {
+ assertArg(isUndefined(modulesToLoad) || isArray(modulesToLoad), 'modulesToLoad', 'not an array');
var runBlocks = [], moduleFn;
forEach(modulesToLoad, function(module) {
if (loadedModules.get(module)) return;
@@ -4418,48 +4580,67 @@ function createInjector(modulesToLoad, strictDi) {
}
}
- function invoke(fn, self, locals, serviceName) {
- if (typeof locals === 'string') {
- serviceName = locals;
- locals = null;
- }
+ function injectionArgs(fn, locals, serviceName) {
var args = [],
- $inject = createInjector.$$annotate(fn, strictDi, serviceName),
- length, i,
- key;
+ $inject = createInjector.$$annotate(fn, strictDi, serviceName);
- for (i = 0, length = $inject.length; i < length; i++) {
- key = $inject[i];
+ for (var i = 0, length = $inject.length; i < length; i++) {
+ var key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
- args.push(
- locals && locals.hasOwnProperty(key)
- ? locals[key]
- : getService(key, serviceName)
- );
+ args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
+ getService(key, serviceName));
}
+ return args;
+ }
+
+ function isClass(func) {
+ // IE 9-11 do not support classes and IE9 leaks with the code below.
+ if (msie <= 11) {
+ return false;
+ }
+ // Workaround for MS Edge.
+ // Check https://connect.microsoft.com/IE/Feedback/Details/2211653
+ return typeof func === 'function'
+ && /^(?:class\s|constructor\()/.test(Function.prototype.toString.call(func));
+ }
+
+ function invoke(fn, self, locals, serviceName) {
+ if (typeof locals === 'string') {
+ serviceName = locals;
+ locals = null;
+ }
+
+ var args = injectionArgs(fn, locals, serviceName);
if (isArray(fn)) {
- fn = fn[length];
+ fn = fn[fn.length - 1];
}
- // http://jsperf.com/angularjs-invoke-apply-vs-switch
- // #5388
- return fn.apply(self, args);
+ if (!isClass(fn)) {
+ // http://jsperf.com/angularjs-invoke-apply-vs-switch
+ // #5388
+ return fn.apply(self, args);
+ } else {
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(fn, args))();
+ }
}
+
function instantiate(Type, locals, serviceName) {
// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
- // Object creation: http://jsperf.com/create-constructor/2
- var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
- var returnedValue = invoke(Type, instance, locals, serviceName);
-
- return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
+ var ctor = (isArray(Type) ? Type[Type.length - 1] : Type);
+ var args = injectionArgs(Type, locals, serviceName);
+ // Empty object at position 0 is ignored for invocation with `new`, but required.
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(ctor, args))();
}
+
return {
invoke: invoke,
instantiate: instantiate,
@@ -4515,7 +4696,7 @@ function $AnchorScrollProvider() {
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
* in the
- * [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
+ * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
*
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
* match any anchor whenever it changes. This can be disabled by calling
@@ -4798,27 +4979,8 @@ function prepareAnimateOptions(options) {
: {};
}
-var $$CoreAnimateRunnerProvider = function() {
- this.$get = ['$q', '$$rAF', function($q, $$rAF) {
- function AnimateRunner() {}
- AnimateRunner.all = noop;
- AnimateRunner.chain = noop;
- AnimateRunner.prototype = {
- end: noop,
- cancel: noop,
- resume: noop,
- pause: noop,
- complete: noop,
- then: function(pass, fail) {
- return $q(function(resolve) {
- $$rAF(function() {
- resolve();
- });
- }).then(pass, fail);
- }
- };
- return AnimateRunner;
- }];
+var $$CoreAnimateJsProvider = function() {
+ this.$get = noop;
};
// this is prefixed with Core since it conflicts with
@@ -4846,65 +5008,75 @@ var $$CoreAnimateQueueProvider = function() {
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- return new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner(); // jshint ignore:line
+
+ // since there are no animations to run the runner needs to be
+ // notified that the animation call is complete.
+ runner.complete();
+ return runner;
}
};
- function addRemoveClassesPostDigest(element, add, remove) {
- var data = postDigestQueue.get(element);
- var classVal;
-
- if (!data) {
- postDigestQueue.put(element, data = {});
- postDigestElements.push(element);
- }
- if (add) {
- forEach(add.split(' '), function(className) {
+ function updateData(data, classes, value) {
+ var changed = false;
+ if (classes) {
+ classes = isString(classes) ? classes.split(' ') :
+ isArray(classes) ? classes : [];
+ forEach(classes, function(className) {
if (className) {
- data[className] = true;
+ changed = true;
+ data[className] = value;
}
});
}
+ return changed;
+ }
+
+ function handleCSSClassChanges() {
+ forEach(postDigestElements, function(element) {
+ var data = postDigestQueue.get(element);
+ if (data) {
+ var existing = splitClasses(element.attr('class'));
+ var toAdd = '';
+ var toRemove = '';
+ forEach(data, function(status, className) {
+ var hasClass = !!existing[className];
+ if (status !== hasClass) {
+ if (status) {
+ toAdd += (toAdd.length ? ' ' : '') + className;
+ } else {
+ toRemove += (toRemove.length ? ' ' : '') + className;
+ }
+ }
+ });
- if (remove) {
- forEach(remove.split(' '), function(className) {
- if (className) {
- data[className] = false;
- }
- });
- }
+ forEach(element, function(elm) {
+ toAdd && jqLiteAddClass(elm, toAdd);
+ toRemove && jqLiteRemoveClass(elm, toRemove);
+ });
+ postDigestQueue.remove(element);
+ }
+ });
+ postDigestElements.length = 0;
+ }
- if (postDigestElements.length > 1) return;
-
- $rootScope.$$postDigest(function() {
- forEach(postDigestElements, function(element) {
- var data = postDigestQueue.get(element);
- if (data) {
- var existing = splitClasses(element.attr('class'));
- var toAdd = '';
- var toRemove = '';
- forEach(data, function(status, className) {
- var hasClass = !!existing[className];
- if (status !== hasClass) {
- if (status) {
- toAdd += (toAdd.length ? ' ' : '') + className;
- } else {
- toRemove += (toRemove.length ? ' ' : '') + className;
- }
- }
- });
- forEach(element, function(elm) {
- toAdd && jqLiteAddClass(elm, toAdd);
- toRemove && jqLiteRemoveClass(elm, toRemove);
- });
- postDigestQueue.remove(element);
- }
- });
+ function addRemoveClassesPostDigest(element, add, remove) {
+ var data = postDigestQueue.get(element) || {};
- postDigestElements.length = 0;
- });
+ var classesAdded = updateData(data, add, true);
+ var classesRemoved = updateData(data, remove, false);
+
+ if (classesAdded || classesRemoved) {
+
+ postDigestQueue.put(element, data);
+ postDigestElements.push(element);
+
+ if (postDigestElements.length === 1) {
+ $rootScope.$$postDigest(handleCSSClassChanges);
+ }
+ }
}
}];
};
@@ -5025,7 +5197,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* when an animation is detected (and animations are enabled), $animate will do the heavy lifting
* to ensure that animation runs with the triggered DOM operation.
*
- * By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
+ * By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't
* included and only when it is active then the animation hooks that `$animate` triggers will be
* functional. Once active then all structural `ng-` directives will trigger animations as they perform
* their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
@@ -5083,8 +5255,8 @@ var $AnimateProvider = ['$provide', function($provide) {
* // remove all the animation event listeners listening for `enter` on the given element and its children
* $animate.off('enter', container);
*
- * // remove the event listener function provided by `listenerFn` that is set
- * // to listen for `enter` on the given `element` as well as its children
+ * // remove the event listener function provided by `callback` that is set
+ * // to listen for `enter` on the given `container` as well as its children
* $animate.off('enter', container, callback);
* ```
*
@@ -5306,17 +5478,30 @@ var $AnimateProvider = ['$provide', function($provide) {
* @kind function
*
* @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
- * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take
- * on the provided styles. For example, if a transition animation is set for the given className then the provided from and
- * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles
- * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter).
+ * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take
+ * on the provided styles. For example, if a transition animation is set for the given classNamem, then the provided `from` and
+ * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding
+ * style in `to`, the style in `from` is applied immediately, and no animation is run.
+ * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate`
+ * method (or as part of the `options` parameter):
+ *
+ * ```js
+ * ngModule.animation('.my-inline-animation', function() {
+ * return {
+ * animate : function(element, from, to, done, options) {
+ * //animation
+ * done();
+ * }
+ * }
+ * });
+ * ```
*
* @param {DOMElement} element the element which the CSS styles will be applied to
* @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation.
* @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation.
* @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If
* this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element.
- * (Note that if no animation is detected then this value will not be appplied to the element.)
+ * (Note that if no animation is detected then this value will not be applied to the element.)
* @param {object=} options an optional collection of options/styles that will be applied to the element
*
* @return {Promise} the animation callback promise
@@ -5334,15 +5519,261 @@ var $AnimateProvider = ['$provide', function($provide) {
}];
}];
-function $$AsyncCallbackProvider() {
- this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) {
- return $$rAF.supported
- ? function(fn) { return $$rAF(fn); }
- : function(fn) {
- return $timeout(fn, 0, false);
+var $$AnimateAsyncRunFactoryProvider = function() {
+ this.$get = ['$$rAF', function($$rAF) {
+ var waitQueue = [];
+
+ function waitForTick(fn) {
+ waitQueue.push(fn);
+ if (waitQueue.length > 1) return;
+ $$rAF(function() {
+ for (var i = 0; i < waitQueue.length; i++) {
+ waitQueue[i]();
+ }
+ waitQueue = [];
+ });
+ }
+
+ return function() {
+ var passed = false;
+ waitForTick(function() {
+ passed = true;
+ });
+ return function(callback) {
+ passed ? callback() : waitForTick(callback);
};
+ };
}];
-}
+};
+
+var $$AnimateRunnerFactoryProvider = function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout',
+ function($q, $sniffer, $$animateAsyncRun, $document, $timeout) {
+
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ AnimateRunner.chain = function(chain, callback) {
+ var index = 0;
+
+ next();
+ function next() {
+ if (index === chain.length) {
+ callback(true);
+ return;
+ }
+
+ chain[index](function(response) {
+ if (response === false) {
+ callback(false);
+ return;
+ }
+ index++;
+ next();
+ });
+ }
+ };
+
+ AnimateRunner.all = function(runners, callback) {
+ var count = 0;
+ var status = true;
+ forEach(runners, function(runner) {
+ runner.done(onProgress);
+ });
+
+ function onProgress(response) {
+ status = status && response;
+ if (++count === runners.length) {
+ callback(status);
+ }
+ }
+ };
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ var rafTick = $$animateAsyncRun();
+ var timeoutTick = function(fn) {
+ $timeout(fn, 0, false);
+ };
+
+ this._doneCallbacks = [];
+ this._tick = function(fn) {
+ var doc = $document[0];
+
+ // the document may not be ready or attached
+ // to the module for some internal tests
+ if (doc && doc.hidden) {
+ timeoutTick(fn);
+ } else {
+ rafTick(fn);
+ }
+ };
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._tick(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+ }];
+};
+
+/**
+ * @ngdoc service
+ * @name $animateCss
+ * @kind object
+ *
+ * @description
+ * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included,
+ * then the `$animateCss` service will actually perform animations.
+ *
+ * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}.
+ */
+var $CoreAnimateCssProvider = function() {
+ this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) {
+
+ return function(element, initialOptions) {
+ // all of the animation functions should create
+ // a copy of the options data, however, if a
+ // parent service has already created a copy then
+ // we should stick to using that
+ var options = initialOptions || {};
+ if (!options.$$prepared) {
+ options = copy(options);
+ }
+
+ // there is no point in applying the styles since
+ // there is no animation that goes on at all in
+ // this version of $animateCss.
+ if (options.cleanupStyles) {
+ options.from = options.to = null;
+ }
+
+ if (options.from) {
+ element.css(options.from);
+ options.from = null;
+ }
+
+ /* jshint newcap: false */
+ var closed, runner = new $$AnimateRunner();
+ return {
+ start: run,
+ end: run
+ };
+
+ function run() {
+ $$rAF(function() {
+ applyAnimationContents();
+ if (!closed) {
+ runner.complete();
+ }
+ closed = true;
+ });
+ return runner;
+ }
+
+ function applyAnimationContents() {
+ if (options.addClass) {
+ element.addClass(options.addClass);
+ options.addClass = null;
+ }
+ if (options.removeClass) {
+ element.removeClass(options.removeClass);
+ options.removeClass = null;
+ }
+ if (options.to) {
+ element.css(options.to);
+ options.to = null;
+ }
+ }
+ };
+ }];
+};
/* global stripHash: true */
@@ -5369,7 +5800,6 @@ function $$AsyncCallbackProvider() {
*/
function Browser(window, document, $log, $sniffer) {
var self = this,
- rawDocument = document[0],
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
@@ -5432,7 +5862,14 @@ function Browser(window, document, $log, $sniffer) {
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
- reloadLocation = null;
+ pendingLocation = null,
+ getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
+ try {
+ return history.state;
+ } catch (e) {
+ // MSIE can reportedly throw when there is no state (UNCONFIRMED).
+ }
+ };
cacheState();
lastHistoryState = cachedState;
@@ -5492,8 +5929,8 @@ function Browser(window, document, $log, $sniffer) {
// Do the assignment again so that those two variables are referentially identical.
lastHistoryState = cachedState;
} else {
- if (!sameBase || reloadLocation) {
- reloadLocation = url;
+ if (!sameBase || pendingLocation) {
+ pendingLocation = url;
}
if (replace) {
location.replace(url);
@@ -5502,14 +5939,18 @@ function Browser(window, document, $log, $sniffer) {
} else {
location.hash = getHash(url);
}
+ if (location.href !== url) {
+ pendingLocation = url;
+ }
}
return self;
// getter
} else {
- // - reloadLocation is needed as browsers don't allow to read out
- // the new location.href if a reload happened.
+ // - pendingLocation is needed as browsers don't allow to read out
+ // the new location.href if a reload happened or if there is a bug like in iOS 9 (see
+ // https://openradar.appspot.com/22186109).
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return reloadLocation || location.href.replace(/%27/g,"'");
+ return pendingLocation || location.href.replace(/%27/g,"'");
}
};
@@ -5531,18 +5972,11 @@ function Browser(window, document, $log, $sniffer) {
urlChangeInit = false;
function cacheStateAndFireUrlChange() {
+ pendingLocation = null;
cacheState();
fireUrlChange();
}
- function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
- }
-
// This variable should be used *only* inside the cacheState function.
var lastCachedState = null;
function cacheState() {
@@ -5766,10 +6200,10 @@ function $BrowserProvider() {
$scope.keys = [];
$scope.cache = $cacheFactory('cacheId');
$scope.put = function(key, value) {
- if ($scope.cache.get(key) === undefined) {
+ if (angular.isUndefined($scope.cache.get(key))) {
$scope.keys.push(key);
}
- $scope.cache.put(key, value === undefined ? null : value);
+ $scope.cache.put(key, angular.isUndefined(value) ? null : value);
};
}]);
</file>
@@ -5792,9 +6226,9 @@ function $CacheFactoryProvider() {
var size = 0,
stats = extend({}, options, {id: cacheId}),
- data = {},
+ data = createMap(),
capacity = (options && options.capacity) || Number.MAX_VALUE,
- lruHash = {},
+ lruHash = createMap(),
freshEnd = null,
staleEnd = null;
@@ -5922,6 +6356,8 @@ function $CacheFactoryProvider() {
delete lruHash[key];
}
+ if (!(key in data)) return;
+
delete data[key];
size--;
},
@@ -5936,9 +6372,9 @@ function $CacheFactoryProvider() {
* Clears the cache object of any entries.
*/
removeAll: function() {
- data = {};
+ data = createMap();
size = 0;
- lruHash = {};
+ lruHash = createMap();
freshEnd = staleEnd = null;
},
@@ -6227,7 +6663,7 @@ function $TemplateCacheProvider() {
* When this property is set to true, the HTML compiler will collect DOM nodes between
* nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
* together as the directive elements. It is recommended that this feature be used on directives
- * which are not strictly behavioural (such as {@link ngClick}), and which
+ * which are not strictly behavioral (such as {@link ngClick}), and which
* do not manipulate or replace child nodes (such as {@link ngInclude}).
*
* #### `priority`
@@ -6245,59 +6681,129 @@ function $TemplateCacheProvider() {
* and other directives used in the directive's template will also be excluded from execution.
*
* #### `scope`
- * **If set to `true`,** then a new scope will be created for this directive. If multiple directives on the
- * same element request a new scope, only one new scope is created. The new scope rule does not
- * apply for the root of the template since the root of the template always gets a new scope.
+ * The scope property can be `true`, an object or a falsy value:
*
- * **If set to `{}` (object hash),** then a new "isolate" scope is created. The 'isolate' scope differs from
- * normal scope in that it does not prototypically inherit from the parent scope. This is useful
- * when creating reusable components, which should not accidentally read or modify data in the
- * parent scope.
+ * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
*
- * The 'isolate' scope takes an object hash which defines a set of local scope properties
- * derived from the parent scope. These local properties are useful for aliasing values for
- * templates. Locals definition is a hash of local scope property to its source:
+ * * **`true`:** A new child scope that prototypically inherits from its parent will be created for
+ * the directive's element. If multiple directives on the same element request a new scope,
+ * only one new scope is created. The new scope rule does not apply for the root of the template
+ * since the root of the template always gets a new scope.
+ *
+ * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
+ * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
+ * scope. This is useful when creating reusable components, which should not accidentally read or modify
+ * data in the parent scope.
+ *
+ * The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the
+ * directive's element. These local properties are useful for aliasing values for templates. The keys in
+ * the object hash map to the name of the property on the isolate scope; the values define how the property
+ * is bound to the parent scope, via matching attributes on the directive's element:
*
* * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
- * always a string since DOM attributes are strings. If no `attr` name is specified then the
- * attribute name is assumed to be the same as the local name.
- * Given `<widget my-attr="hello {{name}}">` and widget definition
- * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect
- * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the
- * `localName` property on the widget scope. The `name` is read from the parent scope (not
- * component scope).
- *
- * * `=` or `=attr` - set up bi-directional binding between a local scope property and the
- * parent scope property of name defined via the value of the `attr` attribute. If no `attr`
- * name is specified then the attribute name is assumed to be the same as the local name.
- * Given `<widget my-attr="parentModel">` and widget definition of
- * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the
+ * always a string since DOM attributes are strings. If no `attr` name is specified then the
+ * attribute name is assumed to be the same as the local name. Given `<my-component
+ * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`,
+ * the directive's scope property `localName` will reflect the interpolated value of `hello
+ * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's
+ * scope. The `name` is read from the parent scope (not the directive's scope).
+ *
+ * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression
+ * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope.
+ * If no `attr` name is specified then the attribute name is assumed to be the same as the local
+ * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: {
+ * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the
+ * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in
+ * `localModel` and vice versa. Optional attributes should be marked as such with a question mark:
+ * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't
+ * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`})
+ * will be thrown upon discovering changes to the local value, since it will be impossible to sync
+ * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
+ * method is used for tracking changes, and the equality check is based on object identity.
+ * However, if an object literal or an array literal is passed as the binding expression, the
+ * equality check is done by value (using the {@link angular.equals} function). It's also possible
+ * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
+ * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
+ *
+ * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
+ * expression passed via the attribute `attr`. The expression is evaluated in the context of the
+ * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
+ * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
+ *
+ * For example, given `<my-component my-attr="parentModel">` and directive definition of
+ * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
* value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
- * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
- * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
- * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If
- * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use
- * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional).
- *
- * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
- * If no `attr` name is specified then the attribute name is assumed to be the same as the
- * local name. Given `<widget my-attr="count = count + value">` and widget definition of
- * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to
- * a function wrapper for the `count = count + value` expression. Often it's desirable to
- * pass data from the isolated scope via an expression to the parent scope, this can be
- * done by passing a map of local variable names and values into the expression wrapper fn.
- * For example, if the expression is `increment(amount)` then we can specify the amount value
- * by calling the `localFn` as `localFn({amount: 22})`.
+ * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
+ * two caveats:
+ * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
+ * sets the same value. That means if your bound value is an object, changes to its properties
+ * in the isolated scope will be reflected in the parent scope (because both reference the same object).
+ * 2. one-way binding watches changes to the **identity** of the parent value. That means the
+ * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
+ * to the value has changed. In most cases, this should not be of concern, but can be important
+ * to know if you one-way bind to an object, and then replace that object in the isolated scope.
+ * If you now change a property of the object in your parent scope, the change will not be
+ * propagated to the isolated scope, because the identity of the object on the parent scope
+ * has not changed. Instead you must assign a new object.
+ *
+ * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
+ * back to the parent. However, it does not make this completely impossible.
+ *
+ * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
+ * no `attr` name is specified then the attribute name is assumed to be the same as the local name.
+ * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
+ * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for
+ * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope
+ * via an expression to the parent scope. This can be done by passing a map of local variable names
+ * and values into the expression wrapper fn. For example, if the expression is `increment(amount)`
+ * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`.
+ *
+ * In general it's possible to apply more than one directive to one element, but there might be limitations
+ * depending on the type of scope required by the directives. The following points will help explain these limitations.
+ * For simplicity only two directives are taken into account, but it is also applicable for several directives:
+ *
+ * * **no scope** + **no scope** => Two directives which don't require their own scope will use their parent's scope
+ * * **child scope** + **no scope** => Both directives will share one single child scope
+ * * **child scope** + **child scope** => Both directives will share one single child scope
+ * * **isolated scope** + **no scope** => The isolated directive will use it's own created isolated scope. The other directive will use
+ * its parent's scope
+ * * **isolated scope** + **child scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives cannot
+ * be applied to the same element.
+ * * **isolated scope** + **isolated scope** => **Won't work!** Only one scope can be related to one element. Therefore these directives
+ * cannot be applied to the same element.
*
*
* #### `bindToController`
- * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will
- * allow a component to have its properties bound to the controller, rather than to scope. When the controller
- * is instantiated, the initial values of the isolate scope bindings are already available.
+ * This property is used to bind scope properties directly to the controller. It can be either
+ * `true` or an object hash with the same format as the `scope` property. Additionally, a controller
+ * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller
+ * definition: `controller: 'myCtrl as myAlias'`.
+ *
+ * When an isolate scope is used for a directive (see above), `bindToController: true` will
+ * allow a component to have its properties bound to the controller, rather than to scope.
+ *
+ * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller
+ * properties. You can access these bindings once they have been initialized by providing a controller method called
+ * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
+ * initialized.
+ *
+ * <div class="alert alert-warning">
+ * **Deprecation warning:** although bindings for non-ES6 class controllers are currently
+ * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
+ * code that relies upon bindings inside a `$onInit` method on the controller, instead.
+ * </div>
+ *
+ * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
+ * This will set up the scope bindings to the controller directly. Note that `scope` can still be used
+ * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate
+ * scope (useful for component directives).
+ *
+ * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`.
+ *
*
* #### `controller`
* Controller constructor function. The controller is instantiated before the
- * pre-linking phase and it is shared with other directives (see
+ * pre-linking phase and can be accessed by other directives (see
* `require` attribute). This allows the directives to communicate with each other and augment
* each other's behavior. The controller is injectable (and supports bracket notation) with the following locals:
*
@@ -6305,10 +6811,10 @@ function $TemplateCacheProvider() {
* * `$element` - Current element
* * `$attrs` - Current attributes object for the element
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
- * `function([scope], cloneLinkingFn, futureParentElement)`.
- * * `scope`: optional argument to override the scope.
- * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content.
- * * `futureParentElement`:
+ * `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
+ * * `scope`: (optional) override the scope.
+ * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
+ * * `futureParentElement` (optional):
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
@@ -6316,14 +6822,48 @@ function $TemplateCacheProvider() {
* as those elements need to created and cloned in a special way when they are defined outside their
* usual containers (e.g. like `<svg>`).
* * See also the `directive.templateNamespace` property.
+ * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
+ * then the default translusion is provided.
+ * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
+ * `true` if the specified slot contains content (i.e. one or more DOM nodes).
+ *
+ * The controller can provide the following methods that act as life-cycle hooks:
+ * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
+ * had their bindings initialized (and before the pre &amp; post linking functions for the directives on
+ * this element). This is a good place to put initialization code for your controller.
+ * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
+ * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
+ * object of the form `{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component
+ * such as cloning the bound value to prevent accidental mutation of the outer value.
+ * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
+ * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
+ * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
+ * components will have their `$onDestroy()` hook called before child components.
+ * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
+ * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
+ * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
+ * they are waiting for their template to load asynchronously and their own compilation and linking has been
+ * suspended until that occurs.
*
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
- * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the
- * injected argument will be an array in corresponding order. If no such directive can be
- * found, or if the directive does not have a controller, then an error is raised (unless no link function
- * is specified, in which case error checking is skipped). The name can be prefixed with:
+ * `require` property can be a string, an array or an object:
+ * * a **string** containing the name of the directive to pass to the linking function
+ * * an **array** containing the names of directives to pass to the linking function. The argument passed to the
+ * linking function will be an array of controllers in the same order as the names in the `require` property
+ * * an **object** whose property values are the names of the directives to pass to the linking function. The argument
+ * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
+ * controllers.
+ *
+ * If the `require` property is an object and `bindToController` is truthy, then the required controllers are
+ * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
+ * have been constructed but before `$onInit` is called.
+ * See the {@link $compileProvider#component} helper for an example of how this can be used.
+ *
+ * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
+ * raised (unless no link function is specified and the required controllers are not being bound to the directive
+ * controller, in which case error checking is skipped). The name can be prefixed with:
*
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
@@ -6337,9 +6877,10 @@ function $TemplateCacheProvider() {
*
* #### `controllerAs`
* Identifier name for a reference to the controller in the directive's scope.
- * This allows the controller to be referenced from the directive template. The directive
- * needs to define a scope for this configuration to be used. Useful in the case when
- * directive is used as component.
+ * This allows the controller to be referenced from the directive template. This is especially
+ * useful when a directive is used as component, i.e. with an `isolate` scope. It's also possible
+ * to use it in a directive without an `isolate` / `new` scope, but you need to be aware that the
+ * `controllerAs` reference might overwrite a property that already exists on the parent scope.
*
*
* #### `restrict`
@@ -6415,14 +6956,6 @@ function $TemplateCacheProvider() {
* The contents are compiled and provided to the directive as a **transclusion function**. See the
* {@link $compile#transclusion Transclusion} section below.
*
- * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
- * directive's element or the entire element:
- *
- * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
- * * `'element'` - transclude the whole of the directive's element including any directives on this
- * element that defined at a lower priority than this directive. When used, the `template`
- * property is ignored.
- *
*
* #### `compile`
*
@@ -6450,7 +6983,7 @@ function $TemplateCacheProvider() {
* <div class="alert alert-warning">
* **Note:** The compile function cannot handle directives that recursively use themselves in their
- * own templates or compile functions. Compiling these directives results in an infinite loop and a
+ * own templates or compile functions. Compiling these directives results in an infinite loop and
* stack overflow errors.
*
* This can be avoided by manually using $compile in the postLink function to imperatively compile
@@ -6506,7 +7039,7 @@ function $TemplateCacheProvider() {
* otherwise the {@link error:$compile:ctreq Missing Required Controller} error is thrown.
*
* Note that you can also require the directive's own controller - it will be made available like
- * like any other controller.
+ * any other controller.
*
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
* This is the same as the `$transclude`
@@ -6532,7 +7065,7 @@ function $TemplateCacheProvider() {
*
* ### Transclusion
*
- * Transclusion is the process of extracting a collection of DOM element from one part of the DOM and
+ * Transclusion is the process of extracting a collection of DOM elements from one part of the DOM and
* copying them to another part of the DOM, while maintaining their connection to the original AngularJS
* scope from where they were taken.
*
@@ -6552,6 +7085,34 @@ function $TemplateCacheProvider() {
* Testing Transclusion Directives}.
* </div>
*
+ * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
+ * directive's element, the entire element or multiple parts of the element contents:
+ *
+ * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
+ * * `'element'` - transclude the whole of the directive's element including any directives on this
+ * element that defined at a lower priority than this directive. When used, the `template`
+ * property is ignored.
+ * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
+ *
+ * **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
+ *
+ * This object is a map where the keys are the name of the slot to fill and the value is an element selector
+ * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`)
+ * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc).
+ *
+ * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
+ *
+ * If the element selector is prefixed with a `?` then that slot is optional.
+ *
+ * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `<my-custom-element>` elements to
+ * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
+ *
+ * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
+ * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
+ * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
+ * injectable into the directive's controller.
+ *
+ *
* #### Transclusion Functions
*
* When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
@@ -6572,7 +7133,7 @@ function $TemplateCacheProvider() {
* content and the `scope` is the newly created transclusion scope, to which the clone is bound.
*
* <div class="alert alert-info">
- * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function
+ * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function
* since you then get a fresh clone of the original DOM and also have access to the new transclusion scope.
* </div>
*
@@ -6604,7 +7165,7 @@ function $TemplateCacheProvider() {
* </div>
*
* The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat}
- * automatically destroy their transluded clones as necessary so you do not need to worry about this if
+ * automatically destroy their transcluded clones as necessary so you do not need to worry about this if
* you are simply using {@link ngTransclude} to inject the transclusion into your directive.
*
*
@@ -6629,19 +7190,19 @@ function $TemplateCacheProvider() {
*
* The `$parent` scope hierarchy will look like this:
*
- * ```
- * - $rootScope
- * - isolate
- * - transclusion
- * ```
+ ```
+ - $rootScope
+ - isolate
+ - transclusion
+ ```
*
* but the scopes will inherit prototypically from different scopes to their `$parent`.
*
- * ```
- * - $rootScope
- * - transclusion
- * - isolate
- * ```
+ ```
+ - $rootScope
+ - transclusion
+ - isolate
+ ```
*
*
* ### Attributes
@@ -6649,10 +7210,9 @@ function $TemplateCacheProvider() {
* The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
* `link()` or `compile()` functions. It has a variety of uses.
*
- * accessing *Normalized attribute names:*
- * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'.
- * the attributes object allows for normalized access to
- * the attributes.
+ * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways:
+ * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access
+ * to the attributes.
*
* * *Directive inter-communication:* All directives share the same instance of the attributes
* object which allows the directives to use the attributes object as inter directive
@@ -6773,8 +7333,15 @@ function $TemplateCacheProvider() {
* directives; if given, it will be passed through to the link functions of
* directives found in `element` during compilation.
* * `transcludeControllers` - an object hash with keys that map controller names
- * to controller instances; if given, it will make the controllers
- * available to directives.
+ * to a hash with the key `instance`, which maps to the controller instance;
+ * if given, it will make the controllers available to directives on the compileNode:
+ * ```
+ * {
+ * parent: {
+ * instance: parentControllerInstance
+ * }
+ * }
+ * ```
* * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
* the cloned elements; only needed for transcludes that are allowed to contain non html
* elements (e.g. SVG elements). See also the directive.controller property.
@@ -6833,13 +7400,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// The assumption is that future DOM event attribute names will begin with
// 'on' and be composed of only English letters.
var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
+ var bindingCache = createMap();
function parseIsolateBindings(scope, directiveName, isController) {
- var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
+ var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/;
var bindings = {};
forEach(scope, function(definition, scopeName) {
+ if (definition in bindingCache) {
+ bindings[scopeName] = bindingCache[definition];
+ return;
+ }
var match = definition.match(LOCAL_REGEXP);
if (!match) {
@@ -6857,6 +7429,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
optional: match[3] === '?',
attrName: match[4] || scopeName
};
+ if (match[4]) {
+ bindingCache[definition] = bindings[scopeName];
+ }
});
return bindings;
@@ -6902,11 +7477,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function assertValidDirectiveName(name) {
var letter = name.charAt(0);
if (!letter || letter !== lowercase(letter)) {
- throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
+ throw $compileMinErr('baddir', "Directive/Component name '{0}' is invalid. The first character must be a lowercase letter", name);
}
if (name !== name.trim()) {
throw $compileMinErr('baddir',
- "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
+ "Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
name);
}
}
@@ -6922,11 +7497,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
* will match as <code>ng-bind</code>), or an object map of directives where the keys are the
* names and the values are the factories.
- * @param {Function|Array} directiveFactory An injectable directive factory function. See
- * {@link guide/directive} for more info.
+ * @param {Function|Array} directiveFactory An injectable directive factory function. See the
+ * {@link guide/directive directive guide} and the {@link $compile compile API} for more info.
* @returns {ng.$compileProvider} Self for chaining.
*/
- this.directive = function registerDirective(name, directiveFactory) {
+ this.directive = function registerDirective(name, directiveFactory) {
assertNotHasOwnProperty(name, 'directive');
if (isString(name)) {
assertValidDirectiveName(name);
@@ -6949,11 +7524,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directive.name = directive.name || name;
directive.require = directive.require || (directive.controller && directive.name);
directive.restrict = directive.restrict || 'EA';
- var bindings = directive.$$bindings =
- parseDirectiveBindings(directive, directive.name);
- if (isObject(bindings.isolateScope)) {
- directive.$$isolateBindings = bindings.isolateScope;
- }
directive.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
} catch (e) {
@@ -6970,6 +7540,131 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return this;
};
+ /**
+ * @ngdoc method
+ * @name $compileProvider#component
+ * @module ng
+ * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`)
+ * @param {Object} options Component definition object (a simplified
+ * {@link ng.$compile#directive-definition-object directive definition object}),
+ * with the following properties (all optional):
+ *
+ * - `controller` – `{(string|function()=}` – controller constructor function that should be
+ * associated with newly created scope or the name of a {@link ng.$compile#-controller-
+ * registered controller} if passed as a string. An empty `noop` function by default.
+ * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope.
+ * If present, the controller will be published to scope under the `controllerAs` name.
+ * If not present, this will default to be `$ctrl`.
+ * - `template` – `{string=|function()=}` – html template as a string or a function that
+ * returns an html template as a string which should be used as the contents of this component.
+ * Empty string by default.
+ *
+ * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
+ * template that should be used as the contents of this component.
+ *
+ * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties.
+ * Component properties are always bound to the component controller and not to the scope.
+ * See {@link ng.$compile#-bindtocontroller- `bindToController`}.
+ * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled.
+ * Disabled by default.
+ * - `$...` – additional properties to attach to the directive factory function and the controller
+ * constructor function. (This is used by the component router to annotate)
+ *
+ * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls.
+ * @description
+ * Register a **component definition** with the compiler. This is a shorthand for registering a special
+ * type of directive, which represents a self-contained UI component in your application. Such components
+ * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`).
+ *
+ * Component definitions are very simple and do not require as much configuration as defining general
+ * directives. Component definitions usually consist only of a template and a controller backing it.
+ *
+ * In order to make the definition easier, components enforce best practices like use of `controllerAs`,
+ * `bindToController`. They always have **isolate scope** and are restricted to elements.
+ *
+ * Here are a few examples of how you would usually define components:
+ *
+ * ```js
+ * var myMod = angular.module(...);
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * controller: function() {
+ * this.name = 'shahar';
+ * }
+ * });
+ *
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * bindings: {name: '@'}
+ * });
+ *
+ * myMod.component('myComp', {
+ * templateUrl: 'views/my-comp.html',
+ * controller: 'MyCtrl',
+ * controllerAs: 'ctrl',
+ * bindings: {name: '@'}
+ * });
+ *
+ * ```
+ * For more examples, and an in-depth guide, see the {@link guide/component component guide}.
+ *
+ * <br />
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
+ */
+ this.component = function registerComponent(name, options) {
+ var controller = options.controller || noop;
+
+ function factory($injector) {
+ function makeInjectable(fn) {
+ if (isFunction(fn) || isArray(fn)) {
+ return function(tElement, tAttrs) {
+ return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
+ };
+ } else {
+ return fn;
+ }
+ }
+
+ var template = (!options.template && !options.templateUrl ? '' : options.template);
+ return {
+ controller: controller,
+ controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
+ template: makeInjectable(template),
+ templateUrl: makeInjectable(options.templateUrl),
+ transclude: options.transclude,
+ scope: {},
+ bindToController: options.bindings || {},
+ restrict: 'E',
+ require: options.require
+ };
+ }
+
+ // Copy any annotation properties (starting with $) over to the factory function
+ // These could be used by libraries such as the new component router
+ forEach(options, function(val, key) {
+ if (key.charAt(0) === '$') {
+ factory[key] = val;
+ controller[key] = val;
+ }
+ });
+
+ factory.$inject = ['$injector'];
+
+ return this.directive(name, factory);
+ };
+
/**
* @ngdoc method
@@ -7061,13 +7756,75 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
+
+ var TTL = 10;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#onChangesTtl
+ * @description
+ *
+ * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
+ * assuming that the model is unstable.
+ *
+ * The current default is 10 iterations.
+ *
+ * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
+ * in several iterations of calls to these hooks. However if an application needs more than the default 10
+ * iterations to stabilize then you should investigate what is causing the model to continuously change during
+ * the `$onChanges` hook execution.
+ *
+ * Increasing the TTL could have performance implications, so you should not change it without proper justification.
+ *
+ * @param {number} limit The number of `$onChanges` hook iterations.
+ * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
+ */
+ this.onChangesTtl = function(value) {
+ if (arguments.length) {
+ TTL = value;
+ return this;
+ }
+ return TTL;
+ };
+
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
+ '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
+ $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
+
+ var SIMPLE_ATTR_NAME = /^\w/;
+ var specialAttrHolder = document.createElement('div');
+
- var Attributes = function(element, attributesToCopy) {
+
+ var onChangesTtl = TTL;
+ // The onChanges hooks should all be run together in a single digest
+ // When changes occur, the call to trigger their hooks will be added to this queue
+ var onChangesQueue;
+
+ // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
+ function flushOnChangesQueue() {
+ try {
+ if (!(--onChangesTtl)) {
+ // We have hit the TTL limit so reset everything
+ onChangesQueue = undefined;
+ throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
+ }
+ // We must run this hook in an apply since the $$postDigest runs outside apply
+ $rootScope.$apply(function() {
+ for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
+ onChangesQueue[i]();
+ }
+ // Reset the queue to trigger a new schedule next time there is a change
+ onChangesQueue = undefined;
+ });
+ } finally {
+ onChangesTtl++;
+ }
+ }
+
+
+ function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
var i, l, key;
@@ -7081,7 +7838,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
this.$$element = element;
- };
+ }
Attributes.prototype = {
/**
@@ -7176,7 +7933,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
- aliasedKey = getAliasedAttrName(node, key),
+ aliasedKey = getAliasedAttrName(key),
observer = key,
nodeName;
@@ -7202,7 +7959,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
- if ((nodeName === 'a' && key === 'href') ||
+ if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
(nodeName === 'img' && key === 'src')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
@@ -7243,10 +8000,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (writeAttr !== false) {
- if (value === null || value === undefined) {
+ if (value === null || isUndefined(value)) {
this.$$element.removeAttr(attrName);
} else {
- this.$$element.attr(attrName, value);
+ if (SIMPLE_ATTR_NAME.test(attrName)) {
+ this.$$element.attr(attrName, value);
+ } else {
+ setSpecialAttr(this.$$element[0], attrName, value);
+ }
}
}
@@ -7277,7 +8038,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string} key Normalized key. (ie ngAttribute) .
* @param {function(interpolatedValue)} fn Function that will be called whenever
the interpolated value of the attribute changes.
- * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info.
+ * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation
+ * guide} for more info.
* @returns {function()} Returns a deregistration function for this observer.
*/
$observe: function(key, fn) {
@@ -7287,7 +8049,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
listeners.push(fn);
$rootScope.$evalAsync(function() {
- if (!listeners.$$inter && attrs.hasOwnProperty(key)) {
+ if (!listeners.$$inter && attrs.hasOwnProperty(key) && !isUndefined(attrs[key])) {
// no one registered attribute interpolation function, so lets call it manually
fn(attrs[key]);
}
@@ -7299,6 +8061,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
};
+ function setSpecialAttr(element, attrName, value) {
+ // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute`
+ // so we have to jump through some hoops to get such an attribute
+ // https://github.com/angular/angular.js/pull/13318
+ specialAttrHolder.innerHTML = "<span " + attrName + ">";
+ var attributes = specialAttrHolder.firstChild.attributes;
+ var attribute = attributes[0];
+ // We have to remove the attribute from its container element before we can add it to the destination element
+ attributes.removeNamedItem(attribute.name);
+ attribute.value = value;
+ element.attributes.setNamedItem(attribute);
+ }
function safeAddClass($element, className) {
try {
@@ -7312,12 +8086,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
- denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
+ denormalizeTemplate = (startSymbol == '{{' && endSymbol == '}}')
? identity
: function denormalizeTemplate(template) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
+ var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
var bindings = $element.data('$binding') || [];
@@ -7344,6 +8119,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
} : noop;
+ compile.$$createComment = function(directiveName, comment) {
+ var content = '';
+ if (debugInfoEnabled) {
+ content = ' ' + (directiveName || '') + ': ' + (comment || '') + ' ';
+ }
+ return document.createComment(content);
+ };
+
return compile;
//================================
@@ -7355,13 +8138,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// modify it.
$compileNodes = jqLite($compileNodes);
}
+
+ var NOT_EMPTY = /\S+/;
+
// We can not compile top level text elements since text nodes can be merged and we will
// not be able to attach scope data to them, so we will wrap them in <span>
- forEach($compileNodes, function(node, index) {
- if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */ ) {
- $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
+ for (var i = 0, len = $compileNodes.length; i < len; i++) {
+ var domNode = $compileNodes[i];
+
+ if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
+ jqLiteWrapNode(domNode, $compileNodes[i] = document.createElement('span'));
}
- });
+ }
+
var compositeLinkFn =
compileNodes($compileNodes, transcludeFn, $compileNodes,
maxPriority, ignoreDirective, previousCompileContext);
@@ -7370,6 +8159,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function publicLinkFn(scope, cloneConnectFn, options) {
assertArg(scope, 'scope');
+ if (previousCompileContext && previousCompileContext.needsNewScope) {
+ // A parent directive did a replace and a directive on this element asked
+ // for transclusion, which caused us to lose a layer of element on which
+ // we could hold the new transclusion scope, so we will create it manually
+ // here.
+ scope = scope.$parent.$new();
+ }
+
options = options || {};
var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
transcludeControllers = options.transcludeControllers,
@@ -7424,7 +8221,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!node) {
return 'html';
} else {
- return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html';
+ return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html';
}
}
@@ -7515,11 +8312,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (nodeLinkFn.scope) {
childScope = scope.$new();
compile.$$addScopeInfo(jqLite(node), childScope);
- var destroyBindings = nodeLinkFn.$$destroyBindings;
- if (destroyBindings) {
- nodeLinkFn.$$destroyBindings = null;
- childScope.$on('$destroyed', destroyBindings);
- }
} else {
childScope = scope;
}
@@ -7538,8 +8330,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = null;
}
- nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
- nodeLinkFn);
+ nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
@@ -7549,8 +8340,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
-
- var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
+ function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
transcludedScope = scope.$new(false, containingScope);
@@ -7562,7 +8352,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transcludeControllers: controllers,
futureParentElement: futureParentElement
});
- };
+ }
+
+ // We need to attach the transclusion slots onto the `boundTranscludeFn`
+ // so that they are available inside the `controllersBoundTransclude` function
+ var boundSlots = boundTranscludeFn.$$slots = createMap();
+ for (var slotName in transcludeFn.$$slots) {
+ if (transcludeFn.$$slots[slotName]) {
+ boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
+ } else {
+ boundSlots[slotName] = null;
+ }
+ }
return boundTranscludeFn;
}
@@ -7608,13 +8409,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
}
- var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
- if (directiveIsMultiElement(directiveNName)) {
- if (ngAttrName === directiveNName + 'Start') {
- attrStartName = name;
- attrEndName = name.substr(0, name.length - 5) + 'end';
- name = name.substr(0, name.length - 6);
- }
+ var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
+ if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
+ attrStartName = name;
+ attrEndName = name.substr(0, name.length - 5) + 'end';
+ name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
@@ -7718,13 +8517,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @returns {Function}
*/
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
- return function(scope, element, attrs, controllers, transcludeFn) {
+ return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) {
element = groupScan(element[0], attrStart, attrEnd);
return linkFn(scope, element, attrs, controllers, transcludeFn);
};
}
/**
+ * A function generator that is used to support both eager and lazy compilation
+ * linking function.
+ * @param eager
+ * @param $compileNodes
+ * @param transcludeFn
+ * @param maxPriority
+ * @param ignoreDirective
+ * @param previousCompileContext
+ * @returns {Function}
+ */
+ function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
+ var compiled;
+
+ if (eager) {
+ return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+ }
+ return function lazyCompilation() {
+ if (!compiled) {
+ compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+
+ // Null out all of these references in order to make them eligible for garbage collection
+ // since this is a potentially long lived closure
+ $compileNodes = transcludeFn = previousCompileContext = null;
+ }
+ return compiled.apply(this, arguments);
+ };
+ }
+
+ /**
* Once the directives have been collected, their compile functions are executed. This method
* is responsible for inlining directive templates as well as terminating the application
* of the directives if the terminal directive has been reached.
@@ -7768,6 +8596,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = originalReplaceDirective,
childTranscludeFn = transcludeFn,
linkFn,
+ didScanForMultipleTransclusion = false,
+ mightHaveMultipleTransclusionError = false,
directiveValue;
// executes all directives on the current element
@@ -7810,6 +8640,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directiveName = directive.name;
+ // If we encounter a condition that can result in transclusion on the directive,
+ // then scan ahead in the remaining directives for others that may cause a multiple
+ // transclusion error to be thrown during the compilation process. If a matching directive
+ // is found, then we know that when we encounter a transcluded directive, we need to eagerly
+ // compile the `transclude` function rather than doing it lazily in order to throw
+ // exceptions at the correct time
+ if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
+ || (directive.transclude && !directive.$$tlb))) {
+ var candidateDirective;
+
+ for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
+ if ((candidateDirective.transclude && !candidateDirective.$$tlb)
+ || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
+ mightHaveMultipleTransclusionError = true;
+ break;
+ }
+ }
+
+ didScanForMultipleTransclusion = true;
+ }
+
if (!directive.templateUrl && directive.controller) {
directiveValue = directive.controller;
controllerDirectives = controllerDirectives || createMap();
@@ -7834,12 +8685,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
terminalPriority = directive.priority;
$template = $compileNode;
$compileNode = templateAttrs.$$element =
- jqLite(document.createComment(' ' + directiveName + ': ' +
- templateAttrs[directiveName] + ' '));
+ jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName]));
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);
- childTranscludeFn = compile($template, transcludeFn, terminalPriority,
+ // Support: Chrome < 50
+ // https://github.com/angular/angular.js/issues/14041
+
+ // In the versions of V8 prior to Chrome 50, the document fragment that is created
+ // in the `replaceWith` function is improperly garbage collected despite still
+ // being referenced by the `parentNode` property of all of the child nodes. By adding
+ // a reference to the fragment via a different property, we can avoid that incorrect
+ // behavior.
+ // TODO: remove this line after Chrome 50 has been released
+ $template[0].$$parentNode = $template[0].parentNode;
+
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name, {
// Don't pass in:
// - controllerDirectives - otherwise we'll create duplicates controllers
@@ -7851,9 +8712,69 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
} else {
+
+ var slots = createMap();
+
$template = jqLite(jqLiteClone(compileNode)).contents();
+
+ if (isObject(directiveValue)) {
+
+ // We have transclusion slots,
+ // collect them up, compile them and store their transclusion functions
+ $template = [];
+
+ var slotMap = createMap();
+ var filledSlots = createMap();
+
+ // Parse the element selectors
+ forEach(directiveValue, function(elementSelector, slotName) {
+ // If an element selector starts with a ? then it is optional
+ var optional = (elementSelector.charAt(0) === '?');
+ elementSelector = optional ? elementSelector.substring(1) : elementSelector;
+
+ slotMap[elementSelector] = slotName;
+
+ // We explicitly assign `null` since this implies that a slot was defined but not filled.
+ // Later when calling boundTransclusion functions with a slot name we only error if the
+ // slot is `undefined`
+ slots[slotName] = null;
+
+ // filledSlots contains `true` for all slots that are either optional or have been
+ // filled. This is used to check that we have not missed any required slots
+ filledSlots[slotName] = optional;
+ });
+
+ // Add the matching elements into their slot
+ forEach($compileNode.contents(), function(node) {
+ var slotName = slotMap[directiveNormalize(nodeName_(node))];
+ if (slotName) {
+ filledSlots[slotName] = true;
+ slots[slotName] = slots[slotName] || [];
+ slots[slotName].push(node);
+ } else {
+ $template.push(node);
+ }
+ });
+
+ // Check for required slots that were not filled
+ forEach(filledSlots, function(filled, slotName) {
+ if (!filled) {
+ throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
+ }
+ });
+
+ for (var slotName in slots) {
+ if (slots[slotName]) {
+ // Only define a transclusion function if the slot was filled
+ slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
+ }
+ }
+ }
+
$compileNode.empty(); // clear contents
- childTranscludeFn = compile($template, transcludeFn);
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
+ undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
+ childTranscludeFn.$$slots = slots;
}
}
@@ -7895,8 +8816,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
- if (newIsolateScopeDirective) {
- markDirectivesAsIsolate(templateDirectives);
+ if (newIsolateScopeDirective || newScopeDirective) {
+ // The original directive caused the current element to be replaced but this element
+ // also needs to have a new scope, so we need to tell the template directives
+ // that they would need to get their scope from further up, if they require transclusion
+ markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
}
directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
@@ -7978,81 +8902,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
-
- function getControllers(directiveName, require, $element, elementControllers) {
- var value;
-
- if (isString(require)) {
- var match = require.match(REQUIRE_PREFIX_REGEXP);
- var name = require.substring(match[0].length);
- var inheritType = match[1] || match[3];
- var optional = match[2] === '?';
-
- //If only parents then start at the parent element
- if (inheritType === '^^') {
- $element = $element.parent();
- //Otherwise attempt getting the controller from elementControllers in case
- //the element is transcluded (and has no data) and to avoid .data if possible
- } else {
- value = elementControllers && elementControllers[name];
- value = value && value.instance;
- }
-
- if (!value) {
- var dataName = '$' + name + 'Controller';
- value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
- }
-
- if (!value && !optional) {
- throw $compileMinErr('ctreq',
- "Controller '{0}', required by directive '{1}', can't be found!",
- name, directiveName);
- }
- } else if (isArray(require)) {
- value = [];
- for (var i = 0, ii = require.length; i < ii; i++) {
- value[i] = getControllers(directiveName, require[i], $element, elementControllers);
- }
- }
-
- return value || null;
- }
-
- function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
- var elementControllers = createMap();
- for (var controllerKey in controllerDirectives) {
- var directive = controllerDirectives[controllerKey];
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- };
-
- var controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
-
- // For directives with element transclusion the element is a comment,
- // but jQuery .data doesn't support attaching data to comment nodes as it's hard to
- // clean up (http://bugs.jquery.com/ticket/8335).
- // Instead, we save the controllers for the element in a local hash and attach to .data
- // later, once we have the actual element.
- elementControllers[directive.name] = controllerInstance;
- if (!hasElementTranscludeDirective) {
- $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
- }
- }
- return elementControllers;
- }
-
- function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn,
- thisLinkFn) {
- var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
- attrs;
+ function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
+ var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
+ attrs, removeScopeBindingWatches, removeControllerBindingWatches;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -8062,8 +8914,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrs = new Attributes($element, templateAttrs);
}
+ controllerScope = scope;
if (newIsolateScopeDirective) {
isolateScope = scope.$new(true);
+ } else if (newScopeDirective) {
+ controllerScope = scope.$parent;
}
if (boundTranscludeFn) {
@@ -8071,10 +8926,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
transcludeFn = controllersBoundTransclude;
transcludeFn.$$boundTransclude = boundTranscludeFn;
+ // expose the slots on the `$transclude` function
+ transcludeFn.isSlotFilled = function(slotName) {
+ return !!boundTranscludeFn.$$slots[slotName];
+ };
}
if (controllerDirectives) {
- elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
+ elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective);
}
if (newIsolateScopeDirective) {
@@ -8084,45 +8943,58 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
- initializeDirectiveBindings(scope, attrs, isolateScope,
- isolateScope.$$isolateBindings,
- newIsolateScopeDirective, isolateScope);
- }
- if (elementControllers) {
- // Initialize bindToController bindings for new/isolate scopes
- var scopeDirective = newIsolateScopeDirective || newScopeDirective;
- var bindings;
- var controllerForBindings;
- if (scopeDirective && elementControllers[scopeDirective.name]) {
- bindings = scopeDirective.$$bindings.bindToController;
- controller = elementControllers[scopeDirective.name];
-
- if (controller && controller.identifier && bindings) {
- controllerForBindings = controller;
- thisLinkFn.$$destroyBindings =
- initializeDirectiveBindings(scope, attrs, controller.instance,
- bindings, scopeDirective);
- }
+ removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
+ isolateScope.$$isolateBindings,
+ newIsolateScopeDirective);
+ if (removeScopeBindingWatches) {
+ isolateScope.$on('$destroy', removeScopeBindingWatches);
}
- for (i in elementControllers) {
- controller = elementControllers[i];
- var controllerResult = controller();
-
- if (controllerResult !== controller.instance) {
- // If the controller constructor has a return value, overwrite the instance
- // from setupControllers and update the element data
- controller.instance = controllerResult;
- $element.data('$' + i + 'Controller', controllerResult);
- if (controller === controllerForBindings) {
- // Remove and re-install bindToController bindings
- thisLinkFn.$$destroyBindings();
- thisLinkFn.$$destroyBindings =
- initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective);
- }
- }
+ }
+
+ // Initialize bindToController bindings
+ for (var name in elementControllers) {
+ var controllerDirective = controllerDirectives[name];
+ var controller = elementControllers[name];
+ var bindings = controllerDirective.$$bindings.bindToController;
+
+ if (controller.identifier && bindings) {
+ removeControllerBindingWatches =
+ initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ }
+
+ var controllerResult = controller();
+ if (controllerResult !== controller.instance) {
+ // If the controller constructor has a return value, overwrite the instance
+ // from setupControllers
+ controller.instance = controllerResult;
+ $element.data('$' + controllerDirective.name + 'Controller', controllerResult);
+ removeControllerBindingWatches && removeControllerBindingWatches();
+ removeControllerBindingWatches =
+ initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
}
+ // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
+ forEach(controllerDirectives, function(controllerDirective, name) {
+ var require = controllerDirective.require;
+ if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
+ extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
+ }
+ });
+
+ // Handle the init and destroy lifecycle hooks on all controllers that have them
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$onInit)) {
+ controllerInstance.$onInit();
+ }
+ if (isFunction(controllerInstance.$onDestroy)) {
+ controllerScope.$on('$destroy', function callOnDestroyHook() {
+ controllerInstance.$onDestroy();
+ });
+ }
+ });
+
// PRELINKING
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[i];
@@ -8156,13 +9028,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
);
}
+ // Trigger $postLink lifecycle hooks
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$postLink)) {
+ controllerInstance.$postLink();
+ }
+ });
+
// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
- function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement) {
+ function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
var transcludeControllers;
-
// No scope passed in:
if (!isScope(scope)) {
+ slotName = futureParentElement;
futureParentElement = cloneAttachFn;
cloneAttachFn = scope;
scope = undefined;
@@ -8174,15 +9054,108 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ if (slotName) {
+ // slotTranscludeFn can be one of three things:
+ // * a transclude function - a filled slot
+ // * `null` - an optional slot that was not filled
+ // * `undefined` - a slot that was not declared (i.e. invalid)
+ var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
+ if (slotTranscludeFn) {
+ return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ } else if (isUndefined(slotTranscludeFn)) {
+ throw $compileMinErr('noslot',
+ 'No parent directive that requires a transclusion with slot name "{0}". ' +
+ 'Element: {1}',
+ slotName, startingTag($element));
+ }
+ } else {
+ return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ }
+ }
+ }
+ }
+
+ function getControllers(directiveName, require, $element, elementControllers) {
+ var value;
+
+ if (isString(require)) {
+ var match = require.match(REQUIRE_PREFIX_REGEXP);
+ var name = require.substring(match[0].length);
+ var inheritType = match[1] || match[3];
+ var optional = match[2] === '?';
+
+ //If only parents then start at the parent element
+ if (inheritType === '^^') {
+ $element = $element.parent();
+ //Otherwise attempt getting the controller from elementControllers in case
+ //the element is transcluded (and has no data) and to avoid .data if possible
+ } else {
+ value = elementControllers && elementControllers[name];
+ value = value && value.instance;
}
+
+ if (!value) {
+ var dataName = '$' + name + 'Controller';
+ value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
+ }
+
+ if (!value && !optional) {
+ throw $compileMinErr('ctreq',
+ "Controller '{0}', required by directive '{1}', can't be found!",
+ name, directiveName);
+ }
+ } else if (isArray(require)) {
+ value = [];
+ for (var i = 0, ii = require.length; i < ii; i++) {
+ value[i] = getControllers(directiveName, require[i], $element, elementControllers);
+ }
+ } else if (isObject(require)) {
+ value = {};
+ forEach(require, function(controller, property) {
+ value[property] = getControllers(directiveName, controller, $element, elementControllers);
+ });
}
+
+ return value || null;
}
- function markDirectivesAsIsolate(directives) {
- // mark all directives as needing isolate scope.
+ function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) {
+ var elementControllers = createMap();
+ for (var controllerKey in controllerDirectives) {
+ var directive = controllerDirectives[controllerKey];
+ var locals = {
+ $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
+ $element: $element,
+ $attrs: attrs,
+ $transclude: transcludeFn
+ };
+
+ var controller = directive.controller;
+ if (controller == '@') {
+ controller = attrs[directive.name];
+ }
+
+ var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
+
+ // For directives with element transclusion the element is a comment.
+ // In this case .data will not attach any data.
+ // Instead, we save the controllers for the element in a local hash and attach to .data
+ // later, once we have the actual element.
+ elementControllers[directive.name] = controllerInstance;
+ $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
+ }
+ return elementControllers;
+ }
+
+ // Depending upon the context in which a directive finds itself it might need to have a new isolated
+ // or child scope created. For instance:
+ // * if the directive has been pulled into a template because another directive with a higher priority
+ // asked for element transclusion
+ // * if the directive itself asks for transclusion but it is at the root of a template and the original
+ // element was replaced. See https://github.com/angular/angular.js/issues/12936
+ function markDirectiveScope(directives, isolateScope, newScope) {
for (var j = 0, jj = directives.length; j < jj; j++) {
- directives[j] = inherit(directives[j], {$$isolateScope: true});
+ directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
}
}
@@ -8209,11 +9182,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
i = 0, ii = directives.length; i < ii; i++) {
try {
directive = directives[i];
- if ((maxPriority === undefined || maxPriority > directive.priority) &&
+ if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
directive.restrict.indexOf(location) != -1) {
if (startAttrName) {
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
}
+ if (!directive.$$bindings) {
+ var bindings = directive.$$bindings =
+ parseDirectiveBindings(directive, directive.name);
+ if (isObject(bindings.isolateScope)) {
+ directive.$$isolateBindings = bindings.isolateScope;
+ }
+ }
tDirectives.push(directive);
match = directive;
}
@@ -8329,7 +9309,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
if (isObject(origAsyncDirective.scope)) {
- markDirectivesAsIsolate(templateDirectives);
+ // the original directive that caused the template to be loaded async required
+ // an isolate scope
+ markDirectiveScope(templateDirectives, true);
}
directives = templateDirectives.concat(directives);
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
@@ -8378,7 +9360,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = boundTranscludeFn;
}
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
- childBoundTranscludeFn, afterTemplateNodeLinkFn);
+ childBoundTranscludeFn);
}
linkQueue = null;
});
@@ -8395,8 +9377,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
}
- afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn,
- afterTemplateNodeLinkFn);
+ afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
}
};
}
@@ -8505,7 +9486,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile: function() {
return {
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
- var $$observers = (attr.$$observers || (attr.$$observers = {}));
+ var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
@@ -8600,41 +9581,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parent.replaceChild(newNode, firstElementToRemove);
}
- // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it?
+ // Append all the `elementsToRemove` to a fragment. This will...
+ // - remove them from the DOM
+ // - allow them to still be traversed with .nextSibling
+ // - allow a single fragment.qSA to fetch all elements being removed
var fragment = document.createDocumentFragment();
- fragment.appendChild(firstElementToRemove);
+ for (i = 0; i < removeCount; i++) {
+ fragment.appendChild(elementsToRemove[i]);
+ }
if (jqLite.hasData(firstElementToRemove)) {
// Copy over user data (that includes Angular's $scope etc.). Don't copy private
// data here because there's no public interface in jQuery to do that and copying over
// event listeners (which is the main use of private data) wouldn't work anyway.
- jqLite(newNode).data(jqLite(firstElementToRemove).data());
+ jqLite.data(newNode, jqLite.data(firstElementToRemove));
- // Remove data of the replaced element. We cannot just call .remove()
- // on the element it since that would deallocate scope that is needed
- // for the new node. Instead, remove the data "manually".
- if (!jQuery) {
- delete jqLite.cache[firstElementToRemove[jqLite.expando]];
- } else {
- // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after
- // the replaced element. The cleanData version monkey-patched by Angular would cause
- // the scope to be trashed and we do need the very same scope to work with the new
- // element. However, we cannot just cache the non-patched version and use it here as
- // that would break if another library patches the method after Angular does (one
- // example is jQuery UI). Instead, set a flag indicating scope destroying should be
- // skipped this one time.
- skipDestroyOnNextJQueryCleanData = true;
- jQuery.cleanData([firstElementToRemove]);
- }
+ // Remove $destroy event listeners from `firstElementToRemove`
+ jqLite(firstElementToRemove).off('$destroy');
}
- for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
- var element = elementsToRemove[k];
- jqLite(element).remove(); // must do this way to clean up expando
- fragment.appendChild(element);
- delete elementsToRemove[k];
- }
+ // Cleanup any data/listeners on the elements and children.
+ // This includes invoking the $destroy event on any elements with listeners.
+ jqLite.cleanData(fragment.querySelectorAll('*'));
+ // Update the jqLite collection to only contain the `newNode`
+ for (i = 1; i < removeCount; i++) {
+ delete elementsToRemove[i];
+ }
elementsToRemove[0] = newNode;
elementsToRemove.length = 1;
}
@@ -8656,57 +9629,61 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// Set up $watches for isolate scope and controller bindings. This process
// only occurs for isolate scopes and new scopes with controllerAs.
- function initializeDirectiveBindings(scope, attrs, destination, bindings,
- directive, newScope) {
- var onNewScopeDestroyed;
- forEach(bindings, function(definition, scopeName) {
+ function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
+ var removeWatchCollection = [];
+ var changes;
+ forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
mode = definition.mode, // @, =, or &
lastValue,
- parentGet, parentSet, compare;
-
- if (!hasOwnProperty.call(attrs, attrName)) {
- // In the case of user defined a binding with the same name as a method in Object.prototype but didn't set
- // the corresponding attribute. We need to make sure subsequent code won't access to the prototype function
- attrs[attrName] = undefined;
- }
+ parentGet, parentSet, compare, removeWatch;
switch (mode) {
case '@':
- if (!attrs[attrName] && !optional) {
- destination[scopeName] = undefined;
+ if (!optional && !hasOwnProperty.call(attrs, attrName)) {
+ destination[scopeName] = attrs[attrName] = void 0;
}
-
attrs.$observe(attrName, function(value) {
- destination[scopeName] = value;
+ if (isString(value)) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, value, oldValue);
+ destination[scopeName] = value;
+ }
});
attrs.$$observers[attrName].$$scope = scope;
- if (attrs[attrName]) {
+ lastValue = attrs[attrName];
+ if (isString(lastValue)) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
- destination[scopeName] = $interpolate(attrs[attrName])(scope);
+ destination[scopeName] = $interpolate(lastValue)(scope);
+ } else if (isBoolean(lastValue)) {
+ // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted
+ // the value to boolean rather than a string, so we special case this situation
+ destination[scopeName] = lastValue;
}
break;
case '=':
- if (optional && !attrs[attrName]) {
- return;
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
}
- parentGet = $parse(attrs[attrName]);
+ if (optional && !attrs[attrName]) break;
+ parentGet = $parse(attrs[attrName]);
if (parentGet.literal) {
compare = equals;
} else {
- compare = function(a, b) { return a === b || (a !== a && b !== b); };
+ compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); };
}
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
lastValue = destination[scopeName] = parentGet(scope);
throw $compileMinErr('nonassign',
- "Expression '{0}' used with directive '{1}' is non-assignable!",
- attrs[attrName], directive.name);
+ "Expression '{0}' in attribute '{1}' used with directive '{2}' is non-assignable!",
+ attrs[attrName], attrName, directive.name);
};
lastValue = destination[scopeName] = parentGet(scope);
var parentValueWatch = function parentValueWatch(parentValue) {
@@ -8723,19 +9700,38 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
- var unwatch;
if (definition.collection) {
- unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
+ removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
- unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
+ removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
}
- onNewScopeDestroyed = (onNewScopeDestroyed || []);
- onNewScopeDestroyed.push(unwatch);
+ removeWatchCollection.push(removeWatch);
break;
- case '&':
+ case '<':
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
+ }
+ if (optional && !attrs[attrName]) break;
+
parentGet = $parse(attrs[attrName]);
+ destination[scopeName] = parentGet(scope);
+
+ removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, newParentValue, oldValue);
+ destination[scopeName] = newParentValue;
+ }, parentGet.literal);
+
+ removeWatchCollection.push(removeWatch);
+ break;
+
+ case '&':
+ // Don't assign Object.prototype method to scope
+ parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
+
// Don't assign noop to destination if expression is not valid
if (parentGet === noop && optional) break;
@@ -8745,16 +9741,39 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
}
});
- var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
- for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
- onNewScopeDestroyed[i]();
+
+ function recordChanges(key, currentValue, previousValue) {
+ if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
+ // If we have not already scheduled the top level onChangesQueue handler then do so now
+ if (!onChangesQueue) {
+ scope.$$postDigest(flushOnChangesQueue);
+ onChangesQueue = [];
+ }
+ // If we have not already queued a trigger of onChanges for this controller then do so now
+ if (!changes) {
+ changes = {};
+ onChangesQueue.push(triggerOnChangesHook);
+ }
+ // If the has been a change on this property already then we need to reuse the previous value
+ if (changes[key]) {
+ previousValue = changes[key].previousValue;
+ }
+ // Store this change
+ changes[key] = {previousValue: previousValue, currentValue: currentValue};
}
- } : noop;
- if (newScope && destroyBindings !== noop) {
- newScope.$on('$destroy', destroyBindings);
- return noop;
}
- return destroyBindings;
+
+ function triggerOnChangesHook() {
+ destination.$onChanges(changes);
+ // Now clear the changes so that we schedule onChanges when more changes arrive
+ changes = undefined;
+ }
+
+ return removeWatchCollection.length && function removeWatches() {
+ for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
+ removeWatchCollection[i]();
+ }
+ };
}
}];
}
@@ -8864,7 +9883,7 @@ function removeComments(jqNodes) {
var $controllerMinErr = minErr('$controller');
-var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
+var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
function identifierForController(controller, ident) {
if (ident && isString(ident)) return ident;
if (isString(controller)) {
@@ -8890,6 +9909,15 @@ function $ControllerProvider() {
/**
* @ngdoc method
+ * @name $controllerProvider#has
+ * @param {string} name Controller name to check.
+ */
+ this.has = function(name) {
+ return controllers.hasOwnProperty(name);
+ };
+
+ /**
+ * @ngdoc method
* @name $controllerProvider#register
* @param {string|Object} name Controller name, or an object map of controllers where the keys are
* the names and the values are the constructors.
@@ -8944,7 +9972,7 @@ function $ControllerProvider() {
* It's just a simple call to {@link auto.$injector $injector}, but extracted into
* a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
*/
- return function(expression, locals, later, ident) {
+ return function $controller(expression, locals, later, ident) {
// PRIVATE API:
// param `later` --- indicates that the controller's constructor is invoked at a later time.
// If true, $controller will allocate the object with the correct
@@ -8995,7 +10023,7 @@ function $ControllerProvider() {
}
var instantiate;
- return instantiate = extend(function() {
+ return instantiate = extend(function $controllerInit() {
var result = $injector.invoke(expression, instance, locals, constructor);
if (result !== instance && (isObject(result) || isFunction(result))) {
instance = result;
@@ -9111,6 +10139,29 @@ function $ExceptionHandlerProvider() {
}];
}
+var $$ForceReflowProvider = function() {
+ this.$get = ['$document', function($document) {
+ return function(domNode) {
+ //the line below will force the browser to perform a repaint so
+ //that all the animated elements within the animation frame will
+ //be properly updated and drawn on screen. This is required to
+ //ensure that the preparation animation is properly flushed so that
+ //the active state picks up from there. DO NOT REMOVE THIS LINE.
+ //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
+ //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
+ //WILL TAKE YEARS AWAY FROM YOUR LIFE.
+ if (domNode) {
+ if (!domNode.nodeType && domNode instanceof jqLite) {
+ domNode = domNode[0];
+ }
+ } else {
+ domNode = $document[0].body;
+ }
+ return domNode.offsetWidth + 1;
+ };
+ }];
+};
+
var APPLICATION_JSON = 'application/json';
var CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': APPLICATION_JSON + ';charset=utf-8'};
var JSON_START = /^\[|^\{(?!\{)/;
@@ -9119,6 +10170,12 @@ var JSON_ENDS = {
'{': /}$/
};
var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
+var $httpMinErr = minErr('$http');
+var $httpMinErrLegacyFn = function(method) {
+ return function() {
+ throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method);
+ };
+};
function serializeValue(v) {
if (isObject(v)) {
@@ -9152,7 +10209,7 @@ function $HttpParamSerializerProvider() {
forEachSorted(params, function(value, key) {
if (value === null || isUndefined(value)) return;
if (isArray(value)) {
- forEach(value, function(v, k) {
+ forEach(value, function(v) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
@@ -9219,8 +10276,8 @@ function $HttpParamSerializerJQLikeProvider() {
function serialize(toSerialize, prefix, topLevel) {
if (toSerialize === null || isUndefined(toSerialize)) return;
if (isArray(toSerialize)) {
- forEach(toSerialize, function(value) {
- serialize(value, prefix + '[]');
+ forEach(toSerialize, function(value, index) {
+ serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
});
} else if (isObject(toSerialize) && !isDate(toSerialize)) {
forEachSorted(toSerialize, function(value, key) {
@@ -9362,10 +10419,9 @@ function $HttpProvider() {
*
* Object containing default values for all {@link ng.$http $http} requests.
*
- * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`}
- * that will provide the cache for all requests who set their `cache` property to `true`.
- * If you set the `defaults.cache = false` then only requests that specify their own custom
- * cache object will be cached. See {@link $http#caching $http Caching} for more information.
+ * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with
+ * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses
+ * by default. See {@link $http#caching $http Caching} for more information.
*
* - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
* Defaults value is `'XSRF-TOKEN'`.
@@ -9441,6 +10497,30 @@ function $HttpProvider() {
return useApplyAsync;
};
+ var useLegacyPromise = true;
+ /**
+ * @ngdoc method
+ * @name $httpProvider#useLegacyPromiseExtensions
+ * @description
+ *
+ * Configure `$http` service to return promises without the shorthand methods `success` and `error`.
+ * This should be used to make sure that applications work without these methods.
+ *
+ * Defaults to true. If no value is specified, returns the current configured value.
+ *
+ * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
+ *
+ * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
+ * otherwise, returns the current configured value.
+ **/
+ this.useLegacyPromiseExtensions = function(value) {
+ if (isDefined(value)) {
+ useLegacyPromise = !!value;
+ return this;
+ }
+ return useLegacyPromise;
+ };
+
/**
* @ngdoc property
* @name $httpProvider#interceptors
@@ -9506,66 +10586,47 @@ function $HttpProvider() {
*
*
* ## General usage
- * The `$http` service is a function which takes a single argument — a configuration object —
- * that is used to generate an HTTP request and returns a {@link ng.$q promise}
- * with two $http specific methods: `success` and `error`.
+ * The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object} —
+ * that is used to generate an HTTP request and returns a {@link ng.$q promise}.
*
* ```js
- * // Simple GET request example :
- * $http.get('/someUrl').
- * success(function(data, status, headers, config) {
+ * // Simple GET request example:
+ * $http({
+ * method: 'GET',
+ * url: '/someUrl'
+ * }).then(function successCallback(response) {
* // this callback will be called asynchronously
* // when the response is available
- * }).
- * error(function(data, status, headers, config) {
+ * }, function errorCallback(response) {
* // called asynchronously if an error occurs
* // or server returns response with an error status.
* });
* ```
*
- * ```js
- * // Simple POST request example (passing data) :
- * $http.post('/someUrl', {msg:'hello word!'}).
- * success(function(data, status, headers, config) {
- * // this callback will be called asynchronously
- * // when the response is available
- * }).
- * error(function(data, status, headers, config) {
- * // called asynchronously if an error occurs
- * // or server returns response with an error status.
- * });
- * ```
+ * The response object has these properties:
*
- *
- * Since the returned value of calling the $http function is a `promise`, you can also use
- * the `then` method to register callbacks, and these callbacks will receive a single argument –
- * an object representing the response. See the API signature and type info below for more
- * details.
+ * - **data** – `{string|Object}` – The response body transformed with the transform
+ * functions.
+ * - **status** – `{number}` – HTTP status code of the response.
+ * - **headers** – `{function([headerName])}` – Header getter function.
+ * - **config** – `{Object}` – The configuration object that was used to generate the request.
+ * - **statusText** – `{string}` – HTTP status text of the response.
*
* A response status code between 200 and 299 is considered a success status and
* will result in the success callback being called. Note that if the response is a redirect,
* XMLHttpRequest will transparently follow it, meaning that the error callback will not be
* called for such responses.
*
- * ## Writing Unit Tests that use $http
- * When unit testing (using {@link ngMock ngMock}), it is necessary to call
- * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
- * request using trained responses.
- *
- * ```
- * $httpBackend.expectGET(...);
- * $http.get(...);
- * $httpBackend.flush();
- * ```
*
* ## Shortcut methods
*
* Shortcut methods are also available. All shortcut methods require passing in the URL, and
- * request data must be passed in for POST/PUT requests.
+ * request data must be passed in for POST/PUT requests. An optional config can be passed as the
+ * last argument.
*
* ```js
- * $http.get('/someUrl').success(successCallback);
- * $http.post('/someUrl', data).success(successCallback);
+ * $http.get('/someUrl', config).then(successCallback, errorCallback);
+ * $http.post('/someUrl', data, config).then(successCallback, errorCallback);
* ```
*
* Complete list of shortcut methods:
@@ -9579,6 +10640,25 @@ function $HttpProvider() {
* - {@link ng.$http#patch $http.patch}
*
*
+ * ## Writing Unit Tests that use $http
+ * When unit testing (using {@link ngMock ngMock}), it is necessary to call
+ * {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
+ * request using trained responses.
+ *
+ * ```
+ * $httpBackend.expectGET(...);
+ * $http.get(...);
+ * $httpBackend.flush();
+ * ```
+ *
+ * ## Deprecation Notice
+ * <div class="alert alert-danger">
+ * The `$http` legacy promise methods `success` and `error` have been deprecated.
+ * Use the standard `then` method instead.
+ * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to
+ * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error.
+ * </div>
+ *
* ## Setting HTTP Headers
*
* The $http service will automatically add certain HTTP headers to all requests. These defaults
@@ -9602,7 +10682,7 @@ function $HttpProvider() {
*
* ```
* module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'
+ * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
* });
* ```
*
@@ -9622,7 +10702,7 @@ function $HttpProvider() {
* data: { test: 'test' }
* }
*
- * $http(req).success(function(){...}).error(function(){...});
+ * $http(req).then(function(){...}, function(){...});
* ```
*
* ## Transforming Requests and Responses
@@ -9632,6 +10712,15 @@ function $HttpProvider() {
* the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions,
* which allows you to `push` or `unshift` a new transformation function into the transformation chain.
*
+ * <div class="alert alert-warning">
+ * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline.
+ * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
+ * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
+ * function will be reflected on the scope and in any templates where the object is data-bound.
+ * To prevent his, transform functions should have no side-effects.
+ * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
+ * </div>
+ *
* ### Default Transformations
*
* The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and
@@ -9689,26 +10778,35 @@ function $HttpProvider() {
*
* ## Caching
*
- * To enable caching, set the request configuration `cache` property to `true` (to use default
- * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}).
- * When the cache is enabled, `$http` stores the response from the server in the specified
- * cache. The next time the same request is made, the response is served from the cache without
- * sending a request to the server.
+ * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must
+ * set the config.cache value or the default cache value to TRUE or to a cache object (created
+ * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes
+ * precedence over the default cache value.
+ *
+ * In order to:
+ * * cache all responses - set the default cache value to TRUE or to a cache object
+ * * cache a specific response - set config.cache value to TRUE or to a cache object
+ *
+ * If caching is enabled, but neither the default cache nor config.cache are set to a cache object,
+ * then the default `$cacheFactory($http)` object is used.
+ *
+ * The default cache value can be set by updating the
+ * {@link ng.$http#defaults `$http.defaults.cache`} property or the
+ * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property.
*
- * Note that even if the response is served from cache, delivery of the data is asynchronous in
- * the same way that real requests are.
+ * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using
+ * the relevant cache object. The next time the same request is made, the response is returned
+ * from the cache without sending a request to the server.
*
- * If there are multiple GET requests for the same URL that should be cached using the same
- * cache, but the cache is not populated yet, only one request to the server will be made and
- * the remaining requests will be fulfilled using the response from the first request.
+ * Take note that:
*
- * You can change the default cache to a new object (built with
- * {@link ng.$cacheFactory `$cacheFactory`}) by updating the
- * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set
- * their `cache` property to `true` will now use this cache object.
+ * * Only GET and JSONP requests are cached.
+ * * The cache key is the request URL including search parameters; headers are not considered.
+ * * Cached responses are returned asynchronously, in the same way as responses from the server.
+ * * If multiple identical requests are made using the same cache, which is not yet populated,
+ * one request will be made to the server and remaining requests will return the same response.
+ * * A cache-control header on the response does not affect if or how responses are cached.
*
- * If you set the default cache to `false` then only requests that specify their own custom
- * cache object will be cached.
*
* ## Interceptors
*
@@ -9728,7 +10826,7 @@ function $HttpProvider() {
*
* There are two kinds of interceptors (and two kinds of rejection interceptors):
*
- * * `request`: interceptors get called with a http `config` object. The function is free to
+ * * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to
* modify the `config` object or create a new one. The function needs to return the `config`
* object directly, or a promise containing the `config` or a new `config` object.
* * `requestError`: interceptor gets called when a previous interceptor threw an error or
@@ -9830,13 +10928,13 @@ function $HttpProvider() {
*
* ### Cross Site Request Forgery (XSRF) Protection
*
- * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which
- * an unauthorized site can gain your user's private data. Angular provides a mechanism
- * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie
- * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only
- * JavaScript that runs on your domain could read the cookie, your server can be assured that
- * the XHR came from JavaScript running on your domain. The header will not be set for
- * cross-domain requests.
+ * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by
+ * which the attacker can trick an authenticated user into unknowingly executing actions on your
+ * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
+ * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
+ * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
+ * cookie, your server can be assured that the XHR came from JavaScript running on your domain.
+ * The header will not be set for cross-domain requests.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
@@ -9854,7 +10952,6 @@ function $HttpProvider() {
* In order to prevent collisions in environments where multiple Angular apps share the
* same domain or subdomain, we recommend that each application uses unique cookie name.
*
- *
* @param {object} config Object describing the request to be made and how it should be
* processed. The object has following properties:
*
@@ -9879,7 +10976,7 @@ function $HttpProvider() {
* transform function or an array of such functions. The transform function takes the http
* response body, headers and status and returns its transformed (typically deserialized) version.
* See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default TransformationjqLiks}
+ * Overriding the Default Transformations}
* - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to
* prepare the string representation of request parameters (specified as an object).
* If specified as string, it is interpreted as function registered with the
@@ -9887,10 +10984,9 @@ function $HttpProvider() {
* by registering it as a {@link auto.$provide#service service}.
* The default serializer is the {@link $httpParamSerializer $httpParamSerializer};
* alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike}
- * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
- * GET request, otherwise if a cache instance built with
- * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
- * caching.
+ * - **cache** – `{boolean|Object}` – A boolean value or object created with
+ * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
+ * See {@link $http#caching $http Caching} for more information.
* - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
* that should abort the request when resolved.
* - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the
@@ -9899,20 +10995,9 @@ function $HttpProvider() {
* - **responseType** - `{string}` - see
* [XMLHttpRequest.responseType](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#xmlhttprequest-responsetype).
*
- * @returns {HttpPromise} Returns a {@link ng.$q promise} object with the
- * standard `then` method and two http specific methods: `success` and `error`. The `then`
- * method takes two arguments a success and an error callback which will be called with a
- * response object. The `success` and `error` methods take a single argument - a function that
- * will be called when the request succeeds or fails respectively. The arguments passed into
- * these functions are destructured representation of the response object passed into the
- * `then` method. The response object has these properties:
+ * @returns {HttpPromise} Returns a {@link ng.$q `Promise}` that will be resolved to a response object
+ * when the request succeeds or fails.
*
- * - **data** – `{string|Object}` – The response body transformed with the transform
- * functions.
- * - **status** – `{number}` – HTTP status code of the response.
- * - **headers** – `{function([headerName])}` – Header getter function.
- * - **config** – `{Object}` – The configuration object that was used to generate the request.
- * - **statusText** – `{string}` – HTTP status text of the response.
*
* @property {Array.<Object>} pendingRequests Array of config objects for currently pending
* requests. This is primarily meant to be used for debugging purposes.
@@ -9954,13 +11039,12 @@ function $HttpProvider() {
$scope.response = null;
$http({method: $scope.method, url: $scope.url, cache: $templateCache}).
- success(function(data, status) {
- $scope.status = status;
- $scope.data = data;
- }).
- error(function(data, status) {
- $scope.data = data || "Request failed";
- $scope.status = status;
+ then(function(response) {
+ $scope.status = response.status;
+ $scope.data = response.data;
+ }, function(response) {
+ $scope.data = response.data || "Request failed";
+ $scope.status = response.status;
});
};
@@ -10008,10 +11092,14 @@ function $HttpProvider() {
*/
function $http(requestConfig) {
- if (!angular.isObject(requestConfig)) {
+ if (!isObject(requestConfig)) {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}
+ if (!isString(requestConfig.url)) {
+ throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
+ }
+
var config = extend({
method: 'get',
transformRequest: defaults.transformRequest,
@@ -10065,34 +11153,36 @@ function $HttpProvider() {
promise = promise.then(thenFn, rejectFn);
}
- promise.success = function(fn) {
- assertArgFn(fn, 'fn');
+ if (useLegacyPromise) {
+ promise.success = function(fn) {
+ assertArgFn(fn, 'fn');
- promise.then(function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
+ promise.then(function(response) {
+ fn(response.data, response.status, response.headers, config);
+ });
+ return promise;
+ };
- promise.error = function(fn) {
- assertArgFn(fn, 'fn');
+ promise.error = function(fn) {
+ assertArgFn(fn, 'fn');
- promise.then(null, function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
+ promise.then(null, function(response) {
+ fn(response.data, response.status, response.headers, config);
+ });
+ return promise;
+ };
+ } else {
+ promise.success = $httpMinErrLegacyFn('success');
+ promise.error = $httpMinErrLegacyFn('error');
+ }
return promise;
function transformResponse(response) {
// make a copy since the response must be cacheable
var resp = extend({}, response);
- if (!response.data) {
- resp.data = response.data;
- } else {
- resp.data = transformData(response.data, response.headers, response.status, config.transformResponse);
- }
+ resp.data = transformData(response.data, response.headers, response.status,
+ config.transformResponse);
return (isSuccess(response.status))
? resp
: $q.reject(resp);
@@ -10122,7 +11212,7 @@ function $HttpProvider() {
defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]);
- // using for-in instead of forEach to avoid unecessary iteration after header has been found
+ // using for-in instead of forEach to avoid unnecessary iteration after header has been found
defaultHeadersIteration:
for (defHeaderName in defHeaders) {
lowercaseDefHeaderName = lowercase(defHeaderName);
@@ -10370,8 +11460,8 @@ function $HttpProvider() {
* Resolves the raw $http promise.
*/
function resolvePromise(response, status, headers, statusText) {
- // normalize internal statuses to 0
- status = Math.max(status, 0);
+ //status: HTTP response status code, 0, -1 (aborted by timeout / promise)
+ status = status >= -1 ? status : 0;
(isSuccess(status) ? deferred.resolve : deferred.reject)({
data: response,
@@ -10402,8 +11492,33 @@ function $HttpProvider() {
}];
}
-function createXhr() {
- return new window.XMLHttpRequest();
+/**
+ * @ngdoc service
+ * @name $xhrFactory
+ *
+ * @description
+ * Factory function used to create XMLHttpRequest objects.
+ *
+ * Replace or decorate this service to create your own custom XMLHttpRequest objects.
+ *
+ * ```
+ * angular.module('myApp', [])
+ * .factory('$xhrFactory', function() {
+ * return function createXhr(method, url) {
+ * return new window.XMLHttpRequest({mozSystem: true});
+ * };
+ * });
+ * ```
+ *
+ * @param {string} method HTTP method of the request (GET, POST, PUT, ..)
+ * @param {string} url URL of the request.
+ */
+function $xhrFactoryProvider() {
+ this.$get = function() {
+ return function createXhr() {
+ return new window.XMLHttpRequest();
+ };
+ };
}
/**
@@ -10411,6 +11526,7 @@ function createXhr() {
* @name $httpBackend
* @requires $window
* @requires $document
+ * @requires $xhrFactory
*
* @description
* HTTP backend used by the {@link ng.$http service} that delegates to
@@ -10423,8 +11539,8 @@ function createXhr() {
* $httpBackend} which can be trained with responses.
*/
function $HttpBackendProvider() {
- this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) {
- return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]);
+ this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) {
+ return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]);
}];
}
@@ -10448,7 +11564,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
});
} else {
- var xhr = createXhr();
+ var xhr = createXhr(method, url);
xhr.open(method, url, true);
forEach(headers, function(value, key) {
@@ -10460,7 +11576,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
xhr.onload = function requestLoaded() {
var statusText = xhr.statusText || '';
- // responseText is the old-school way of retrieving response (supported by IE8 & 9)
+ // responseText is the old-school way of retrieving response (supported by IE9)
// response/responseType properties were introduced in XHR Level2 spec (supported by IE10)
var response = ('response' in xhr) ? xhr.response : xhr.responseText;
@@ -10511,7 +11627,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
}
- xhr.send(post);
+ xhr.send(isUndefined(post) ? null : post);
}
if (timeout > 0) {
@@ -10528,7 +11644,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
function completeRequest(callback, status, response, headersString, statusText) {
// cancel timeout and subsequent timeout promise resolution
- if (timeoutId !== undefined) {
+ if (isDefined(timeoutId)) {
$browserDefer.cancel(timeoutId);
}
jsonpDone = xhr = null;
@@ -10595,8 +11711,16 @@ $interpolateMinErr.interr = function(text, err) {
*
* Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
*
+ * <div class="alert alert-danger">
+ * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular
+ * template within a Python Jinja template (or any other template language). Mixing templating
+ * languages is **very dangerous**. The embedding template language will not safely escape Angular
+ * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS)
+ * security bugs!
+ * </div>
+ *
* @example
-<example module="customInterpolationApp">
+<example name="custom-interpolation-markup" module="customInterpolationApp">
<file name="index.html">
<script>
var customInterpolationApp = angular.module('customInterpolationApp', []);
@@ -10611,7 +11735,7 @@ $interpolateMinErr.interr = function(text, err) {
this.label = "This binding is brought you by // interpolation symbols.";
});
</script>
-<div ng-app="App" ng-controller="DemoController as demo">
+<div ng-controller="DemoController as demo">
//demo.label//
</div>
</file>
@@ -10695,6 +11819,15 @@ function $InterpolateProvider() {
return value;
}
+ //TODO: this is the same as the constantWatchDelegate in parse.js
+ function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
+ var unwatch;
+ return unwatch = scope.$watch(function constantInterpolateWatch(scope) {
+ unwatch();
+ return constantInterp(scope);
+ }, listener, objectEquality);
+ }
+
/**
* @ngdoc service
* @name $interpolate
@@ -10714,7 +11847,7 @@ function $InterpolateProvider() {
* ```js
* var $interpolate = ...; // injected
* var exp = $interpolate('Hello {{name | uppercase}}!');
- * expect(exp({name:'Angular'}).toEqual('Hello ANGULAR!');
+ * expect(exp({name:'Angular'})).toEqual('Hello ANGULAR!');
* ```
*
* `$interpolate` takes an optional fourth argument, `allOrNothing`. If `allOrNothing` is
@@ -10790,6 +11923,19 @@ function $InterpolateProvider() {
* - `context`: evaluation context for all expressions embedded in the interpolated text
*/
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
+ // Provide a quick exit and simplified result function for text with no interpolation
+ if (!text.length || text.indexOf(startSymbol) === -1) {
+ var constantInterp;
+ if (!mustHaveExpression) {
+ var unescapedText = unescapeText(text);
+ constantInterp = valueFn(unescapedText);
+ constantInterp.exp = text;
+ constantInterp.expressions = [];
+ constantInterp.$$watchDelegate = constantWatchDelegate;
+ }
+ return constantInterp;
+ }
+
allOrNothing = !!allOrNothing;
var startIndex,
endIndex,
@@ -10926,8 +12072,8 @@ function $InterpolateProvider() {
}
function $IntervalProvider() {
- this.$get = ['$rootScope', '$window', '$q', '$$q',
- function($rootScope, $window, $q, $$q) {
+ this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
+ function($rootScope, $window, $q, $$q, $browser) {
var intervals = {};
@@ -11068,11 +12214,12 @@ function $IntervalProvider() {
count = isDefined(count) ? count : 0;
- promise.then(null, null, (!hasParams) ? fn : function() {
- fn.apply(null, args);
- });
-
promise.$$intervalId = setInterval(function tick() {
+ if (skipApply) {
+ $browser.defer(callback);
+ } else {
+ $rootScope.$evalAsync(callback);
+ }
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
@@ -11088,6 +12235,14 @@ function $IntervalProvider() {
intervals[promise.$$intervalId] = deferred;
return promise;
+
+ function callback() {
+ if (!hasParams) {
+ fn(iteration);
+ } else {
+ fn.apply(null, args);
+ }
+ }
}
@@ -11098,7 +12253,7 @@ function $IntervalProvider() {
* @description
* Cancels a task associated with the `promise`.
*
- * @param {promise} promise returned by the `$interval` function.
+ * @param {Promise=} promise returned by the `$interval` function.
* @returns {boolean} Returns `true` if the task was successfully canceled.
*/
interval.cancel = function(promise) {
@@ -11125,75 +12280,6 @@ function $IntervalProvider() {
*
* * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
*/
-function $LocaleProvider() {
- this.$get = function() {
- return {
- id: 'en-us',
-
- NUMBER_FORMATS: {
- DECIMAL_SEP: '.',
- GROUP_SEP: ',',
- PATTERNS: [
- { // Decimal Pattern
- minInt: 1,
- minFrac: 0,
- maxFrac: 3,
- posPre: '',
- posSuf: '',
- negPre: '-',
- negSuf: '',
- gSize: 3,
- lgSize: 3
- },{ //Currency Pattern
- minInt: 1,
- minFrac: 2,
- maxFrac: 2,
- posPre: '\u00A4',
- posSuf: '',
- negPre: '(\u00A4',
- negSuf: ')',
- gSize: 3,
- lgSize: 3
- }
- ],
- CURRENCY_SYM: '$'
- },
-
- DATETIME_FORMATS: {
- MONTH:
- 'January,February,March,April,May,June,July,August,September,October,November,December'
- .split(','),
- SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','),
- DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','),
- SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','),
- AMPMS: ['AM','PM'],
- medium: 'MMM d, y h:mm:ss a',
- 'short': 'M/d/yy h:mm a',
- fullDate: 'EEEE, MMMM d, y',
- longDate: 'MMMM d, y',
- mediumDate: 'MMM d, y',
- shortDate: 'M/d/yy',
- mediumTime: 'h:mm:ss a',
- shortTime: 'h:mm a',
- ERANAMES: [
- "Before Christ",
- "Anno Domini"
- ],
- ERAS: [
- "BC",
- "AD"
- ]
- },
-
- pluralCat: function(num) {
- if (num === 1) {
- return 'one';
- }
- return 'other';
- }
- };
- };
-}
var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
@@ -11284,12 +12370,12 @@ function serverBase(url) {
*
* @constructor
* @param {string} appBase application base URL
+ * @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} basePrefix url path prefix
*/
-function LocationHtml5Url(appBase, basePrefix) {
+function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
basePrefix = basePrefix || '';
- var appBaseNoFile = stripFile(appBase);
parseAbsoluteUrl(appBase, this);
@@ -11336,14 +12422,14 @@ function LocationHtml5Url(appBase, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
- if ((appUrl = beginsWith(appBase, url)) !== undefined) {
+ if (isDefined(appUrl = beginsWith(appBase, url))) {
prevAppUrl = appUrl;
- if ((appUrl = beginsWith(basePrefix, appUrl)) !== undefined) {
+ if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) {
rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
- } else if ((appUrl = beginsWith(appBaseNoFile, url)) !== undefined) {
+ } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
} else if (appBaseNoFile == url + '/') {
rewrittenUrl = appBaseNoFile;
@@ -11363,10 +12449,10 @@ function LocationHtml5Url(appBase, basePrefix) {
*
* @constructor
* @param {string} appBase application base URL
+ * @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} hashPrefix hashbang prefix
*/
-function LocationHashbangUrl(appBase, hashPrefix) {
- var appBaseNoFile = stripFile(appBase);
+function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
parseAbsoluteUrl(appBase, this);
@@ -11475,14 +12561,13 @@ function LocationHashbangUrl(appBase, hashPrefix) {
*
* @constructor
* @param {string} appBase application base URL
+ * @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} hashPrefix hashbang prefix
*/
-function LocationHashbangInHtml5Url(appBase, hashPrefix) {
+function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
this.$$html5 = true;
LocationHashbangUrl.apply(this, arguments);
- var appBaseNoFile = stripFile(appBase);
-
this.$$parseLinkUrl = function(url, relHref) {
if (relHref && relHref[0] === '#') {
// special case for links to hash fragments:
@@ -11512,7 +12597,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : '';
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
- // include hashPrefix in $$absUrl when $$url is empty so IE8 & 9 do not reload page because of removal of '#'
+ // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
this.$$absUrl = appBase + hashPrefix + this.$$url;
};
@@ -11770,9 +12855,9 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return hash fragment when called without any parameter.
+ * Returns the hash fragment when called without any parameters.
*
- * Change hash fragment when called with parameter and return `$location`.
+ * Changes the hash fragment when called with a parameter and returns `$location`.
*
*
* ```js
@@ -11793,8 +12878,8 @@ var locationPrototype = {
* @name $location#replace
*
* @description
- * If called, all changes to $location during current `$digest` will be replacing current history
- * record, instead of adding new one.
+ * If called, all changes to $location during the current `$digest` will replace the current history
+ * record, instead of adding a new one.
*/
replace: function() {
this.$$replace = true;
@@ -12021,7 +13106,9 @@ function $LocationProvider() {
appBase = stripHash(initialUrl);
LocationMode = LocationHashbangUrl;
}
- $location = new LocationMode(appBase, '#' + hashPrefix);
+ var appBaseNoFile = stripFile(appBase);
+
+ $location = new LocationMode(appBase, appBaseNoFile, '#' + hashPrefix);
$location.$$parseLinkUrl(initialUrl, initialUrl);
$location.$$state = $browser.state();
@@ -12101,11 +13188,18 @@ function $LocationProvider() {
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
+
+ if (isUndefined(beginsWith(appBaseNoFile, newUrl))) {
+ // If we are navigating outside of the app then force a reload
+ $window.location.href = newUrl;
+ return;
+ }
+
$rootScope.$evalAsync(function() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
-
+ newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
@@ -12388,6 +13482,24 @@ function ensureSafeMemberName(name, fullExpression) {
return name;
}
+function getStringValue(name) {
+ // Property names must be strings. This means that non-string objects cannot be used
+ // as keys in an object. Any non-string object, including a number, is typecasted
+ // into a string via the toString method.
+ // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names
+ //
+ // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it
+ // to a string. It's not always possible. If `name` is an object and its `toString` method is
+ // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown:
+ //
+ // TypeError: Cannot convert object to primitive value
+ //
+ // For performance reasons, we don't catch this error here and allow it to propagate up the call
+ // stack. Note that you'll get the same error in JavaScript if you try to access a property using
+ // such a 'broken' object as a key.
+ return name + '';
+}
+
function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj) {
@@ -12433,6 +13545,16 @@ function ensureSafeFunction(obj, fullExpression) {
}
}
+function ensureSafeAssignContext(obj, fullExpression) {
+ if (obj) {
+ if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor ||
+ obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
+ throw $parseMinErr('isecaf',
+ 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression);
+ }
+ }
+}
+
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
@@ -12636,6 +13758,7 @@ AST.ArrayExpression = 'ArrayExpression';
AST.Property = 'Property';
AST.ObjectExpression = 'ObjectExpression';
AST.ThisExpression = 'ThisExpression';
+AST.LocalsExpression = 'LocalsExpression';
// Internal use only
AST.NGValueParameter = 'NGValueParameter';
@@ -12774,8 +13897,10 @@ AST.prototype = {
primary = this.arrayDeclaration();
} else if (this.expect('{')) {
primary = this.object();
- } else if (this.constants.hasOwnProperty(this.peek().text)) {
- primary = copy(this.constants[this.consume().text]);
+ } else if (this.selfReferential.hasOwnProperty(this.peek().text)) {
+ primary = copy(this.selfReferential[this.consume().text]);
+ } else if (this.options.literals.hasOwnProperty(this.peek().text)) {
+ primary = { type: AST.Literal, value: this.options.literals[this.consume().text]};
} else if (this.peek().identifier) {
primary = this.identifier();
} else if (this.peek().constant) {
@@ -12927,16 +14052,9 @@ AST.prototype = {
return false;
},
-
- /* `undefined` is not a constant, it is an identifier,
- * but using it as an identifier is not supported
- */
- constants: {
- 'true': { type: AST.Literal, value: true },
- 'false': { type: AST.Literal, value: false },
- 'null': { type: AST.Literal, value: null },
- 'undefined': {type: AST.Literal, value: undefined },
- 'this': {type: AST.ThisExpression }
+ selfReferential: {
+ 'this': {type: AST.ThisExpression },
+ '$locals': {type: AST.LocalsExpression }
}
};
@@ -13056,6 +14174,10 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.constant = false;
ast.toWatch = [];
break;
+ case AST.LocalsExpression:
+ ast.constant = false;
+ ast.toWatch = [];
+ break;
}
}
@@ -13114,6 +14236,7 @@ ASTCompiler.prototype = {
this.state.computing = 'assign';
var result = this.nextId();
this.recurse(assignable, result);
+ this.return_(result);
extra = 'fn.assign=' + this.generateFunction('assign', 's,v,l');
}
var toWatch = getInputs(ast.body);
@@ -13146,6 +14269,8 @@ ASTCompiler.prototype = {
'ensureSafeMemberName',
'ensureSafeObject',
'ensureSafeFunction',
+ 'getStringValue',
+ 'ensureSafeAssignContext',
'ifDefined',
'plus',
'text',
@@ -13154,6 +14279,8 @@ ASTCompiler.prototype = {
ensureSafeMemberName,
ensureSafeObject,
ensureSafeFunction,
+ getStringValue,
+ ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
@@ -13294,9 +14421,13 @@ ASTCompiler.prototype = {
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
+ if (create && create !== 1) {
+ self.addEnsureSafeAssignContext(left);
+ }
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
+ self.getStringValue(right);
self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
@@ -13374,12 +14505,13 @@ ASTCompiler.prototype = {
right = this.nextId();
left = {};
if (!isAssignable(ast.left)) {
- throw $parseMinErr('lval', 'Trying to assing a value to a non l-value');
+ throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
}
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
+ self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -13415,6 +14547,10 @@ ASTCompiler.prototype = {
this.assign(intoId, 's');
recursionFn('s');
break;
+ case AST.LocalsExpression:
+ this.assign(intoId, 'l');
+ recursionFn('l');
+ break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
recursionFn('v');
@@ -13505,6 +14641,10 @@ ASTCompiler.prototype = {
this.current().body.push(this.ensureSafeFunction(item), ';');
},
+ addEnsureSafeAssignContext: function(item) {
+ this.current().body.push(this.ensureSafeAssignContext(item), ';');
+ },
+
ensureSafeObject: function(item) {
return 'ensureSafeObject(' + item + ',text)';
},
@@ -13517,6 +14657,14 @@ ASTCompiler.prototype = {
return 'ensureSafeFunction(' + item + ',text)';
},
+ getStringValue: function(item) {
+ this.assign(item, 'getStringValue(' + item + ')');
+ },
+
+ ensureSafeAssignContext: function(item) {
+ return 'ensureSafeAssignContext(' + item + ',text)';
+ },
+
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
@@ -13594,7 +14742,7 @@ ASTInterpreter.prototype = {
forEach(ast.body, function(expression) {
expressions.push(self.recurse(expression.expression));
});
- var fn = ast.body.length === 0 ? function() {} :
+ var fn = ast.body.length === 0 ? noop :
ast.body.length === 1 ? expressions[0] :
function(scope, locals) {
var lastValue;
@@ -13694,6 +14842,7 @@ ASTInterpreter.prototype = {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
ensureSafeObject(lhs.value, self.expression);
+ ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -13729,8 +14878,12 @@ ASTInterpreter.prototype = {
return function(scope) {
return context ? {value: scope} : scope;
};
+ case AST.LocalsExpression:
+ return function(scope, locals) {
+ return context ? {value: locals} : locals;
+ };
case AST.NGValueParameter:
- return function(scope, locals, assign, inputs) {
+ return function(scope, locals, assign) {
return context ? {value: assign} : assign;
};
}
@@ -13891,9 +15044,13 @@ ASTInterpreter.prototype = {
var value;
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
+ rhs = getStringValue(rhs);
ensureSafeMemberName(rhs, expression);
- if (create && create !== 1 && lhs && !(lhs[rhs])) {
- lhs[rhs] = {};
+ if (create && create !== 1) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[rhs])) {
+ lhs[rhs] = {};
+ }
}
value = lhs[rhs];
ensureSafeObject(value, expression);
@@ -13908,8 +15065,11 @@ ASTInterpreter.prototype = {
nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
- if (create && create !== 1 && lhs && !(lhs[right])) {
- lhs[right] = {};
+ if (create && create !== 1) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[right])) {
+ lhs[right] = {};
+ }
}
var value = lhs != null ? lhs[right] : undefined;
if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
@@ -13937,7 +15097,7 @@ var Parser = function(lexer, $filter, options) {
this.lexer = lexer;
this.$filter = $filter;
this.options = options;
- this.ast = new AST(this.lexer);
+ this.ast = new AST(lexer, options);
this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
new ASTCompiler(this.ast, $filter);
};
@@ -13950,32 +15110,6 @@ Parser.prototype = {
}
};
-//////////////////////////////////////////////////
-// Parser helper functions
-//////////////////////////////////////////////////
-
-function setter(obj, path, setValue, fullExp) {
- ensureSafeObject(obj, fullExp);
-
- var element = path.split('.'), key;
- for (var i = 0; element.length > 1; i++) {
- key = ensureSafeMemberName(element.shift(), fullExp);
- var propertyObj = ensureSafeObject(obj[key], fullExp);
- if (!propertyObj) {
- propertyObj = {};
- obj[key] = propertyObj;
- }
- obj = propertyObj;
- }
- key = ensureSafeMemberName(element.shift(), fullExp);
- ensureSafeObject(obj[key], fullExp);
- obj[key] = setValue;
- return setValue;
-}
-
-var getterFnCacheDefault = createMap();
-var getterFnCacheExpensive = createMap();
-
function isPossiblyDangerousMemberName(name) {
return name == 'constructor';
}
@@ -14040,20 +15174,53 @@ function getValueOf(value) {
function $ParseProvider() {
var cacheDefault = createMap();
var cacheExpensive = createMap();
+ var literals = {
+ 'true': true,
+ 'false': false,
+ 'null': null,
+ 'undefined': undefined
+ };
+
+ /**
+ * @ngdoc method
+ * @name $parseProvider#addLiteral
+ * @description
+ *
+ * Configure $parse service to add literal values that will be present as literal at expressions.
+ *
+ * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name.
+ * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`.
+ *
+ **/
+ this.addLiteral = function(literalName, literalValue) {
+ literals[literalName] = literalValue;
+ };
- this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
+ this.$get = ['$filter', function($filter) {
+ var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
- csp: $sniffer.csp,
- expensiveChecks: false
+ csp: noUnsafeEval,
+ expensiveChecks: false,
+ literals: copy(literals)
},
$parseOptionsExpensive = {
- csp: $sniffer.csp,
- expensiveChecks: true
+ csp: noUnsafeEval,
+ expensiveChecks: true,
+ literals: copy(literals)
};
+ var runningChecksEnabled = false;
+
+ $parse.$$runningExpensiveChecks = function() {
+ return runningChecksEnabled;
+ };
- return function $parse(exp, interceptorFn, expensiveChecks) {
+ return $parse;
+
+ function $parse(exp, interceptorFn, expensiveChecks) {
var parsedExpression, oneTime, cacheKey;
+ expensiveChecks = expensiveChecks || runningChecksEnabled;
+
switch (typeof exp) {
case 'string':
exp = exp.trim();
@@ -14079,6 +15246,9 @@ function $ParseProvider() {
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
+ if (expensiveChecks) {
+ parsedExpression = expensiveChecksInterceptor(parsedExpression);
+ }
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -14087,9 +15257,33 @@ function $ParseProvider() {
return addInterceptor(exp, interceptorFn);
default:
- return noop;
+ return addInterceptor(noop, interceptorFn);
}
- };
+ }
+
+ function expensiveChecksInterceptor(fn) {
+ if (!fn) return fn;
+ expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
+ expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
+ expensiveCheckFn.constant = fn.constant;
+ expensiveCheckFn.literal = fn.literal;
+ for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
+ fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);
+ }
+ expensiveCheckFn.inputs = fn.inputs;
+
+ return expensiveCheckFn;
+
+ function expensiveCheckFn(scope, locals, assign, inputs) {
+ var expensiveCheckOldValue = runningChecksEnabled;
+ runningChecksEnabled = true;
+ try {
+ return fn(scope, locals, assign, inputs);
+ } finally {
+ runningChecksEnabled = expensiveCheckOldValue;
+ }
+ }
+ }
function expressionInputDirtyCheck(newValue, oldValueOfValue) {
@@ -14206,25 +15400,22 @@ function $ParseProvider() {
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch;
return unwatch = scope.$watch(function constantWatch(scope) {
- return parsedExpression(scope);
- }, function constantListener(value, old, scope) {
- if (isFunction(listener)) {
- listener.apply(this, arguments);
- }
unwatch();
- }, objectEquality);
+ return parsedExpression(scope);
+ }, listener, objectEquality);
}
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
var watchDelegate = parsedExpression.$$watchDelegate;
+ var useInputs = false;
var regularWatch =
watchDelegate !== oneTimeLiteralWatchDelegate &&
watchDelegate !== oneTimeWatchDelegate;
var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
- var value = parsedExpression(scope, locals, assign, inputs);
+ var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value, scope, locals);
} : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = parsedExpression(scope, locals, assign, inputs);
@@ -14242,6 +15433,7 @@ function $ParseProvider() {
// If there is an interceptor, but no watchDelegate then treat the interceptor like
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
fn.$$watchDelegate = inputsWatchDelegate;
+ useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
}
@@ -14263,15 +15455,15 @@ function $ParseProvider() {
* [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
- * implementations, and the other which resembles ES6 promises to some degree.
+ * implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
* # $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
- * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony,
+ * function as the first argument. This is similar to the native Promise implementation from ES6,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
- * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are
+ * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
* available yet.
*
* It can be used like so:
@@ -14303,6 +15495,8 @@ function $ParseProvider() {
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
+ * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise.
+ *
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
@@ -14434,7 +15628,7 @@ function $ParseProvider() {
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
- * # Testing
+ * # Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
@@ -14491,18 +15685,6 @@ function $$QProvider() {
*/
function qFactory(nextTick, exceptionHandler) {
var $qMinErr = minErr('$q', TypeError);
- function callOnce(self, resolveFn, rejectFn) {
- var called = false;
- function wrap(fn) {
- return function(value) {
- if (called) return;
- called = true;
- fn.call(self, value);
- };
- }
-
- return [wrap(resolveFn), wrap(rejectFn)];
- }
/**
* @ngdoc method
@@ -14515,15 +15697,23 @@ function qFactory(nextTick, exceptionHandler) {
* @returns {Deferred} Returns a new instance of deferred.
*/
var defer = function() {
- return new Deferred();
+ var d = new Deferred();
+ //Necessary to support unbound execution :/
+ d.resolve = simpleBind(d, d.resolve);
+ d.reject = simpleBind(d, d.reject);
+ d.notify = simpleBind(d, d.notify);
+ return d;
};
function Promise() {
this.$$state = { status: 0 };
}
- Promise.prototype = {
+ extend(Promise.prototype, {
then: function(onFulfilled, onRejected, progressBack) {
+ if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
+ return this;
+ }
var result = new Deferred();
this.$$state.pending = this.$$state.pending || [];
@@ -14544,7 +15734,7 @@ function qFactory(nextTick, exceptionHandler) {
return handleCallback(error, false, callback);
}, progressBack);
}
- };
+ });
//Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native
function simpleBind(context, fn) {
@@ -14585,13 +15775,9 @@ function qFactory(nextTick, exceptionHandler) {
function Deferred() {
this.promise = new Promise();
- //Necessary to support unbound execution :/
- this.resolve = simpleBind(this, this.resolve);
- this.reject = simpleBind(this, this.reject);
- this.notify = simpleBind(this, this.notify);
}
- Deferred.prototype = {
+ extend(Deferred.prototype, {
resolve: function(val) {
if (this.promise.$$state.status) return;
if (val === this.promise) {
@@ -14606,23 +15792,34 @@ function qFactory(nextTick, exceptionHandler) {
},
$$resolve: function(val) {
- var then, fns;
-
- fns = callOnce(this, this.$$resolve, this.$$reject);
+ var then;
+ var that = this;
+ var done = false;
try {
if ((isObject(val) || isFunction(val))) then = val && val.then;
if (isFunction(then)) {
this.promise.$$state.status = -1;
- then.call(val, fns[0], fns[1], this.notify);
+ then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify));
} else {
this.promise.$$state.value = val;
this.promise.$$state.status = 1;
scheduleProcessQueue(this.promise.$$state);
}
} catch (e) {
- fns[1](e);
+ rejectPromise(e);
exceptionHandler(e);
}
+
+ function resolvePromise(val) {
+ if (done) return;
+ done = true;
+ that.$$resolve(val);
+ }
+ function rejectPromise(val) {
+ if (done) return;
+ done = true;
+ that.$$reject(val);
+ }
},
reject: function(reason) {
@@ -14654,7 +15851,7 @@ function qFactory(nextTick, exceptionHandler) {
});
}
}
- };
+ });
/**
* @ngdoc method
@@ -14737,6 +15934,9 @@ function qFactory(nextTick, exceptionHandler) {
* the promise comes from a source that can't be trusted.
*
* @param {*} value Value or a promise
+ * @param {Function=} successCallback
+ * @param {Function=} errorCallback
+ * @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
@@ -14756,6 +15956,9 @@ function qFactory(nextTick, exceptionHandler) {
* Alias of {@link ng.$q#when when} to maintain naming consistency with ES6.
*
* @param {*} value Value or a promise
+ * @param {Function=} successCallback
+ * @param {Function=} errorCallback
+ * @param {Function=} progressCallback
* @returns {Promise} Returns a promise of the passed value or promise
*/
var resolve = when;
@@ -14805,11 +16008,6 @@ function qFactory(nextTick, exceptionHandler) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}
- if (!(this instanceof Q)) {
- // More useful when $Q is the Promise itself.
- return new Q(resolver);
- }
-
var deferred = new Deferred();
function resolveFn(value) {
@@ -14825,6 +16023,10 @@ function qFactory(nextTick, exceptionHandler) {
return deferred.promise;
};
+ // Let's make the instanceof operator work for promises, so that
+ // `new $q(fn) instanceof $q` would evaluate to true.
+ $Q.prototype = Promise.prototype;
+
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
@@ -14844,7 +16046,7 @@ function $$RAFProvider() { //rAF
$window.webkitCancelRequestAnimationFrame;
var rafSupported = !!requestAnimationFrame;
- var rafFn = rafSupported
+ var raf = rafSupported
? function(fn) {
var id = requestAnimationFrame(fn);
return function() {
@@ -14858,47 +16060,9 @@ function $$RAFProvider() { //rAF
};
};
- queueFn.supported = rafSupported;
-
- var cancelLastRAF;
- var taskCount = 0;
- var taskQueue = [];
- return queueFn;
+ raf.supported = rafSupported;
- function flush() {
- for (var i = 0; i < taskQueue.length; i++) {
- var task = taskQueue[i];
- if (task) {
- taskQueue[i] = null;
- task();
- }
- }
- taskCount = taskQueue.length = 0;
- }
-
- function queueFn(asyncFn) {
- var index = taskQueue.length;
-
- taskCount++;
- taskQueue.push(asyncFn);
-
- if (index === 0) {
- cancelLastRAF = rafFn(flush);
- }
-
- return function cancelQueueFn() {
- if (index >= 0) {
- taskQueue[index] = null;
- index = null;
-
- if (--taskCount === 0 && cancelLastRAF) {
- cancelLastRAF();
- cancelLastRAF = null;
- taskQueue.length = 0;
- }
- }
- };
- }
+ return raf;
}];
}
@@ -14916,15 +16080,15 @@ function $$RAFProvider() { //rAF
* exposed as $$____ properties
*
* Loop operations are optimized by using while(count--) { ... }
- * - this means that in order to keep the same order of execution as addition we have to add
+ * - This means that in order to keep the same order of execution as addition we have to add
* items to the array at the beginning (unshift) instead of at the end (push)
*
* Child scopes are created and removed often
- * - Using an array would be slow since inserts in middle are expensive so we use linked list
+ * - Using an array would be slow since inserts in the middle are expensive; so we use linked lists
*
- * There are few watches then a lot of observers. This is why you don't want the observer to be
- * implemented in the same way as watch. Watch requires return of initialization function which
- * are expensive to construct.
+ * There are fewer watches than observers. This is why you don't want the observer to be implemented
+ * in the same way as watch. Watch requires return of the initialization function which is expensive
+ * to construct.
*/
@@ -14966,7 +16130,7 @@ function $$RAFProvider() { //rAF
* Every application has a single root {@link ng.$rootScope.Scope scope}.
* All other scopes are descendant scopes of the root scope. Scopes provide separation
* between the model and the view, via a mechanism for watching the model for changes.
- * They also provide an event emission/broadcast and subscription facility. See the
+ * They also provide event emission/broadcast and subscription facility. See the
* {@link guide/scope developer guide on scopes}.
*/
function $RootScopeProvider() {
@@ -14996,13 +16160,36 @@ function $RootScopeProvider() {
return ChildScope;
}
- this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
- function($injector, $exceptionHandler, $parse, $browser) {
+ this.$get = ['$exceptionHandler', '$parse', '$browser',
+ function($exceptionHandler, $parse, $browser) {
function destroyChildScope($event) {
$event.currentScope.$$destroyed = true;
}
+ function cleanUpScope($scope) {
+
+ if (msie === 9) {
+ // There is a memory leak in IE9 if all child scopes are not disconnected
+ // completely when a scope is destroyed. So this code will recurse up through
+ // all this scopes children
+ //
+ // See issue https://github.com/angular/angular.js/issues/10706
+ $scope.$$childHead && cleanUpScope($scope.$$childHead);
+ $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
+ }
+
+ // The code below works around IE9 and V8's memory leaks
+ //
+ // See:
+ // - https://code.google.com/p/v8/issues/detail?id=2073#c26
+ // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
+ // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
+
+ $scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead =
+ $scope.$$childTail = $scope.$root = $scope.$$watchers = null;
+ }
+
/**
* @ngdoc type
* @name $rootScope.Scope
@@ -15011,12 +16198,9 @@ function $RootScopeProvider() {
* A root scope can be retrieved using the {@link ng.$rootScope $rootScope} key from the
* {@link auto.$injector $injector}. Child scopes are created using the
* {@link ng.$rootScope.Scope#$new $new()} method. (Most scopes are created automatically when
- * compiled HTML template is executed.)
+ * compiled HTML template is executed.) See also the {@link guide/scope Scopes guide} for
+ * an in-depth introduction and usage examples.
*
- * Here is a simple scope snippet to show how you can interact with the scope.
- * ```html
- * <file src="./test/ng/rootScopeSpec.js" tag="docs1" />
- * ```
*
* # Inheritance
* A scope can inherit from a parent scope, as in this example:
@@ -15158,10 +16342,10 @@ function $RootScopeProvider() {
* Registers a `listener` callback to be executed whenever the `watchExpression` changes.
*
* - The `watchExpression` is called on every call to {@link ng.$rootScope.Scope#$digest
- * $digest()} and should return the value that will be watched. (Since
- * {@link ng.$rootScope.Scope#$digest $digest()} reruns when it detects changes the
- * `watchExpression` can execute multiple times per
- * {@link ng.$rootScope.Scope#$digest $digest()} and should be idempotent.)
+ * $digest()} and should return the value that will be watched. (`watchExpression` should not change
+ * its value when executed multiple times with the same input because it may be executed multiple
+ * times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be
+ * [idempotent](http://en.wikipedia.org/wiki/Idempotence).
* - The `listener` is called only when the value from the current `watchExpression` and the
* previous call to `watchExpression` are not equal (with the exception of the initial run,
* see below). Inequality is determined according to reference inequality,
@@ -15178,9 +16362,9 @@ function $RootScopeProvider() {
*
*
* If you want to be notified whenever {@link ng.$rootScope.Scope#$digest $digest} is called,
- * you can register a `watchExpression` function with no `listener`. (Since `watchExpression`
- * can execute multiple times per {@link ng.$rootScope.Scope#$digest $digest} cycle when a
- * change is detected, be prepared for multiple calls to your listener.)
+ * you can register a `watchExpression` function with no `listener`. (Be prepared for
+ * multiple calls to your `watchExpression` because it will execute multiple times in a
+ * single {@link ng.$rootScope.Scope#$digest $digest} cycle if a change is detected.)
*
* After a watcher is registered with the scope, the `listener` fn is called asynchronously
* (via {@link ng.$rootScope.Scope#$evalAsync $evalAsync}) to initialize the
@@ -15261,7 +16445,7 @@ function $RootScopeProvider() {
* - `newVal` contains the current value of the `watchExpression`
* - `oldVal` contains the previous value of the `watchExpression`
* - `scope` refers to the current scope
- * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of
+ * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of
* comparing for reference equality.
* @returns {function()} Returns a deregistration function for this listener.
*/
@@ -15510,7 +16694,7 @@ function $RootScopeProvider() {
// copy the items to oldValue and look for changes.
newLength = 0;
for (key in newValue) {
- if (newValue.hasOwnProperty(key)) {
+ if (hasOwnProperty.call(newValue, key)) {
newLength++;
newItem = newValue[key];
oldItem = oldValue[key];
@@ -15532,7 +16716,7 @@ function $RootScopeProvider() {
// we used to have more keys, need to find them and destroy them.
changeDetected++;
for (key in oldValue) {
- if (!newValue.hasOwnProperty(key)) {
+ if (!hasOwnProperty.call(newValue, key)) {
oldLength--;
delete oldValue[key];
}
@@ -15626,13 +16810,13 @@ function $RootScopeProvider() {
*
*/
$digest: function() {
- var watch, value, last,
+ var watch, value, last, fn, get,
watchers,
length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
- logIdx, logMsg, asyncTask;
+ logIdx, asyncTask;
beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
@@ -15672,7 +16856,8 @@ function $RootScopeProvider() {
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch) {
- if ((value = watch.get(current)) !== (last = watch.last) &&
+ get = watch.get;
+ if ((value = get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
: (typeof value === 'number' && typeof last === 'number'
@@ -15680,7 +16865,8 @@ function $RootScopeProvider() {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;
- watch.fn(value, ((last === initWatchVal) ? value : last), current);
+ fn = watch.fn;
+ fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
@@ -15802,16 +16988,9 @@ function $RootScopeProvider() {
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
this.$$listeners = {};
- // All of the code below is bogus code that works around V8's memory leak via optimized code
- // and inline caches.
- //
- // see:
- // - https://code.google.com/p/v8/issues/detail?id=2073#c26
- // - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
- // - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
-
- this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
- this.$$childTail = this.$root = this.$$watchers = null;
+ // Disconnect the next sibling to prevent `cleanUpScope` destroying those too
+ this.$$nextSibling = null;
+ cleanUpScope(this);
},
/**
@@ -15887,7 +17066,7 @@ function $RootScopeProvider() {
});
}
- asyncQueue.push({scope: this, expression: expr, locals: locals});
+ asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
@@ -15942,11 +17121,14 @@ function $RootScopeProvider() {
$apply: function(expr) {
try {
beginPhase('$apply');
- return this.$eval(expr);
+ try {
+ return this.$eval(expr);
+ } finally {
+ clearPhase();
+ }
} catch (e) {
$exceptionHandler(e);
} finally {
- clearPhase();
try {
$rootScope.$digest();
} catch (e) {
@@ -15976,6 +17158,7 @@ function $RootScopeProvider() {
$applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
+ expr = $parse(expr);
scheduleApplyAsync();
function $applyAsyncExpression() {
@@ -16251,6 +17434,21 @@ function $RootScopeProvider() {
}
/**
+ * @ngdoc service
+ * @name $rootElement
+ *
+ * @description
+ * The root element of Angular application. This is either the element where {@link
+ * ng.directive:ngApp ngApp} was declared or the element passed into
+ * {@link angular.bootstrap}. The element represents the root element of application. It is also the
+ * location where the application's {@link auto.$injector $injector} service gets
+ * published, and can be retrieved using `$rootElement.injector()`.
+ */
+
+
+// the implementation is in angular.bootstrap
+
+/**
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
@@ -16464,13 +17662,15 @@ function $SceDelegateProvider() {
* @kind function
*
* @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * provided. This must be an array or null. A snapshot of this array is used so further
+ * changes to the array are ignored.
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * Note: **an empty whitelist array will block all URLs**!
+ * <div class="alert alert-warning">
+ * **Note:** an empty whitelist array will block all URLs!
+ * </div>
*
* @return {Array} the currently set whitelist array.
*
@@ -16493,17 +17693,17 @@ function $SceDelegateProvider() {
* @kind function
*
* @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * provided. This must be an array or null. A snapshot of this array is used so further
+ * changes to the array are ignored.
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * The typical usage for the blacklist is to **block
- * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
- * these would otherwise be trusted but actually return content from the redirected domain.
+ * The typical usage for the blacklist is to **block
+ * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
+ * these would otherwise be trusted but actually return content from the redirected domain.
*
- * Finally, **the blacklist overrides the whitelist** and has the final say.
+ * Finally, **the blacklist overrides the whitelist** and has the final say.
*
* @return {Array} the currently set blacklist array.
*
@@ -16614,7 +17814,7 @@ function $SceDelegateProvider() {
'Attempted to trust a value in invalid context. Context: {0}; Value: {1}',
type, trustedValue);
}
- if (trustedValue === null || trustedValue === undefined || trustedValue === '') {
+ if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') {
return trustedValue;
}
// All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting
@@ -16662,6 +17862,11 @@ function $SceDelegateProvider() {
* returns the originally supplied value if the queried context type is a supertype of the
* created type. If this condition isn't satisfied, throws an exception.
*
+ * <div class="alert alert-danger">
+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ * </div>
+ *
* @param {string} type The kind of context in which this value is to be used.
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} call.
@@ -16669,7 +17874,7 @@ function $SceDelegateProvider() {
* `$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);
@@ -16804,7 +18009,7 @@ 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*:
@@ -16862,10 +18067,10 @@ function $SceDelegateProvider() {
* - There are exactly **two wildcard sequences** - `*` and `**`. All other characters
* match themselves.
* - `*`: matches zero or more occurrences of any character other than one of the following 6
- * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use
+ * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use
* in a whitelist.
* - `**`: matches zero or more occurrences of *any* character. As such, it's not
- * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g.
+ * 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.) Its usage at the very end of the path is ok. (e.g.
* http://foo.example.com/templates/**).
@@ -16873,11 +18078,11 @@ function $SceDelegateProvider() {
* - *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
@@ -16887,7 +18092,7 @@ function $SceDelegateProvider() {
* 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
@@ -17396,6 +18601,10 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
+ // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
+ // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
+ isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
+ hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
@@ -17440,7 +18649,7 @@ function $SnifferProvider() {
// so let's not use the history API also
// We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
// jshint -W018
- history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
+ history: !!(hasHistoryPushState && !(android < 4) && !boxee),
// jshint +W018
hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
@@ -17466,29 +18675,66 @@ function $SnifferProvider() {
}];
}
-var $compileMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$compile');
/**
- * @ngdoc service
- * @name $templateRequest
- *
+ * @ngdoc provider
+ * @name $templateRequestProvider
* @description
- * The `$templateRequest` service runs security checks then downloads the provided template using
- * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
- * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
- * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
- * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
- * when `tpl` is of type string and `$templateCache` has the matching entry.
- *
- * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
- * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
- *
- * @return {Promise} a promise for the HTTP response data of the given URL.
+ * Used to configure the options passed to the {@link $http} service when making a template request.
*
- * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
+ * For example, it can be used for specifying the "Accept" header that is sent to the server, when
+ * requesting a template.
*/
function $TemplateRequestProvider() {
+
+ var httpOptions;
+
+ /**
+ * @ngdoc method
+ * @name $templateRequestProvider#httpOptions
+ * @description
+ * The options to be passed to the {@link $http} service when making the request.
+ * You can use this to override options such as the "Accept" header for template requests.
+ *
+ * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the
+ * options if not overridden here.
+ *
+ * @param {string=} value new value for the {@link $http} options.
+ * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter.
+ */
+ this.httpOptions = function(val) {
+ if (val) {
+ httpOptions = val;
+ return this;
+ }
+ return httpOptions;
+ };
+
+ /**
+ * @ngdoc service
+ * @name $templateRequest
+ *
+ * @description
+ * The `$templateRequest` service runs security checks then downloads the provided template using
+ * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
+ * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
+ * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
+ * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
+ * when `tpl` is of type string and `$templateCache` has the matching entry.
+ *
+ * If you want to pass custom options to the `$http` service, such as setting the Accept header you
+ * can configure this via {@link $templateRequestProvider#httpOptions}.
+ *
+ * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
+ * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
+ *
+ * @return {Promise} a promise for the HTTP response data of the given URL.
+ *
+ * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
+ */
this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
+
function handleRequestFn(tpl, ignoreRequestError) {
handleRequestFn.totalPendingRequests++;
@@ -17511,12 +18757,10 @@ function $TemplateRequestProvider() {
transformResponse = null;
}
- var httpOptions = {
- cache: $templateCache,
- transformResponse: transformResponse
- };
-
- return $http.get(tpl, httpOptions)
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
['finally'](function() {
handleRequestFn.totalPendingRequests--;
})
@@ -17527,7 +18771,7 @@ function $TemplateRequestProvider() {
function handleError(resp) {
if (!ignoreRequestError) {
- throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
+ throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
tpl, resp.status, resp.statusText);
}
return $q.reject(resp);
@@ -17687,8 +18931,8 @@ function $TimeoutProvider() {
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @param {...*=} Pass additional parameters to the executed function.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
- * promise will be resolved with is the return value of the `fn` function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
*
*/
function timeout(fn, delay, invokeApply) {
@@ -17775,20 +19019,13 @@ var originUrl = urlResolve(window.location.href);
*
* 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 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
@@ -17937,7 +19174,7 @@ function $$CookieReader($document) {
// the first value that is seen for a cookie is the most
// specific one. values for the same cookie name that
// follow are for less specific paths.
- if (lastCookies[name] === undefined) {
+ if (isUndefined(lastCookies[name])) {
lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
}
}
@@ -18068,6 +19305,7 @@ function $FilterProvider($provide) {
* your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
* (`myapp_subsection_filterx`).
* </div>
+ * @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.
*/
@@ -18370,6 +19608,10 @@ function getTypeForFilter(val) {
return (val === null) ? 'null' : typeof val;
}
+var MAX_DIGITS = 22;
+var DECIMAL_SEP = '.';
+var ZERO_CHAR = '0';
+
/**
* @ngdoc filter
* @name currency
@@ -18415,9 +19657,9 @@ function getTypeForFilter(val) {
}
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)');
+ 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');
});
</file>
</example>
@@ -18451,7 +19693,7 @@ function currencyFilter($locale) {
* Formats a number as text.
*
* If the input is null or undefined, it will just be returned.
- * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is 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.
*
*
@@ -18459,7 +19701,7 @@ function currencyFilter($locale) {
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
- * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
+ * @returns {string} Number rounded to fractionSize and places a “,” after each third digit.
*
* @example
<example module="numberFilterExample">
@@ -18494,8 +19736,6 @@ function currencyFilter($locale) {
</file>
</example>
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
@@ -18509,102 +19749,227 @@ function numberFilter($locale) {
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (isObject(number)) return '';
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
- var isNegative = number < 0;
- number = Math.abs(number);
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
+ }
- var isInfinity = number === Infinity;
- if (!isInfinity && !isFinite(number)) return '';
+ // 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;
+ }
- var numStr = number + '',
- formatedText = '',
- hasExponent = false,
- parts = [];
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */}
- if (isInfinity) formatedText = '\u221e';
+ 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--;
- if (!isInfinity && numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- number = 0;
- } else {
- formatedText = numStr;
- hasExponent = true;
+ // 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 (!isInfinity && !hasExponent) {
- var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
+ // 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;
+ }
- // determine fractionSize if it is not specified
- if (isUndefined(fractionSize)) {
- fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
- }
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
+}
+
+/**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
- // safely round numbers in JS without hitting imprecisions of floating-point arithmetics
- // inspired by:
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
- number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
- var i, pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
+ if (roundAt > 0) {
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (i = 0; i < pos; i++) {
- if ((pos - i) % group === 0 && i !== 0) {
- formatedText += groupSep;
+ // 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;
+ }
+
+ if (digit >= 5) {
+ if (roundAt - 1 < 0) {
+ for (var k = 0; k > roundAt; k--) {
+ digits.unshift(0);
+ parsedNumber.i++;
}
- formatedText += whole.charAt(i);
+ digits.unshift(1);
+ parsedNumber.i++;
+ } else {
+ digits[roundAt - 1]++;
}
}
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i) % lgroup === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
- // format fraction part.
- while (fraction.length < fractionSize) {
- fraction += '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 (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
+
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
} else {
- if (fractionSize > 0 && number < 1) {
- formatedText = number.toFixed(fractionSize);
- number = parseFloat(formatedText);
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
+
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
}
- }
- if (number === 0) {
- isNegative = false;
- }
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
+ }
- parts.push(isNegative ? pattern.negPre : pattern.posPre,
- formatedText,
- 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;
+ while (num.length < digits) num = ZERO_CHAR + num;
if (trim) {
num = num.substr(num.length - digits);
}
@@ -18612,7 +19977,7 @@ function padNumber(num, digits, trim) {
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
@@ -18620,14 +19985,15 @@ function dateGetter(name, size, offset, trim) {
value += offset;
}
if (value === 0 && offset == -12) value = 12;
- return padNumber(value, size, trim);
+ return padNumber(value, size, trim, negWrap);
};
}
-function dateStrGetter(name, shortForm) {
+function dateStrGetter(name, shortForm, standAlone) {
return function(date, formats) {
var value = date['get' + name]();
- var get = uppercase(shortForm ? ('SHORT' + name) : name);
+ var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
+ var get = uppercase(propPrefix + name);
return formats[get][value];
};
@@ -18682,13 +20048,14 @@ function longEraGetter(date, formats) {
}
var DATE_FORMATS = {
- yyyy: dateGetter('FullYear', 4),
- yy: dateGetter('FullYear', 2, 0, true),
- y: dateGetter('FullYear', 1),
+ yyyy: dateGetter('FullYear', 4, 0, false, true),
+ yy: dateGetter('FullYear', 2, 0, true, true),
+ y: dateGetter('FullYear', 1, 0, false, true),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
+ LLLL: dateStrGetter('Month', false, true),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
@@ -18714,7 +20081,7 @@ var DATE_FORMATS = {
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;
/**
@@ -18734,6 +20101,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
* * `'MMM'`: Month in year (Jan-Dec)
* * `'MM'`: Month in year, padded (01-12)
* * `'M'`: Month in year (1-12)
+ * * `'LLLL'`: Stand-alone month in year (January-December)
* * `'dd'`: Day in month, padded (01-31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
@@ -18873,13 +20241,13 @@ function dateFilter($locale) {
var dateTimezoneOffset = date.getTimezoneOffset();
if (timezone) {
- dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
+ dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
date = convertTimezoneToLocal(date, timezone, true);
}
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
- : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
});
return text;
@@ -19060,7 +20428,7 @@ function limitToFilter() {
if (!isArray(input) && !isString(input)) return input;
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
- begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin;
+ begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
if (limit >= 0) {
return input.slice(begin, begin + limit);
@@ -19083,8 +20451,9 @@ function limitToFilter() {
* Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
* for strings and numerically for numbers. Note: if you notice numbers are not being sorted
* as expected, make sure they are actually being saved as numbers and not strings.
+ * Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported.
*
- * @param {Array} array The array to sort.
+ * @param {Array} array The array (or array-like object) to sort.
* @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
* used by the comparator to determine the order of elements.
*
@@ -19115,17 +20484,6 @@ function limitToFilter() {
* `reverse` is not set, which means it defaults to `false`.
<example module="orderByExample">
<file name="index.html">
- <script>
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
- }]);
- </script>
<div ng-controller="ExampleController">
<table class="friend">
<tr>
@@ -19141,6 +20499,17 @@ function limitToFilter() {
</table>
</div>
</file>
+ <file name="script.js">
+ angular.module('orderByExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends =
+ [{name:'John', phone:'555-1212', age:10},
+ {name:'Mary', phone:'555-9876', age:19},
+ {name:'Mike', phone:'555-4321', age:21},
+ {name:'Adam', phone:'555-5678', age:35},
+ {name:'Julie', phone:'555-8765', age:29}];
+ }]);
+ </file>
</example>
*
* The predicate and reverse parameters can be controlled dynamically through scope properties,
@@ -19148,49 +20517,24 @@ function limitToFilter() {
* @example
<example module="orderByExample">
<file name="index.html">
- <script>
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
- $scope.predicate = 'age';
- $scope.reverse = true;
- $scope.order = function(predicate) {
- $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
- $scope.predicate = predicate;
- };
- }]);
- </script>
- <style type="text/css">
- .sortorder:after {
- content: '\25b2';
- }
- .sortorder.reverse:after {
- content: '\25bc';
- }
- </style>
<div ng-controller="ExampleController">
<pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
<hr/>
- [ <a href="" ng-click="predicate=''">unsorted</a> ]
+ <button ng-click="predicate=''">Set to unsorted</button>
<table class="friend">
<tr>
- <th>
- <a href="" ng-click="order('name')">Name</a>
- <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <a href="" ng-click="order('phone')">Phone Number</a>
- <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
- </th>
- <th>
- <a href="" ng-click="order('age')">Age</a>
- <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
- </th>
+ <th>
+ <button ng-click="order('name')">Name</button>
+ <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('age')">Age</button>
+ <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
+ </th>
</tr>
<tr ng-repeat="friend in friends | orderBy:predicate:reverse">
<td>{{friend.name}}</td>
@@ -19200,6 +20544,31 @@ function limitToFilter() {
</table>
</div>
</file>
+ <file name="script.js">
+ angular.module('orderByExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends =
+ [{name:'John', phone:'555-1212', age:10},
+ {name:'Mary', phone:'555-9876', age:19},
+ {name:'Mike', phone:'555-4321', age:21},
+ {name:'Adam', phone:'555-5678', age:35},
+ {name:'Julie', phone:'555-8765', age:29}];
+ $scope.predicate = 'age';
+ $scope.reverse = true;
+ $scope.order = function(predicate) {
+ $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
+ $scope.predicate = predicate;
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .sortorder:after {
+ content: '\25b2';
+ }
+ .sortorder.reverse:after {
+ content: '\25bc';
+ }
+ </file>
</example>
*
* It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the
@@ -19211,21 +20580,30 @@ function limitToFilter() {
* @example
<example module="orderByExample">
<file name="index.html">
- <div ng-controller="ExampleController">
- <table class="friend">
- <tr>
- <th><a href="" ng-click="reverse=false;order('name', false)">Name</a>
- (<a href="" ng-click="order('-name',false)">^</a>)</th>
- <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th>
- <th><a href="" ng-click="reverse=!reverse;order('age',reverse)">Age</a></th>
- </tr>
- <tr ng-repeat="friend in friends">
- <td>{{friend.name}}</td>
- <td>{{friend.phone}}</td>
- <td>{{friend.age}}</td>
- </tr>
- </table>
- </div>
+ <div ng-controller="ExampleController">
+ <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
+ <table class="friend">
+ <tr>
+ <th>
+ <button ng-click="order('name')">Name</button>
+ <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('age')">Age</button>
+ <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
+ </th>
+ </tr>
+ <tr ng-repeat="friend in friends">
+ <td>{{friend.name}}</td>
+ <td>{{friend.phone}}</td>
+ <td>{{friend.age}}</td>
+ </tr>
+ </table>
+ </div>
</file>
<file name="script.js">
@@ -19239,24 +20617,42 @@ function limitToFilter() {
{ name: 'Adam', phone: '555-5678', age: 35 },
{ name: 'Julie', phone: '555-8765', age: 29 }
];
- $scope.order = function(predicate, reverse) {
- $scope.friends = orderBy($scope.friends, predicate, reverse);
+ $scope.order = function(predicate) {
+ $scope.predicate = predicate;
+ $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
+ $scope.friends = orderBy($scope.friends, predicate, $scope.reverse);
};
- $scope.order('-age',false);
+ $scope.order('age', true);
}]);
</file>
+
+ <file name="style.css">
+ .sortorder:after {
+ content: '\25b2';
+ }
+ .sortorder.reverse:after {
+ content: '\25bc';
+ }
+ </file>
</example>
*/
orderByFilter.$inject = ['$parse'];
function orderByFilter($parse) {
return function(array, sortPredicate, reverseOrder) {
- if (!(isArrayLike(array))) return array;
+ if (array == null) return array;
+ if (!isArrayLike(array)) {
+ throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array);
+ }
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
var predicates = processPredicates(sortPredicate, reverseOrder);
+ // Add a predicate at the end that evaluates to the element index. This makes the
+ // sort stable as it works as a tie-breaker when all the input predicates cannot
+ // distinguish between two elements.
+ predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1});
// The next three lines are a version of a Swartzian Transform idiom from Perl
// (sometimes called the Decorate-Sort-Undecorate idiom)
@@ -19570,20 +20966,7 @@ var htmlAnchorDirective = valueFn({
* {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
* A special directive is necessary because we cannot use interpolation inside the `disabled`
- * attribute. The following example would make the button enabled on Chrome/Firefox
- * but not on older IEs:
- *
- * ```html
- * <!-- See below for an example of ng-disabled being used correctly -->
- * <div ng-init="isDisabled = false">
- * <button disabled="{{isDisabled}}">Disabled</button>
- * </div>
- * ```
- *
- * This is because the HTML specification does not require browsers to preserve the values of
- * boolean attributes such as `disabled` (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
<example>
@@ -19618,15 +21001,9 @@ var htmlAnchorDirective = valueFn({
* Note that this directive should not be used together with {@link ngModel `ngModel`},
* as this can lead to unexpected behavior.
*
- * ### Why do we need `ngChecked`?
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as checked. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngChecked` directive solves this problem for the `checked` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
* @example
<example>
<file name="index.html">
@@ -19655,13 +21032,12 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as readonly. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngReadonly` directive solves this problem for the `readonly` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * Sets the `readOnly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ *
+ * 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
<example>
<file name="index.html">
@@ -19690,13 +21066,11 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as selected. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngSelected` directive solves this problem for the `selected` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `selected`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
<example>
@@ -19728,13 +21102,12 @@ var htmlAnchorDirective = valueFn({
* @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.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngOpen` directive solves this problem for the `open` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * 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.
+ *
* @example
<example>
<file name="index.html">
@@ -19878,6 +21251,7 @@ function nullFormRenameControl(control, name) {
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
+ * @property {boolean} $pending True if at least one containing control or form is pending.
* @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
* @property {Object} $error Is an object hash, containing references to controls or
@@ -19917,8 +21291,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
var form = this,
controls = [];
- var parentForm = form.$$parentForm = element.parent().controller('form') || nullFormCtrl;
-
// init state
form.$error = {};
form.$$success = {};
@@ -19929,8 +21301,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$valid = true;
form.$invalid = false;
form.$submitted = false;
-
- parentForm.$addControl(form);
+ form.$$parentForm = nullFormCtrl;
/**
* @ngdoc method
@@ -19969,11 +21340,23 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
/**
* @ngdoc method
* @name form.FormController#$addControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
- * Register a control with the form.
+ * Register a control with the form. Input elements using ngModelController do this automatically
+ * when they are linked.
+ *
+ * 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.
*
- * Input elements using ngModelController do this automatically when they are linked.
+ * 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.
+ *
+ * For example, if an input control is added that is already `$dirty` and has `$error` properties,
+ * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
form.$addControl = function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
@@ -19984,6 +21367,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
if (control.$name) {
form[control.$name] = control;
}
+
+ control.$$parentForm = form;
};
// Private API: rename a form control
@@ -20000,11 +21385,18 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
/**
* @ngdoc method
* @name form.FormController#$removeControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
* Deregister a control from the form.
*
* Input elements using ngModelController do this automatically when they are destroyed.
+ *
+ * Note that only the removed control's validation state (`$errors`etc.) will be removed from the
+ * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be
+ * different from case to case. For example, removing the only `$dirty` control from a form may or
+ * may not mean that the form is still `$dirty`.
*/
form.$removeControl = function(control) {
if (control.$name && form[control.$name] === control) {
@@ -20021,6 +21413,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
});
arrayRemove(controls, control);
+ control.$$parentForm = nullFormCtrl;
};
@@ -20057,7 +21450,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
delete object[property];
}
},
- parentForm: parentForm,
$animate: $animate
});
@@ -20076,7 +21468,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
$animate.addClass(element, DIRTY_CLASS);
form.$dirty = true;
form.$pristine = false;
- parentForm.$setDirty();
+ form.$$parentForm.$setDirty();
};
/**
@@ -20132,7 +21524,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$setSubmitted = function() {
$animate.addClass(element, SUBMITTED_CLASS);
form.$submitted = true;
- parentForm.$setSubmitted();
+ form.$$parentForm.$setSubmitted();
};
}
@@ -20171,17 +21563,14 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
*
* In Angular, forms can be nested. This means that the outer form is valid when all of the child
* forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so
- * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to
- * `<form>` but can be nested. This allows you to have nested forms, which is very useful when
- * using Angular validation directives in forms that are dynamically generated using the
- * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name`
- * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an
- * `ngForm` directive and nest these in an outer `form` element.
- *
+ * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
+ * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group
+ * of controls needs to be determined.
*
* # CSS classes
* - `ng-valid` is set if the form is valid.
* - `ng-invalid` is set if the form is invalid.
+ * - `ng-pending` is set if the form is pending.
* - `ng-pristine` is set if the form is pristine.
* - `ng-dirty` is set if the form is dirty.
* - `ng-submitted` is set if the form was submitted.
@@ -20257,7 +21646,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
</script>
<style>
.my-form {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: transparent;
}
@@ -20302,10 +21690,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* related scope, under this name.
*/
var formDirectiveFactory = function(isNgForm) {
- return ['$timeout', function($timeout) {
+ return ['$timeout', '$parse', function($timeout, $parse) {
var formDirective = {
name: 'form',
restrict: isNgForm ? 'EAC' : 'E',
+ require: ['form', '^^?form'], //first is the form's own ctrl, second is an optional parent form
controller: FormController,
compile: function ngFormCompile(formElement, attr) {
// Setup initial state of the control
@@ -20314,7 +21703,9 @@ var formDirectiveFactory = function(isNgForm) {
var nameAttr = attr.name ? 'name' : (isNgForm && attr.ngForm ? 'ngForm' : false);
return {
- pre: function ngFormPreLink(scope, formElement, attr, controller) {
+ pre: function ngFormPreLink(scope, formElement, attr, ctrls) {
+ var controller = ctrls[0];
+
// if `action` attr is not present on the form, prevent the default action (submission)
if (!('action' in attr)) {
// we can't use jq events because if a form is destroyed during submission the default
@@ -20343,22 +21734,24 @@ var formDirectiveFactory = function(isNgForm) {
});
}
- var parentFormCtrl = controller.$$parentForm;
+ var parentFormCtrl = ctrls[1] || controller.$$parentForm;
+ parentFormCtrl.$addControl(controller);
+
+ var setter = nameAttr ? getSetter(controller.$name) : noop;
if (nameAttr) {
- setter(scope, controller.$name, controller, controller.$name);
+ setter(scope, controller);
attr.$observe(nameAttr, function(newValue) {
if (controller.$name === newValue) return;
- setter(scope, controller.$name, undefined, controller.$name);
- parentFormCtrl.$$renameControl(controller, newValue);
- setter(scope, controller.$name, controller, controller.$name);
+ setter(scope, undefined);
+ controller.$$parentForm.$$renameControl(controller, newValue);
+ setter = getSetter(controller.$name);
+ setter(scope, controller);
});
}
formElement.on('$destroy', function() {
- parentFormCtrl.$removeControl(controller);
- if (nameAttr) {
- setter(scope, attr[nameAttr], undefined, controller.$name);
- }
+ controller.$$parentForm.$removeControl(controller);
+ setter(scope, undefined);
extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
});
}
@@ -20367,6 +21760,14 @@ var formDirectiveFactory = function(isNgForm) {
};
return formDirective;
+
+ function getSetter(expression) {
+ if (expression === '') {
+ //create an assignable expression, so forms with an empty name can be renamed later
+ return $parse('this[""]').assign;
+ }
+ return $parse(expression).assign || noop;
+ }
}];
};
@@ -20379,20 +21780,38 @@ var ngFormDirective = formDirectiveFactory(true);
DIRTY_CLASS: false,
UNTOUCHED_CLASS: false,
TOUCHED_CLASS: false,
- $ngModelMinErr: false,
+ ngModelMinErr: false,
*/
-// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
-var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
-var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
+// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
+var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/;
+// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
+// Note: We are being more lenient, because browsers are too.
+// 1. Scheme
+// 2. Slashes
+// 3. Username
+// 4. Password
+// 5. Hostname
+// 6. Port
+// 7. Path
+// 8. Query
+// 9. Fragment
+// 1111111111111111 222 333333 44444 555555555555555555555555 666 77777777 8888888 999
+var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
-var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
-var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
-var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
-var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
+var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/;
+var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
+var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
+var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
+var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
+var PARTIAL_VALIDATION_TYPES = createMap();
+forEach('date,datetime-local,month,time,week'.split(','), function(type) {
+ PARTIAL_VALIDATION_TYPES[type] = true;
+});
+
var inputType = {
/**
@@ -20417,8 +21836,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -20511,9 +21930,17 @@ var inputType = {
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
- * valid ISO date string (yyyy-MM-dd).
+ * valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
+ * (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5
+ * constraint validation.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
- * a valid ISO date string (yyyy-MM-dd).
+ * a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
+ * (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5
+ * constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -20605,10 +22032,18 @@ var inputType = {
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
- * valid ISO datetime format (yyyy-MM-ddTHH:mm:ss).
- * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
- * a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss).
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
+ * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
+ * Note that `min` will also add native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
+ * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
+ * Note that `max` will also add native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -20689,7 +22124,7 @@ var inputType = {
*
* @description
* Input with time validation and transformation. In browsers that do not yet support
- * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
*
@@ -20701,10 +22136,18 @@ var inputType = {
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
- * valid ISO time format (HH:mm:ss).
- * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be a
- * valid ISO time format (HH:mm:ss).
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the
+ * `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the
+ * `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -20724,7 +22167,7 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- <label for="exampleInput">Pick a between 8am and 5pm:</label>
+ <label for="exampleInput">Pick a time between 8am and 5pm:</label>
<input type="time" id="exampleInput" name="input" ng-model="example.value"
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
<div role="alert">
@@ -20796,10 +22239,18 @@ var inputType = {
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
- * valid ISO week format (yyyy-W##).
- * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
- * a valid ISO week format (yyyy-W##).
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -20893,10 +22344,19 @@ var inputType = {
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be
- * a valid ISO month format (yyyy-MM).
- * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must
- * be a valid ISO month format (yyyy-MM).
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
+
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -21011,8 +22471,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21109,8 +22569,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21208,8 +22668,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21428,7 +22888,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
if (!$sniffer.android) {
var composing = false;
- element.on('compositionstart', function(data) {
+ element.on('compositionstart', function() {
composing = true;
});
@@ -21438,6 +22898,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
+ var timeout;
+
var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
@@ -21467,8 +22929,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
if ($sniffer.hasEvent('input')) {
element.on('input', listener);
} else {
- var timeout;
-
var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
@@ -21500,8 +22960,32 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// or form autocomplete on newer browser, we need "change" event to catch it
element.on('change', listener);
+ // Some native input types (date-family) have the ability to change validity without
+ // firing any input/change events.
+ // For these event types, when native validators are present and the browser supports the type,
+ // check for validity changes on various DOM events.
+ if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
+ element.on(PARTIAL_VALIDATION_EVENTS, function(ev) {
+ if (!timeout) {
+ var validity = this[VALIDITY_STATE_PROPERTY];
+ var origBadInput = validity.badInput;
+ var origTypeMismatch = validity.typeMismatch;
+ timeout = $browser.defer(function() {
+ timeout = null;
+ if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
+ listener(ev);
+ }
+ });
+ }
+ });
+ }
+
ctrl.$render = function() {
- element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
+ // Workaround for Firefox validation #12102.
+ var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;
+ if (element.val() !== value) {
+ element.val(value);
+ }
};
}
@@ -21612,7 +23096,7 @@ function createDateInputType(type, regexp, parseDate, format) {
ctrl.$formatters.push(function(value) {
if (value && !isDate(value)) {
- throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
+ throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
}
if (isValidDate(value)) {
previousDate = value;
@@ -21654,7 +23138,7 @@ function createDateInputType(type, regexp, parseDate, format) {
}
function parseObservedDateValue(val) {
- return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined;
+ return isDefined(val) && !isDate(val) ? parseDate(val) || undefined : val;
}
};
}
@@ -21665,11 +23149,7 @@ function badInputChecker(scope, element, attr, ctrl) {
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
- // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430):
- // - also sets validity.badInput (should only be validity.typeMismatch).
- // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email)
- // - can ignore this case as we can still read out the erroneous email...
- return validity.badInput && !validity.typeMismatch ? undefined : value;
+ return validity.badInput || validity.typeMismatch ? undefined : value;
});
}
}
@@ -21688,7 +23168,7 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$formatters.push(function(value) {
if (!ctrl.$isEmpty(value)) {
if (!isNumber(value)) {
- throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
+ throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
}
value = value.toString();
}
@@ -21781,7 +23261,7 @@ function parseConstantExpr($parse, context, name, expression, fallback) {
if (isDefined(expression)) {
parseFn = $parse(expression);
if (!parseFn.constant) {
- throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
+ throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' +
'`{1}`.', name, expression);
}
return parseFn(context);
@@ -21841,8 +23321,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21880,8 +23360,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * value does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22149,7 +23629,7 @@ var ngBindDirective = ['$compile', function($compile) {
$compile.$$addBindingInfo(element, attr.ngBind);
element = element[0];
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
- element.textContent = value === undefined ? '' : value;
+ element.textContent = isUndefined(value) ? '' : value;
});
};
}
@@ -22217,7 +23697,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate
$compile.$$addBindingInfo(element, interpolateFn.expressions);
element = element[0];
attr.$observe('ngBindTemplate', function(value) {
- element.textContent = value === undefined ? '' : value;
+ element.textContent = isUndefined(value) ? '' : value;
});
};
}
@@ -22520,9 +24000,10 @@ function classDirective(name, selector) {
* new classes added.
*
* @animations
- * **add** - happens just before the class is applied to the elements
- *
- * **remove** - happens just before the class is removed from the element
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
*
* @element ANY
* @param {expression} ngClass {@link guide/expression Expression} to eval. The result
@@ -22634,7 +24115,6 @@ function classDirective(name, selector) {
</file>
<file name="style.css">
.base-class {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
@@ -23067,27 +24547,29 @@ var ngControllerDirective = [function() {
*
* @element html
* @description
- * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
+ *
+ * Angular has some features that can break certain
+ * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
+ *
+ * If you intend to implement these rules then you must tell Angular not to use these features.
*
* This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
*
- * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
- * For Angular to be CSP compatible there are only two things that we need to do differently:
*
- * - don't use `Function` constructor to generate optimized value getters
- * - don't inject custom stylesheet into the document
+ * The following rules affect Angular:
*
- * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp`
- * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will
- * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will
- * be raised.
+ * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
+ * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
+ * increase in the speed of evaluating Angular expressions.
*
- * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically
- * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}).
- * To make those directives work in CSP mode, include the `angular-csp.css` manually.
+ * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
+ * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
+ * To make these directives work when a CSP rule is blocking inline styles, you must link to the
+ * `angular-csp.css` in your HTML manually.
*
- * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This
- * autodetection however triggers a CSP error to be logged in the console:
+ * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
+ * and automatically deactivates this feature in the {@link $parse} service. This autodetection,
+ * however, triggers a CSP error to be logged in the console:
*
* ```
* Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
@@ -23096,11 +24578,39 @@ var ngControllerDirective = [function() {
* ```
*
* This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
- * directive on the root element of the application or on the `angular.js` script tag, whichever
- * appears first in the html document.
+ * directive on an element of the HTML document that appears before the `<script>` tag that loads
+ * the `angular.js` file.
*
* *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
*
+ * You can specify which of the CSP related Angular features should be deactivated by providing
+ * a value for the `ng-csp` attribute. The options are as follows:
+ *
+ * * no-inline-style: this stops Angular from injecting CSS styles into the DOM
+ *
+ * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings
+ *
+ * You can use these values in the following combinations:
+ *
+ *
+ * * No declaration means that Angular will assume that you can do inline styles, but it will do
+ * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions
+ * of Angular.
+ *
+ * * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
+ * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
+ * of Angular.
+ *
+ * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
+ * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
+ *
+ * * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
+ * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
+ *
+ * * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
+ * styles nor use eval, which is the same as an empty: ng-csp.
+ * E.g.`<body ng-csp="no-inline-style;no-unsafe-eval">`
+ *
* @example
* This example shows how to apply the `ngCsp` directive to the `html` tag.
```html
@@ -23232,7 +24742,7 @@ var ngControllerDirective = [function() {
// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
// bootstrap the system (before $parse is instantiated), for this reason we just have
-// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc
+// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc
/**
* @ngdoc directive
@@ -23752,8 +25262,10 @@ forEach(
* and `leave` effects.
*
* @animations
- * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container
- * leave - happens just before the `ngIf` contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
+ * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM |
*
* @element ANY
* @scope
@@ -23779,7 +25291,6 @@ forEach(
}
.animate-if.ng-enter, .animate-if.ng-leave {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
}
@@ -23795,7 +25306,7 @@ forEach(
</file>
</example>
*/
-var ngIfDirective = ['$animate', function($animate) {
+var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
multiElement: true,
transclude: 'element',
@@ -23811,7 +25322,7 @@ var ngIfDirective = ['$animate', function($animate) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
- clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
+ clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
@@ -23866,8 +25377,10 @@ var ngIfDirective = ['$animate', function($animate) {
* access on some browsers.
*
* @animations
- * enter - animation is used to bring new content into the browser.
- * leave - animation is used to animate existing content away.
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
+ * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
*
* The enter and leave animation occur concurrently.
*
@@ -23877,7 +25390,13 @@ var ngIfDirective = ['$animate', function($animate) {
* @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
* make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
* @param {string=} onload Expression to evaluate when a new partial is loaded.
- *
+ * <div class="alert alert-warning">
+ * **Note:** When using onload on SVG elements in IE11, the browser will try to call
+ * a function with the name on the window element, which will usually throw a
+ * "function is undefined" error. To fix this, you can instead use `data-onload` or a
+ * different form that {@link guide/directive#normalization matches} `onload`.
+ * </div>
+ *
* @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
* $anchorScroll} to scroll the viewport after the content is loaded.
*
@@ -23928,7 +25447,6 @@ var ngIfDirective = ['$animate', function($animate) {
}
.slide-animate.ng-enter, .slide-animate.ng-leave {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
position:absolute;
@@ -24070,6 +25588,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
+ if (scope.$$destroyed) return;
+
if (thisChangeId !== changeCounter) return;
var newScope = scope.$new();
ctrl.template = response;
@@ -24091,6 +25611,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
currentScope.$emit('$includeContentLoaded', src);
scope.$eval(onloadExp);
}, function() {
+ if (scope.$$destroyed) return;
+
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
@@ -24119,7 +25641,7 @@ var ngIncludeFillContentDirective = ['$compile',
priority: -400,
require: 'ngInclude',
link: function(scope, $element, $attr, ctrl) {
- if (/SVG/.test($element[0].toString())) {
+ if (toString.call($element[0]).match(/SVG/)) {
// WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
// support innerHTML, so detect this here and try to generate the contents
// specially.
@@ -24147,16 +25669,18 @@ var ngIncludeFillContentDirective = ['$compile',
* current scope.
*
* <div class="alert alert-danger">
- * The only appropriate use of `ngInit` is for aliasing special properties of
- * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you
- * should use {@link guide/controller controllers} rather than `ngInit`
- * to initialize values on a scope.
+ * This directive can be abused to add unnecessary amounts of logic into your templates.
+ * There are only a few appropriate uses of `ngInit`, such as for aliasing special properties of
+ * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below; and for injecting data via
+ * server side scripting. Besides these few cases, you should use {@link guide/controller controllers}
+ * rather than `ngInit` to initialize values on a scope.
* </div>
+ *
* <div class="alert alert-warning">
- * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make
- * sure you have parenthesis for correct precedence:
+ * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make
+ * sure you have parentheses to ensure correct operator precedence:
* <pre class="prettyprint">
- * `<div ng-init="test1 = (data | orderBy:'name')"></div>`
+ * `<div ng-init="test1 = ($index | toString)"></div>`
* </pre>
* </div>
*
@@ -24269,7 +25793,7 @@ var ngInitDirective = ngDirective({
* </file>
* </example>
*
- * ### Example - splitting on whitespace
+ * ### Example - splitting on newline
* <example name="ngList-directive-newlines">
* <file name="index.html">
* <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
@@ -24346,16 +25870,19 @@ var VALID_CLASS = 'ng-valid',
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending';
-
+ PENDING_CLASS = 'ng-pending',
+ EMPTY_CLASS = 'ng-empty',
+ NOT_EMPTY_CLASS = 'ng-not-empty';
-var $ngModelMinErr = new minErr('ngModel');
+var ngModelMinErr = minErr('ngModel');
/**
* @ngdoc type
* @name ngModel.NgModelController
*
- * @property {string} $viewValue Actual string value in the view.
+ * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
+ * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
+ * is set.
* @property {*} $modelValue The value in the model that the control is bound to.
* @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
the control reads value from the DOM. The functions are called in array order, each passing
@@ -24569,7 +26096,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$$success = {}; // keep valid keys here
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
-
+ this.$$parentForm = nullFormCtrl;
var parsedNgModel = $parse($attr.ngModel),
parsedNgModelAssign = parsedNgModel.assign,
@@ -24594,13 +26121,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
};
ngModelSet = function($scope, newValue) {
if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
+ invokeModelSetter($scope, {$$$p: newValue});
} else {
- parsedNgModelAssign($scope, ctrl.$modelValue);
+ parsedNgModelAssign($scope, newValue);
}
};
} else if (!parsedNgModel.assign) {
- throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
+ throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
$attr.ngModel, startingTag($element));
}
};
@@ -24621,7 +26148,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* the `$viewValue` are different from last time.
*
* Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
- * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue`
+ * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
* or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
* invoked if you only change a property on the objects.
*/
@@ -24649,8 +26176,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
return isUndefined(value) || value === '' || value === null || value !== value;
};
- var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
- currentValidationRunId = 0;
+ this.$$updateEmptyClasses = function(value) {
+ if (ctrl.$isEmpty(value)) {
+ $animate.removeClass($element, NOT_EMPTY_CLASS);
+ $animate.addClass($element, EMPTY_CLASS);
+ } else {
+ $animate.removeClass($element, EMPTY_CLASS);
+ $animate.addClass($element, NOT_EMPTY_CLASS);
+ }
+ };
+
+
+ var currentValidationRunId = 0;
/**
* @ngdoc method
@@ -24683,7 +26220,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
unset: function(object, property) {
delete object[property];
},
- parentForm: parentForm,
$animate: $animate
});
@@ -24721,7 +26257,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$pristine = false;
$animate.removeClass($element, PRISTINE_CLASS);
$animate.addClass($element, DIRTY_CLASS);
- parentForm.$setDirty();
+ ctrl.$$parentForm.$setDirty();
};
/**
@@ -24768,11 +26304,14 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* which may be caused by a pending debounced event or because the input is waiting for a some
* future event.
*
- * If you have an input that uses `ng-model-options` to set up debounced events or events such
- * as blur you can have a situation where there is a period when the `$viewValue`
- * is out of synch with the ngModel's `$modelValue`.
+ * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
+ * depend on special events such as blur, you can have a situation where there is a period when
+ * the `$viewValue` is out of sync with the ngModel's `$modelValue`.
+ *
+ * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
+ * and reset the input to the last committed view value.
*
- * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
+ * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue`
* programmatically before these debounced/future events have resolved/occurred, because Angular's
* dirty checking mechanism is not able to tell whether the model has actually changed or not.
*
@@ -24785,39 +26324,63 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* angular.module('cancel-update-example', [])
*
* .controller('CancelUpdateController', ['$scope', function($scope) {
- * $scope.resetWithCancel = function(e) {
- * if (e.keyCode == 27) {
- * $scope.myForm.myInput1.$rollbackViewValue();
- * $scope.myValue = '';
- * }
- * };
- * $scope.resetWithoutCancel = function(e) {
+ * $scope.model = {};
+ *
+ * $scope.setEmpty = function(e, value, rollback) {
* if (e.keyCode == 27) {
- * $scope.myValue = '';
+ * e.preventDefault();
+ * if (rollback) {
+ * $scope.myForm[value].$rollbackViewValue();
+ * }
+ * $scope.model[value] = '';
* }
* };
* }]);
* </file>
* <file name="index.html">
* <div ng-controller="CancelUpdateController">
- * <p>Try typing something in each input. See that the model only updates when you
- * blur off the input.
- * </p>
- * <p>Now see what happens if you start typing then press the Escape key</p>
+ * <p>Both of these inputs are only updated if they are blurred. Hitting escape should
+ * empty them. Follow these steps and observe the difference:</p>
+ * <ol>
+ * <li>Type something in the input. You will see that the model is not yet updated</li>
+ * <li>Press the Escape key.
+ * <ol>
+ * <li> In the first example, nothing happens, because the model is already '', and no
+ * update is detected. If you blur the input, the model will be set to the current view.
+ * </li>
+ * <li> In the second example, the pending update is cancelled, and the input is set back
+ * to the last committed view value (''). Blurring the input does nothing.
+ * </li>
+ * </ol>
+ * </li>
+ * </ol>
*
* <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
- * <p id="inputDescription1">With $rollbackViewValue()</p>
- * <input name="myInput1" aria-describedby="inputDescription1" ng-model="myValue"
- * ng-keydown="resetWithCancel($event)"><br/>
- * myValue: "{{ myValue }}"
- *
- * <p id="inputDescription2">Without $rollbackViewValue()</p>
- * <input name="myInput2" aria-describedby="inputDescription2" ng-model="myValue"
- * ng-keydown="resetWithoutCancel($event)"><br/>
- * myValue: "{{ myValue }}"
+ * <div>
+ * <p id="inputDescription1">Without $rollbackViewValue():</p>
+ * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1"
+ * ng-keydown="setEmpty($event, 'value1')">
+ * value1: "{{ model.value1 }}"
+ * </div>
+ *
+ * <div>
+ * <p id="inputDescription2">With $rollbackViewValue():</p>
+ * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2"
+ * ng-keydown="setEmpty($event, 'value2', true)">
+ * value2: "{{ model.value2 }}"
+ * </div>
* </form>
* </div>
* </file>
+ <file name="style.css">
+ div {
+ display: table-cell;
+ }
+ div:nth-child(1) {
+ padding-right: 30px;
+ }
+
+ </file>
* </example>
*/
this.$rollbackViewValue = function() {
@@ -24891,7 +26454,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function processParseErrors() {
var errorKey = ctrl.$$parserName || 'parse';
- if (parserValid === undefined) {
+ if (isUndefined(parserValid)) {
setValidity(errorKey, null);
} else {
if (!parserValid) {
@@ -24931,13 +26494,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
forEach(ctrl.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
- throw $ngModelMinErr("$asyncValidators",
+ throw ngModelMinErr('nopromise',
"Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
}
setValidity(name, undefined);
validatorPromises.push(promise.then(function() {
setValidity(name, true);
- }, function(error) {
+ }, function() {
allValid = false;
setValidity(name, false);
}));
@@ -24987,6 +26550,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
@@ -25061,37 +26625,47 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @description
* Update the view value.
*
- * This method should be called when an input directive want to change the view value; typically,
- * this is done from within a DOM event handler.
- *
- * For example {@link ng.directive:input input} calls it when the value of the input changes and
- * {@link ng.directive:select select} calls it when an option is selected.
- *
- * If the new `value` is an object (rather than a string or a number), we should make a copy of the
- * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep
- * watch of objects, it only looks for a change of identity. If you only change the property of
- * the object then ngModel will not realise that the object has changed and will not invoke the
- * `$parsers` and `$validators` pipelines.
+ * This method should be called when a control wants to change the view value; typically,
+ * this is done from within a DOM event handler. For example, the {@link ng.directive:input input}
+ * directive calls it when the value of the input changes and {@link ng.directive:select select}
+ * calls it when an option is selected.
*
- * For this reason, you should not change properties of the copy once it has been passed to
- * `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly.
- *
- * When this method is called, the new `value` will be staged for committing through the `$parsers`
+ * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
* and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
* value sent directly for processing, finally to be applied to `$modelValue` and then the
- * **expression** specified in the `ng-model` attribute.
- *
- * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
+ * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners,
+ * in the `$viewChangeListeners` list, are called.
*
* In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
* and the `default` trigger is not listed, all those actions will remain pending until one of the
* `updateOn` events is triggered on the DOM element.
* All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
* directive is used with a custom debounce for this particular event.
+ * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce`
+ * is specified, once the timer runs out.
+ *
+ * When used with standard inputs, the view value will always be a string (which is in some cases
+ * parsed into another type, such as a `Date` object for `input[date]`.)
+ * However, custom controls might also pass objects to this method. In this case, we should make
+ * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
+ * perform a deep watch of objects, it only looks for a change of identity. If you only change
+ * the property of the object then ngModel will not realize that the object has changed and
+ * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
+ * not change properties of the copy once it has been passed to `$setViewValue`.
+ * Otherwise you may cause the model value on the scope to change incorrectly.
+ *
+ * <div class="alert alert-info">
+ * In any case, the value passed to the method should always reflect the current value
+ * of the control. For example, if you are calling `$setViewValue` for an input element,
+ * you should pass the input DOM value. Otherwise, the control and the scope model become
+ * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change
+ * the control's DOM value in any way. If we want to change the control's DOM value
+ * programmatically, we should update the `ngModel` scope expression. Its new value will be
+ * picked up by the model controller, which will run it through the `$formatters`, `$render` it
+ * to update the DOM, and finally call `$validate` on it.
+ * </div>
*
- * Note that calling this function does not trigger a `$digest`.
- *
- * @param {string} value Value from the view.
+ * @param {*} value value from the view.
* @param {string} trigger Event that triggered the update.
*/
this.$setViewValue = function(value, trigger) {
@@ -25159,6 +26733,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
@@ -25189,7 +26764,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* require.
* - Providing validation behavior (i.e. required, number, email, url).
* - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
+ * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
+ * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
* - Registering the control with its parent {@link ng.directive:form form}.
*
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
@@ -25217,6 +26793,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - {@link ng.directive:select select}
* - {@link ng.directive:textarea textarea}
*
+ * # Complex Models (objects or collections)
+ *
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the
+ * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered.
+ *
+ * The model must be assigned an entirely new object or collection before a re-rendering will occur.
+ *
+ * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression
+ * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or
+ * if the select is given the `multiple` attribute.
+ *
+ * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the
+ * first level of the object (or only changing the properties of an item in the collection if it's an array) will still
+ * not trigger a re-rendering of the model.
+ *
* # CSS classes
* The following CSS classes are added and removed on the associated input/select/textarea element
* depending on the validity of the model.
@@ -25230,13 +26822,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - `ng-touched`: the control has been blurred
* - `ng-untouched`: the control hasn't been blurred
* - `ng-pending`: any `$asyncValidators` are unfulfilled
+ * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
+ * by the {@link ngModel.NgModelController#$isEmpty} method
+ * - `ng-not-empty`: the view contains a non-empty value
*
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
* ## Animation Hooks
*
* Animations within models are triggered when any of the associated CSS classes are added and removed
- * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
+ * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
* `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
* The animations that are triggered within ngModel are similar to how they work in ngClass and
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
@@ -25268,7 +26863,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
</script>
<style>
.my-input {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: transparent;
}
@@ -25355,7 +26949,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || nullFormCtrl;
+ formCtrl = ctrls[1] || modelCtrl.$$parentForm;
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
@@ -25364,12 +26958,12 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
- formCtrl.$$renameControl(modelCtrl, newValue);
+ modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
}
});
scope.$on('$destroy', function() {
- formCtrl.$removeControl(modelCtrl);
+ modelCtrl.$$parentForm.$removeControl(modelCtrl);
});
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
@@ -25380,7 +26974,7 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
});
}
- element.on('blur', function(ev) {
+ element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
@@ -25461,12 +27055,13 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
</label><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
+ <pre>user.data = <span ng-bind="user.data"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('optionsExample', [])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'say', data: '' };
+ $scope.user = { name: 'John', data: '' };
$scope.cancel = function(e) {
if (e.keyCode == 27) {
@@ -25481,20 +27076,20 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
var other = element(by.model('user.data'));
it('should allow custom events', function() {
- input.sendKeys(' hello');
+ input.sendKeys(' Doe');
input.click();
- expect(model.getText()).toEqual('say');
+ expect(model.getText()).toEqual('John');
other.click();
- expect(model.getText()).toEqual('say hello');
+ expect(model.getText()).toEqual('John Doe');
});
it('should $rollbackViewValue when model changes', function() {
- input.sendKeys(' hello');
- expect(input.getAttribute('value')).toEqual('say hello');
+ input.sendKeys(' Doe');
+ expect(input.getAttribute('value')).toEqual('John Doe');
input.sendKeys(protractor.Key.ESCAPE);
- expect(input.getAttribute('value')).toEqual('say');
+ expect(input.getAttribute('value')).toEqual('John');
other.click();
- expect(model.getText()).toEqual('say');
+ expect(model.getText()).toEqual('John');
});
</file>
</example>
@@ -25520,7 +27115,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
<file name="app.js">
angular.module('optionsExample', [])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'say' };
+ $scope.user = { name: 'Igor' };
}]);
</file>
</example>
@@ -25564,7 +27159,7 @@ var ngModelOptionsDirective = function() {
var that = this;
this.$options = copy($scope.$eval($attrs.ngModelOptions));
// Allow adding/overriding bound events
- if (this.$options.updateOn !== undefined) {
+ if (isDefined(this.$options.updateOn)) {
this.$options.updateOnDefault = false;
// extract "default" pseudo-event from list of events that can trigger a model update
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
@@ -25587,7 +27182,6 @@ function addSetValidityMethod(context) {
classCache = {},
set = context.set,
unset = context.unset,
- parentForm = context.parentForm,
$animate = context.$animate;
classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
@@ -25595,7 +27189,7 @@ function addSetValidityMethod(context) {
ctrl.$setValidity = setValidity;
function setValidity(validationErrorKey, state, controller) {
- if (state === undefined) {
+ if (isUndefined(state)) {
createAndSet('$pending', validationErrorKey, controller);
} else {
unsetAndCleanup('$pending', validationErrorKey, controller);
@@ -25639,7 +27233,7 @@ function addSetValidityMethod(context) {
}
toggleValidationCss(validationErrorKey, combinedState);
- parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
+ ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
}
function createAndSet(name, value, controller) {
@@ -25754,19 +27348,27 @@ var ngOptionsMinErr = minErr('ngOptions');
*
* ## Complex Models (objects or collections)
*
- * **Note:** By default, `ngModel` watches the model by reference, not value. This is important when
- * binding any input directive to a model that is an object or a collection.
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding the select to a model that is an object or a collection.
*
- * Since this is a common situation for `ngOptions` the directive additionally watches the model using
- * `$watchCollection` when the select has the `multiple` attribute or when there is a `track by` clause in
- * the options expression. This allows ngOptions to trigger a re-rendering of the options even if the actual
- * object/collection has not changed identity but only a property on the object or an item in the collection
- * changes.
+ * One issue occurs if you want to preselect an option. For example, if you set
+ * the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection,
+ * because the objects are not identical. So by default, you should always reference the item in your collection
+ * for preselections, e.g.: `$scope.selected = $scope.collection[3]`.
*
- * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
- * if the model is an array). This means that changing a property deeper inside the object/collection that the
- * first level will not trigger a re-rendering.
+ * Another solution is to use a `track by` clause, because then `ngOptions` will track the identity
+ * of the item not by reference, but by the result of the `track by` expression. For example, if your
+ * collection items have an id property, you would `track by item.id`.
*
+ * A different issue with objects or collections is that ngModel won't detect if an object property or
+ * a collection item changes. For that reason, `ngOptions` additionally watches the model using
+ * `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute.
+ * This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection
+ * has not changed identity, but only a property on the object or an item in the collection changes.
+ *
+ * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
+ * if the model is an array). This means that changing a property deeper than the first level inside the
+ * object/collection will not trigger a re-rendering.
*
* ## `select` **`as`**
*
@@ -25779,17 +27381,13 @@ var ngOptionsMinErr = minErr('ngOptions');
* ### `select` **`as`** and **`track by`**
*
* <div class="alert alert-warning">
- * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
+ * Be careful when using `select` **`as`** and **`track by`** in the same expression.
* </div>
*
- * Consider the following example:
- *
- * ```html
- * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
- * ```
+ * Given this array of items on the $scope:
*
* ```js
- * $scope.values = [{
+ * $scope.items = [{
* id: 1,
* label: 'aLabel',
* subItem: { name: 'aSubItem' }
@@ -25798,20 +27396,33 @@ var ngOptionsMinErr = minErr('ngOptions');
* label: 'bLabel',
* subItem: { name: 'bSubItem' }
* }];
+ * ```
*
- * $scope.selected = { name: 'aSubItem' };
+ * This will work:
+ *
+ * ```html
+ * <select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select>
* ```
+ * ```js
+ * $scope.selected = $scope.items[0];
+ * ```
+ *
+ * but this will not work:
*
- * With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
- * of the data source (to `item` in this example). To calculate whether an element is selected, we do the
- * following:
+ * ```html
+ * <select ng-options="item.subItem as item.label for item in items track by item.id" ng-model="selected"></select>
+ * ```
+ * ```js
+ * $scope.selected = $scope.items[0].subItem;
+ * ```
*
- * 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
- * 2. Apply **`track by`** to the already selected value in `ngModel`.
- * In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
- * value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
- * a wrong object, the selected element can't be found, `<select>` is always reset to the "not
- * selected" option.
+ * In both examples, the **`track by`** expression is applied successfully to each `item` in the
+ * `items` array. Because the selected option has been set programmatically in the controller, the
+ * **`track by`** expression is also applied to the `ngModel` value. In the first example, the
+ * `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with
+ * no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`**
+ * expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
+ * is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
*
* @param {string} ngModel Assignable angular expression to data-bind to.
@@ -26046,8 +27657,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];
- var locals = getLocals(optionValues[key], key);
- var selectValue = getTrackByValueFn(optionValues[key], locals);
+ var locals = getLocals(value, key);
+ var selectValue = getTrackByValueFn(value, locals);
watchedArray.push(selectValue);
// Only need to watch the displayFn if there is a specific label expression
@@ -26113,17 +27724,10 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var optionTemplate = document.createElement('option'),
optGroupTemplate = document.createElement('optgroup');
- return {
- restrict: 'A',
- terminal: true,
- require: ['select', '?ngModel'],
- link: function(scope, selectElement, attr, ctrls) {
-
- // if ngModel is not defined, we don't need to do anything
- var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
+ function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
var multiple = attr.multiple;
// The emptyOption allows the application developer to provide their own custom "empty"
@@ -26172,7 +27776,6 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
unknownOption.remove();
};
-
// Update the controller methods for multiple selectable options
if (!multiple) {
@@ -26180,14 +27783,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var option = options.getOptionFromViewValue(value);
if (option && !option.disabled) {
+ // Don't update the option when it is already selected.
+ // For example, the browser will select the first option by default. In that case,
+ // most properties are set automatically - except the `selected` attribute, which we
+ // set always
+
if (selectElement[0].value !== option.selectValue) {
removeUnknownOption();
removeEmptyOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
- option.element.setAttribute('selected', 'selected');
}
+
+ option.element.setAttribute('selected', 'selected');
} else {
if (value === null || providedEmptyOption) {
removeUnknownOption();
@@ -26247,7 +27856,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
forEach(selectedValues, function(value) {
var option = options.selectValueMap[value];
- if (!option.disabled) selections.push(options.getViewValueFromOption(option));
+ if (option && !option.disabled) selections.push(options.getViewValueFromOption(option));
});
return selections;
@@ -26300,11 +27909,16 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
function updateOptionElement(option, element) {
option.element = element;
element.disabled = option.disabled;
- if (option.value !== element.value) element.value = option.selectValue;
+ // NOTE: The label must be set before the value, otherwise IE10/11/EDGE create unresponsive
+ // selects in certain circumstances when multiple selects are next to each other and display
+ // the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
+ // See https://github.com/angular/angular.js/issues/11314 for more info.
+ // This is unfortunately untestable with unit / e2e tests
if (option.label !== element.label) {
element.label = option.label;
element.textContent = option.label;
}
+ if (option.value !== element.value) element.value = option.selectValue;
}
function addOrReuseElement(parent, current, type, templateElement) {
@@ -26342,10 +27956,15 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var emptyOption_ = emptyOption && emptyOption[0];
var unknownOption_ = unknownOption && unknownOption[0];
+ // We cannot rely on the extracted empty option being the same as the compiled empty option,
+ // because the compiled empty option might have been replaced by a comment because
+ // it had an "element" transclusion directive on it (such as ngIf)
if (emptyOption_ || unknownOption_) {
while (current &&
(current === emptyOption_ ||
- current === unknownOption_)) {
+ current === unknownOption_ ||
+ current.nodeType === NODE_TYPE_COMMENT ||
+ (nodeName_(current) === 'option' && current.value === ''))) {
current = current.nextSibling;
}
}
@@ -26374,7 +27993,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var groupElement;
var optionElement;
- if (option.group) {
+ if (isDefined(option.group)) {
// This option is to live in a group
// See if we have already created this group
@@ -26435,14 +28054,28 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// Check to see if the value has changed due to the update to the options
if (!ngModelCtrl.$isEmpty(previousValue)) {
var nextValue = selectCtrl.readValue();
- if (ngOptions.trackBy ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
+ var isNotPrimitive = ngOptions.trackBy || multiple;
+ if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
ngModelCtrl.$setViewValue(nextValue);
ngModelCtrl.$render();
}
}
}
+ }
+ return {
+ restrict: 'A',
+ terminal: true,
+ require: ['select', 'ngModel'],
+ link: {
+ pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
+ // Deactivate the SelectController.register method to prevent
+ // option directives from accidentally registering themselves
+ // (and unwanted $destroy handlers etc.)
+ ctrls[0].registerOption = noop;
+ },
+ post: ngOptionsPostLink
}
};
}];
@@ -26662,7 +28295,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
// If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
- // In JS `NaN !== NaN`, so we have to exlicitly check.
+ // In JS `NaN !== NaN`, so we have to explicitly check.
if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
@@ -26707,8 +28340,10 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). |
* | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). |
*
- * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
- * This may be useful when, for instance, nesting ngRepeats.
+ * <div class="alert alert-info">
+ * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
+ * This may be useful when, for instance, nesting ngRepeats.
+ * </div>
*
*
* # Iterating over object properties
@@ -26720,32 +28355,44 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * You need to be aware that the JavaScript specification does not define the order of keys
- * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive
- * used to sort the keys alphabetically.)
+ * However, there are a limitations compared to array iteration:
+ *
+ * - The JavaScript specification does not define the order of keys
+ * returned for an object, so Angular relies on the order returned by the browser
+ * when running `for key in myObj`. Browsers generally follow the strategy of providing
+ * keys in the order in which they were defined, although there are exceptions when keys are deleted
+ * and reinstated. See the
+ * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
+ *
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
*
- * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
- * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
- * keys in the order in which they were defined, although there are exceptions when keys are deleted
- * and reinstated. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_issues
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw if used with one.
*
- * If this is not desired, the recommended workaround is to convert your object into an array
- * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
* do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
* or implement a `$watch` on the object yourself.
*
*
* # Tracking and Duplicates
*
- * When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM:
+ * `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
+ * the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
*
* * When an item is added, a new instance of the template is added to the DOM.
* * When an item is removed, its template instance is removed from the DOM.
* * When items are reordered, their respective templates are reordered in the DOM.
*
- * By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
- * there are duplicates, it is not possible to maintain a one-to-one mapping between collection
- * items and DOM elements.
+ * To minimize creation of DOM elements, `ngRepeat` uses a function
+ * to "keep track" of all items in the collection and their corresponding DOM elements.
+ * For example, if an item is added to the collection, ngRepeat will know that all other items
+ * already have DOM elements, and will not re-render them.
+ *
+ * The default tracking function (which tracks items by their identity) does not allow
+ * duplicate items in arrays. This is because when there are duplicates, it is not possible
+ * to maintain a one-to-one mapping between collection items and DOM elements.
*
* If you do need to repeat duplicate items, you can substitute the default tracking behavior
* with your own using the `track by` expression.
@@ -26758,7 +28405,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* </div>
* ```
*
- * You may use arbitrary expressions in `track by`, including references to custom functions
+ * You may also use arbitrary expressions in `track by`, including references to custom functions
* on the scope:
* ```html
* <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
@@ -26766,10 +28413,14 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* </div>
* ```
*
- * If you are working with objects that have an identifier property, you can track
+ * <div class="alert alert-success">
+ * If you are working with objects that have an identifier property, you should track
* by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
* will not have to rebuild the DOM elements for items it has already rendered, even if the
- * JavaScript objects in the collection have been substituted for new ones:
+ * JavaScript objects in the collection have been substituted for new ones. For large collections,
+ * this significantly improves rendering performance. If you don't have a unique identifier,
+ * `track by $index` can also provide a performance boost.
+ * </div>
* ```html
* <div ng-repeat="model in collection track by model.id">
* {{model.name}}
@@ -26838,11 +28489,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
*
* @animations
- * **.enter** - when a new item is added to the list or when an item is revealed after a filter
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter |
+ * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out |
+ * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
*
- * **.leave** - when an item is removed from the list or when an item is filtered out
- *
- * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
+ * See the example below for defining CSS animations with ngRepeat.
*
* @element ANY
* @scope
@@ -26896,22 +28549,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
*
* @example
- * This example initializes the scope to a list of names and
- * then uses `ngRepeat` to display every person:
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
+ * results by name. New (entering) and removed (leaving) items are animated.
+ <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true">
<file name="index.html">
- <div ng-init="friends = [
- {name:'John', age:25, gender:'boy'},
- {name:'Jessie', age:30, gender:'girl'},
- {name:'Johanna', age:28, gender:'girl'},
- {name:'Joy', age:15, gender:'girl'},
- {name:'Mary', age:28, gender:'girl'},
- {name:'Peter', age:95, gender:'boy'},
- {name:'Sebastian', age:50, gender:'boy'},
- {name:'Erika', age:27, gender:'girl'},
- {name:'Patrick', age:40, gender:'boy'},
- {name:'Samantha', age:60, gender:'girl'}
- ]">
+ <div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
@@ -26924,6 +28566,22 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
</ul>
</div>
</file>
+ <file name="script.js">
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ $scope.friends = [
+ {name:'John', age:25, gender:'boy'},
+ {name:'Jessie', age:30, gender:'girl'},
+ {name:'Johanna', age:28, gender:'girl'},
+ {name:'Joy', age:15, gender:'girl'},
+ {name:'Mary', age:28, gender:'girl'},
+ {name:'Peter', age:95, gender:'boy'},
+ {name:'Sebastian', age:50, gender:'boy'},
+ {name:'Erika', age:27, gender:'girl'},
+ {name:'Patrick', age:40, gender:'boy'},
+ {name:'Samantha', age:60, gender:'girl'}
+ ];
+ });
+ </file>
<file name="animations.css">
.example-animate-container {
background:white;
@@ -26934,7 +28592,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
.animate-repeat {
- line-height:40px;
+ line-height:30px;
list-style:none;
box-sizing:border-box;
}
@@ -26942,7 +28600,6 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
@@ -26957,7 +28614,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
- max-height:40px;
+ max-height:30px;
}
</file>
<file name="protractor.js" type="protractor">
@@ -26984,7 +28641,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
</file>
</example>
*/
-var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
+var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
@@ -27019,7 +28676,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
var expression = $attr.ngRepeat;
- var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
+ var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
@@ -27114,7 +28771,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
// if object, extract keys, in enumeration order, unsorted
collectionKeys = [];
for (var itemKey in collection) {
- if (collection.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
+ if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
collectionKeys.push(itemKey);
}
}
@@ -27183,7 +28840,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
if (getBlockStart(block) != nextNode) {
// existing item which got moved
- $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
+ $animate.move(getBlockNodes(block.clone), null, previousNode);
}
previousNode = getBlockEnd(block);
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
@@ -27195,8 +28852,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var endNode = ngRepeatEndComment.cloneNode(false);
clone[clone.length++] = endNode;
- // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper?
- $animate.enter(clone, null, jqLite(previousNode));
+ $animate.enter(clone, null, previousNode);
previousNode = endNode;
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
@@ -27299,12 +28955,14 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
* property to block during animation states--ngAnimate will handle the style toggling automatically for you.
*
* @animations
- * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible
- * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngShow` expression evaluates to a truthy value and just before contents are set to visible |
*
* @element ANY
* @param {expression} ngShow If the {@link guide/expression expression} is truthy
@@ -27339,9 +28997,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
background: white;
}
- .animate-show.ng-hide-add.ng-hide-add-active,
- .animate-show.ng-hide-remove.ng-hide-remove-active {
- -webkit-transition: all linear 0.5s;
+ .animate-show.ng-hide-add, .animate-show.ng-hide-remove {
transition: all linear 0.5s;
}
@@ -27465,12 +29121,15 @@ var ngShowDirective = ['$animate', function($animate) {
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display
* property to block during animation states--ngAnimate will handle the style toggling automatically for you.
*
* @animations
- * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden
- * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | after the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible |
+ *
*
* @element ANY
* @param {expression} ngHide If the {@link guide/expression expression} is truthy then
@@ -27498,7 +29157,6 @@ var ngShowDirective = ['$animate', function($animate) {
</file>
<file name="animations.css">
.animate-hide {
- -webkit-transition: all linear 0.5s;
transition: all linear 0.5s;
line-height: 20px;
opacity: 1;
@@ -27633,8 +29291,10 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
* </div>
* @animations
- * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container
- * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container |
+ * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM |
*
* @usage
*
@@ -27697,7 +29357,6 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
}
.animate-switch.ng-animate {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
position:absolute;
@@ -27734,7 +29393,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
</file>
</example>
*/
-var ngSwitchDirective = ['$animate', function($animate) {
+var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
return {
require: 'ngSwitch',
@@ -27775,7 +29434,7 @@ var ngSwitchDirective = ['$animate', function($animate) {
selectedTransclude.transclude(function(caseElement, selectedScope) {
selectedScopes.push(selectedScope);
var anchor = selectedTransclude.element;
- caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: ');
+ caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
var block = { clone: caseElement };
selectedElements.push(block);
@@ -27818,67 +29477,186 @@ var ngSwitchDefaultDirective = ngDirective({
* @description
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
*
- * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
+ * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
+ * as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
+ *
+ * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
+ * content of this element will be removed before the transcluded content is inserted.
+ * If the transcluded content is empty, the existing content is left intact. This lets you provide fallback content in the case
+ * that no transcluded content is provided.
*
* @element ANY
*
+ * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty
+ * or its value is the same as the name of the attribute then the default slot is used.
+ *
* @example
- <example module="transcludeExample">
- <file name="index.html">
- <script>
- angular.module('transcludeExample', [])
- .directive('pane', function(){
- return {
- restrict: 'E',
- transclude: true,
- scope: { title:'@' },
- template: '<div style="border: 1px solid black;">' +
- '<div style="background-color: gray">{{title}}</div>' +
- '<ng-transclude></ng-transclude>' +
- '</div>'
- };
- })
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.title = 'Lorem Ipsum';
- $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <input ng-model="title" aria-label="title"> <br/>
- <textarea ng-model="text" aria-label="text"></textarea> <br/>
- <pane title="{{title}}">{{text}}</pane>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should have transcluded', function() {
- var titleElement = element(by.model('title'));
- titleElement.clear();
- titleElement.sendKeys('TITLE');
- var textElement = element(by.model('text'));
- textElement.clear();
- textElement.sendKeys('TEXT');
- expect(element(by.binding('title')).getText()).toEqual('TITLE');
- expect(element(by.binding('text')).getText()).toEqual('TEXT');
- });
- </file>
- </example>
+ * ### Basic transclusion
+ * This example demonstrates basic transclusion of content into a component directive.
+ * <example name="simpleTranscludeExample" module="transcludeExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeExample', [])
+ * .directive('pane', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: { title:'@' },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div style="background-color: gray">{{title}}</div>' +
+ * '<ng-transclude></ng-transclude>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane title="{{title}}">{{text}}</pane>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.binding('title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * });
+ * </file>
+ * </example>
+ *
+ * @example
+ * ### Transclude fallback content
+ * This example shows how to use `NgTransclude` with fallback content, that
+ * is displayed if no transcluded content is provided.
+ *
+ * <example module="transcludeFallbackContentExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeFallbackContentExample', [])
+ * .directive('myButton', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: true,
+ * template: '<button style="cursor: pointer;">' +
+ * '<ng-transclude>' +
+ * '<b style="color: red;">Button1</b>' +
+ * '</ng-transclude>' +
+ * '</button>'
+ * };
+ * });
+ * </script>
+ * <!-- fallback button content -->
+ * <my-button id="fallback"></my-button>
+ * <!-- modified button content -->
+ * <my-button id="modified">
+ * <i style="color: green;">Button2</i>
+ * </my-button>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have different transclude element content', function() {
+ * expect(element(by.id('fallback')).getText()).toBe('Button1');
+ * expect(element(by.id('modified')).getText()).toBe('Button2');
+ * });
+ * </file>
+ * </example>
*
+ * @example
+ * ### Multi-slot transclusion
+ * This example demonstrates using multi-slot transclusion in a component directive.
+ * <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
+ * <file name="index.html">
+ * <style>
+ * .title, .footer {
+ * background-color: gray
+ * }
+ * </style>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane>
+ * <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
+ * <pane-body><p>{{text}}</p></pane-body>
+ * </pane>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('multiSlotTranscludeExample', [])
+ * .directive('pane', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: {
+ * 'title': '?paneTitle',
+ * 'body': 'paneBody',
+ * 'footer': '?paneFooter'
+ * },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div class="title" ng-transclude="title">Fallback Title</div>' +
+ * '<div ng-transclude="body"></div>' +
+ * '<div class="footer" ng-transclude="footer">Fallback Footer</div>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.link = "https://google.com";
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded the title and the body', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.css('.title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer');
+ * });
+ * </file>
+ * </example>
*/
+var ngTranscludeMinErr = minErr('ngTransclude');
var ngTranscludeDirective = ngDirective({
restrict: 'EAC',
link: function($scope, $element, $attrs, controller, $transclude) {
+
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ // If the attribute is of the form: `ng-transclude="ng-transclude"`
+ // then treat it like the default
+ $attrs.ngTransclude = '';
+ }
+
+ function ngTranscludeCloneAttachFn(clone) {
+ if (clone.length) {
+ $element.empty();
+ $element.append(clone);
+ }
+ }
+
if (!$transclude) {
- throw minErr('ngTransclude')('orphan',
+ throw ngTranscludeMinErr('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}
- $transclude(function(clone) {
- $element.empty();
- $element.append(clone);
- });
+ // If there is no slot name defined or the slot name is not optional
+ // then transclude the slot
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
}
});
@@ -27932,6 +29710,15 @@ var scriptDirective = ['$templateCache', function($templateCache) {
var noopNgModelController = { $setViewValue: noop, $render: noop };
+function chromeHack(optionElement) {
+ // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
+ // Adding an <option selected="selected"> element to a <select required="required"> should
+ // automatically select the new element
+ if (optionElement[0].hasAttribute('selected')) {
+ optionElement[0].selected = true;
+ }
+}
+
/**
* @ngdoc type
* @name select.SelectController
@@ -27941,7 +29728,7 @@ var noopNgModelController = { $setViewValue: noop, $render: noop };
* added `<option>` elements, perhaps by an `ngRepeat` directive.
*/
var SelectController =
- ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+ ['$element', '$scope', function($element, $scope) {
var self = this,
optionsMap = new HashMap();
@@ -28001,12 +29788,17 @@ var SelectController =
// Tell the select control that an option, with the given value, has been added
self.addOption = function(value, element) {
+ // Skip comment nodes, as they only pollute the `optionsMap`
+ if (element[0].nodeType === NODE_TYPE_COMMENT) return;
+
assertNotHasOwnProperty(value, '"option value"');
if (value === '') {
self.emptyOption = element;
}
var count = optionsMap.get(value) || 0;
optionsMap.put(value, count + 1);
+ self.ngModelCtrl.$render();
+ chromeHack(element);
};
// Tell the select control that an option, with the given value, has been removed
@@ -28028,6 +29820,39 @@ var SelectController =
self.hasOption = function(value) {
return !!optionsMap.get(value);
};
+
+
+ self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
+
+ if (interpolateValueFn) {
+ // The value attribute is interpolated
+ var oldVal;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ if (isDefined(oldVal)) {
+ self.removeOption(oldVal);
+ }
+ oldVal = newVal;
+ self.addOption(newVal, optionElement);
+ });
+ } else if (interpolateTextFn) {
+ // The text content is interpolated
+ optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
+ optionAttrs.$set('value', newVal);
+ if (oldVal !== newVal) {
+ self.removeOption(oldVal);
+ }
+ self.addOption(newVal, optionElement);
+ });
+ } else {
+ // The value attribute is static
+ self.addOption(optionAttrs.value, optionElement);
+ }
+
+ optionElement.on('$destroy', function() {
+ self.removeOption(optionAttrs.value);
+ self.ngModelCtrl.$render();
+ });
+ };
}];
/**
@@ -28038,31 +29863,164 @@ var SelectController =
* @description
* HTML `SELECT` element with angular data-binding.
*
- * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits such as reducing
- * memory and increasing speed by not creating a new scope for each repeated instance, as well as providing
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression.
+ * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
+ * between the scope and the `<select>` control (including setting default values).
+ * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or
+ * {@link ngOptions `ngOptions`} directives.
*
- * When an item in the `<select>` menu is selected, the array element or object property
- * represented by the selected option will be bound to the model identified by the `ngModel`
- * directive.
+ * When an item in the `<select>` menu is selected, the value of the selected option will be bound
+ * to the model identified by the `ngModel` directive. With static or repeated options, this is
+ * the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
+ * If you want dynamic value attributes, you can use interpolation inside the value attribute.
*
- * If the viewValue contains a value that doesn't match any of the options then the control
- * will automatically add an "unknown" option, which it then removes when this is resolved.
+ * <div class="alert alert-warning">
+ * Note that the value of a `select` directive used without `ngOptions` is always a string.
+ * When the model needs to be bound to a non-string value, you must either explicitly convert it
+ * using a directive (see example below) or use `ngOptions` to specify the set of options.
+ * This is because an option element can only be bound to string values at present.
+ * </div>
+ *
+ * If the viewValue of `ngModel` does not match any of the options, then the control
+ * will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
*
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
* <div class="alert alert-info">
- * The value of a `select` directive used without `ngOptions` is always a string.
- * When the model needs to be bound to a non-string value, you must either explictly convert it
- * using a directive (see example below) or use `ngOptions` to specify the set of options.
- * This is because an option element can only be bound to string values at present.
+ * In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
+ * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression, and additionally in reducing memory and increasing speed by not creating
+ * a new scope for each repeated instance.
+ * </div>
+ *
+ *
+ * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} multiple Allows multiple options to be selected. The selected values will be
+ * bound to the model as an array.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds required attribute and required validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
+ * when you want to data-bind to the required attribute.
+ * @param {string=} ngChange Angular expression to be executed when selected option(s) changes due to user
+ * interaction with the select element.
+ * @param {string=} ngOptions sets the options that the select is populated with and defines what is
+ * set on the model on selection. See {@link ngOptions `ngOptions`}.
+ *
+ * @example
+ * ### Simple `select` elements with static options
+ *
+ * <example name="static-select" module="staticSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="singleSelect"> Single select: </label><br>
+ * <select name="singleSelect" ng-model="data.singleSelect">
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ *
+ * <label for="singleSelect"> Single select with "not selected" option and dynamic option values: </label><br>
+ * <select name="singleSelect" id="singleSelect" ng-model="data.singleSelect">
+ * <option value="">---Please select---</option> <!-- not selected / blank option -->
+ * <option value="{{data.option1}}">Option 1</option> <!-- interpolation -->
+ * <option value="option-2">Option 2</option>
+ * </select><br>
+ * <button ng-click="forceUnknownOption()">Force unknown option</button><br>
+ * <tt>singleSelect = {{data.singleSelect}}</tt>
+ *
+ * <hr>
+ * <label for="multipleSelect"> Multiple select: </label><br>
+ * <select name="multipleSelect" id="multipleSelect" ng-model="data.multipleSelect" multiple>
+ * <option value="option-1">Option 1</option>
+ * <option value="option-2">Option 2</option>
+ * <option value="option-3">Option 3</option>
+ * </select><br>
+ * <tt>multipleSelect = {{data.multipleSelect}}</tt><br/>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * singleSelect: null,
+ * multipleSelect: [],
+ * option1: 'option-1',
+ * };
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.data.singleSelect = 'nonsense';
+ * };
+ * }]);
+ * </file>
+ *</example>
+ *
+ * ### Using `ngRepeat` to generate `select` options
+ * <example name="ngrepeat-select" module="ngrepeatSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="repeatSelect"> Repeat select: </label>
+ * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect">
+ * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
+ * </select>
+ * </form>
+ * <hr>
+ * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/>
* </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('ngrepeatSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * repeatSelect: null,
+ * availableOptions: [
+ * {id: '1', name: 'Option A'},
+ * {id: '2', name: 'Option B'},
+ * {id: '3', name: 'Option C'}
+ * ],
+ * };
+ * }]);
+ * </file>
+ *</example>
+ *
*
- * ### Example (binding `select` to a non-string value)
+ * ### Using `select` with `ngOptions` and setting a default value
+ * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
+ *
+ * <example name="select-with-default-values" module="defaultValueSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="mySelect">Make a choice:</label>
+ * <select name="mySelect" id="mySelect"
+ * ng-options="option.name for option in data.availableOptions track by option.id"
+ * ng-model="data.selectedOption"></select>
+ * </form>
+ * <hr>
+ * <tt>option = {{data.selectedOption}}</tt><br/>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('defaultValueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * availableOptions: [
+ * {id: '1', name: 'Option A'},
+ * {id: '2', name: 'Option B'},
+ * {id: '3', name: 'Option C'}
+ * ],
+ * selectedOption: {id: '3', name: 'Option C'} //This sets the default value of the select in the ui
+ * };
+ * }]);
+ * </file>
+ *</example>
+ *
+ *
+ * ### Binding `select` to a non-string value via `ngModel` parsing / formatting
*
* <example name="select-with-non-string-options" module="nonStringSelect">
* <file name="index.html">
@@ -28107,7 +30065,14 @@ var selectDirective = function() {
restrict: 'E',
require: ['select', '?ngModel'],
controller: SelectController,
- link: function(scope, element, attr, ctrls) {
+ priority: 1,
+ link: {
+ pre: selectPreLink,
+ post: selectPostLink
+ }
+ };
+
+ function selectPreLink(scope, element, attr, ctrls) {
// if ngModel is not defined, we don't need to do anything
var ngModelCtrl = ctrls[1];
@@ -28117,13 +30082,6 @@ var selectDirective = function() {
selectCtrl.ngModelCtrl = ngModelCtrl;
- // We delegate rendering to the `writeValue` method, which can be changed
- // if the select can have multiple selected values or if the options are being
- // generated by `ngOptions`
- ngModelCtrl.$render = function() {
- selectCtrl.writeValue(ngModelCtrl.$viewValue);
- };
-
// When the selected item(s) changes we delegate getting the value of the select control
// to the `readValue` method, which can be changed if the select can have multiple
// selected values or if the options are being generated by `ngOptions`
@@ -28177,7 +30135,23 @@ var selectDirective = function() {
}
}
- };
+
+ function selectPostLink(scope, element, attrs, ctrls) {
+ // if ngModel is not defined, we don't need to do anything
+ var ngModelCtrl = ctrls[1];
+ if (!ngModelCtrl) return;
+
+ var selectCtrl = ctrls[0];
+
+ // We delegate rendering to the `writeValue` method, which can be changed
+ // if the select can have multiple selected values or if the options are being
+ // generated by `ngOptions`.
+ // This must be done in the postLink fn to prevent $render to be called before
+ // all nodes have been linked correctly.
+ ngModelCtrl.$render = function() {
+ selectCtrl.writeValue(ngModelCtrl.$viewValue);
+ };
+ }
};
@@ -28185,32 +30159,23 @@ var selectDirective = function() {
// of dynamically created (and destroyed) option elements to their containing select
// directive via its controller.
var optionDirective = ['$interpolate', function($interpolate) {
-
- function chromeHack(optionElement) {
- // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
- // Adding an <option selected="selected"> element to a <select required="required"> should
- // automatically select the new element
- if (optionElement[0].hasAttribute('selected')) {
- optionElement[0].selected = true;
- }
- }
-
return {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
-
- // If the value attribute is not defined then we fall back to the
- // text content of the option element, which may be interpolated
- if (isUndefined(attr.value)) {
- var interpolateFn = $interpolate(element.text(), true);
- if (!interpolateFn) {
+ if (isDefined(attr.value)) {
+ // If the value attribute is defined, check if it contains an interpolation
+ var interpolateValueFn = $interpolate(attr.value, true);
+ } else {
+ // If the value attribute is not defined then we fall back to the
+ // text content of the option element, which may be interpolated
+ var interpolateTextFn = $interpolate(element.text(), true);
+ if (!interpolateTextFn) {
attr.$set('value', element.text());
}
}
return function(scope, element, attr) {
-
// This is an optimization over using ^^ since we don't want to have to search
// all the way to the root of the DOM for every single option element
var selectCtrlName = '$selectController',
@@ -28218,30 +30183,8 @@ var optionDirective = ['$interpolate', function($interpolate) {
selectCtrl = parent.data(selectCtrlName) ||
parent.parent().data(selectCtrlName); // in case we are in optgroup
- // Only update trigger option updates if this is an option within a `select`
- // that also has `ngModel` attached
- if (selectCtrl && selectCtrl.ngModelCtrl) {
-
- if (interpolateFn) {
- scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
- attr.$set('value', newVal);
- if (oldVal !== newVal) {
- selectCtrl.removeOption(oldVal);
- }
- selectCtrl.addOption(newVal, element);
- selectCtrl.ngModelCtrl.$render();
- chromeHack(element);
- });
- } else {
- selectCtrl.addOption(attr.value, element);
- selectCtrl.ngModelCtrl.$render();
- chromeHack(element);
- }
-
- element.on('$destroy', function() {
- selectCtrl.removeOption(attr.value);
- selectCtrl.ngModelCtrl.$render();
- });
+ if (selectCtrl) {
+ selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn);
}
};
}
@@ -28253,6 +30196,64 @@ var styleDirective = valueFn({
terminal: false
});
+/**
+ * @ngdoc directive
+ * @name ngRequired
+ *
+ * @description
+ *
+ * ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be
+ * applied to custom controls.
+ *
+ * The directive sets the `required` attribute on the element if the Angular expression inside
+ * `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we
+ * cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide}
+ * for more info.
+ *
+ * The validator will set the `required` error key to true if the `required` attribute is set and
+ * calling {@link ngModel.NgModelController#$isEmpty `NgModelController.$isEmpty`} with the
+ * {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} returns `true`. For example, the
+ * `$isEmpty()` implementation for `input[text]` checks the length of the `$viewValue`. When developing
+ * custom controls, `$isEmpty()` can be overwritten to account for a $viewValue that is not string-based.
+ *
+ * @example
+ * <example name="ngRequiredDirective" module="ngRequiredExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngRequiredExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.required = true;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="required">Toggle required: </label>
+ * <input type="checkbox" ng-model="required" id="required" />
+ * <br>
+ * <label for="input">This input must be filled if `required` is true: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-required="required" /><br>
+ * <hr>
+ * required error set? = <code>{{form.input.$error.required}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var required = element(by.binding('form.input.$error.required'));
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should set the required error', function() {
+ expect(required.getText()).toContain('true');
+
+ input.sendKeys('123');
+ expect(required.getText()).not.toContain('true');
+ expect(model.getText()).toContain('123');
+ });
+ * </file>
+ * </example>
+ */
var requiredDirective = function() {
return {
restrict: 'A',
@@ -28272,7 +30273,81 @@ var requiredDirective = function() {
};
};
+/**
+ * @ngdoc directive
+ * @name ngPattern
+ *
+ * @description
+ *
+ * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * does not match a RegExp which is obtained by evaluating the Angular expression given in the
+ * `ngPattern` attribute value:
+ * * If the expression evaluates to a RegExp object, then this is used directly.
+ * * If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
+ * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ *
+ * <div class="alert alert-info">
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * </div>
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `pattern` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is
+ * not available.
+ * </li>
+ * <li>
+ * The `ngPattern` attribute must be an expression, while the `pattern` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngPatternDirective" module="ngPatternExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngPatternExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.regex = '\\d+';
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="regex">Set a pattern (regex string): </label>
+ * <input type="text" ng-model="regex" id="regex" />
+ * <br>
+ * <label for="input">This input is restricted by the current pattern: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-pattern="regex" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default pattern', function() {
+ input.sendKeys('aaa');
+ expect(model.getText()).not.toContain('aaa');
+ input.clear().then(function() {
+ input.sendKeys('123');
+ expect(model.getText()).toContain('123');
+ });
+ });
+ * </file>
+ * </example>
+ */
var patternDirective = function() {
return {
restrict: 'A',
@@ -28296,14 +30371,80 @@ var patternDirective = function() {
ctrl.$validate();
});
- ctrl.$validators.pattern = function(value) {
- return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value);
+ ctrl.$validators.pattern = function(modelValue, viewValue) {
+ // HTML5 pattern constraint validates the input value, so we validate the viewValue
+ return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
};
}
};
};
+/**
+ * @ngdoc directive
+ * @name ngMaxlength
+ *
+ * @description
+ *
+ * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is longer than the integer obtained by evaluating the Angular expression given in the
+ * `ngMaxlength` attribute value.
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ * </li>
+ * <li>
+ * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngMaxlengthDirective" module="ngMaxlengthExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngMaxlengthExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.maxlength = 5;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="maxlength">Set a maxlength: </label>
+ * <input type="number" ng-model="maxlength" id="maxlength" />
+ * <br>
+ * <label for="input">This input is restricted by the current maxlength: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-maxlength="maxlength" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+ it('should validate the input with the default maxlength', function() {
+ input.sendKeys('abcdef');
+ expect(model.getText()).not.toContain('abcdef');
+
+ input.clear().then(function() {
+ input.sendKeys('abcde');
+ expect(model.getText()).toContain('abcde');
+ });
+ });
+ * </file>
+ * </example>
+ */
var maxlengthDirective = function() {
return {
restrict: 'A',
@@ -28324,6 +30465,70 @@ var maxlengthDirective = function() {
};
};
+/**
+ * @ngdoc directive
+ * @name ngMinlength
+ *
+ * @description
+ *
+ * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is shorter than the integer obtained by evaluating the Angular expression given in the
+ * `ngMinlength` attribute value.
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `minlength` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ * </li>
+ * <li>
+ * The `ngMinlength` value must be an expression, while the `minlength` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngMinlengthDirective" module="ngMinlengthExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngMinlengthExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.minlength = 3;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="minlength">Set a minlength: </label>
+ * <input type="number" ng-model="minlength" id="minlength" />
+ * <br>
+ * <label for="input">This input is restricted by the current minlength: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-minlength="minlength" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default minlength', function() {
+ input.sendKeys('ab');
+ expect(model.getText()).not.toContain('ab');
+
+ input.sendKeys('abc');
+ expect(model.getText()).toContain('abc');
+ });
+ * </file>
+ * </example>
+ */
var minlengthDirective = function() {
return {
restrict: 'A',
@@ -28343,17 +30548,162 @@ var minlengthDirective = function() {
};
};
- if (window.angular.bootstrap) {
- //AngularJS is already loaded, so we can return here...
+if (window.angular.bootstrap) {
+ //AngularJS is already loaded, so we can return here...
+ if (window.console) {
console.log('WARNING: Tried to load angular more than once.');
- return;
+ }
+ return;
+}
+
+//try to bind to jquery now so that one can write jqLite(document).ready()
+//but we will rebind on bootstrap again.
+bindJQuery();
+
+publishExternalAPI(angular);
+
+angular.module("ngLocale", [], ["$provide", function($provide) {
+var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
+function getDecimals(n) {
+ n = n + '';
+ var i = n.indexOf('.');
+ return (i == -1) ? 0 : n.length - i - 1;
+}
+
+function getVF(n, opt_precision) {
+ var v = opt_precision;
+
+ if (undefined === v) {
+ v = Math.min(getDecimals(n), 3);
}
- //try to bind to jquery now so that one can write jqLite(document).ready()
- //but we will rebind on bootstrap again.
- bindJQuery();
+ var base = Math.pow(10, v);
+ var f = ((n * base) | 0) % base;
+ return {v: v, f: f};
+}
- publishExternalAPI(angular);
+$provide.value("$locale", {
+ "DATETIME_FORMATS": {
+ "AMPMS": [
+ "AM",
+ "PM"
+ ],
+ "DAY": [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday"
+ ],
+ "ERANAMES": [
+ "Before Christ",
+ "Anno Domini"
+ ],
+ "ERAS": [
+ "BC",
+ "AD"
+ ],
+ "FIRSTDAYOFWEEK": 6,
+ "MONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+ "SHORTDAY": [
+ "Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat"
+ ],
+ "SHORTMONTH": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ],
+ "STANDALONEMONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+ "WEEKENDRANGE": [
+ 5,
+ 6
+ ],
+ "fullDate": "EEEE, MMMM d, y",
+ "longDate": "MMMM d, y",
+ "medium": "MMM d, y h:mm:ss a",
+ "mediumDate": "MMM d, y",
+ "mediumTime": "h:mm:ss a",
+ "short": "M/d/yy h:mm a",
+ "shortDate": "M/d/yy",
+ "shortTime": "h:mm a"
+ },
+ "NUMBER_FORMATS": {
+ "CURRENCY_SYM": "$",
+ "DECIMAL_SEP": ".",
+ "GROUP_SEP": ",",
+ "PATTERNS": [
+ {
+ "gSize": 3,
+ "lgSize": 3,
+ "maxFrac": 3,
+ "minFrac": 0,
+ "minInt": 1,
+ "negPre": "-",
+ "negSuf": "",
+ "posPre": "",
+ "posSuf": ""
+ },
+ {
+ "gSize": 3,
+ "lgSize": 3,
+ "maxFrac": 2,
+ "minFrac": 2,
+ "minInt": 1,
+ "negPre": "-\u00a4",
+ "negSuf": "",
+ "posPre": "\u00a4",
+ "posSuf": ""
+ }
+ ]
+ },
+ "id": "en-us",
+ "localeID": "en_US",
+ "pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
+});
+}]);
jqLite(document).ready(function() {
angularInit(document, bootstrap);
@@ -28361,4 +30711,4 @@ var minlengthDirective = function() {
})(window, document);
-!window.angular.$$csp() && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'); \ No newline at end of file
+!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'); \ No newline at end of file