summaryrefslogtreecommitdiff
path: root/xstatic/pkg/angular/data/angular.js
diff options
context:
space:
mode:
Diffstat (limited to 'xstatic/pkg/angular/data/angular.js')
-rw-r--r--xstatic/pkg/angular/data/angular.js9955
1 files changed, 6671 insertions, 3284 deletions
diff --git a/xstatic/pkg/angular/data/angular.js b/xstatic/pkg/angular/data/angular.js
index 7ab67fb..ea563c0 100644
--- a/xstatic/pkg/angular/data/angular.js
+++ b/xstatic/pkg/angular/data/angular.js
@@ -1,6 +1,6 @@
/**
- * @license AngularJS v1.3.18
- * (c) 2010-2014 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.4.10
+ * (c) 2010-2015 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, document, undefined) {'use strict';
@@ -38,28 +38,33 @@
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
return function() {
- var code = arguments[0],
- prefix = '[' + (module ? module + ':' : '') + code + '] ',
- template = arguments[1],
- templateArgs = arguments,
+ var SKIP_INDEXES = 2;
- message, i;
+ var templateArgs = arguments,
+ code = templateArgs[0],
+ message = '[' + (module ? module + ':' : '') + code + '] ',
+ template = templateArgs[1],
+ paramPrefix, i;
- message = prefix + template.replace(/\{\d+\}/g, function(match) {
- var index = +match.slice(1, -1), arg;
+ message += template.replace(/\{\d+\}/g, function(match) {
+ var index = +match.slice(1, -1),
+ shiftedIndex = index + SKIP_INDEXES;
- if (index + 2 < templateArgs.length) {
- return toDebugString(templateArgs[index + 2]);
+ if (shiftedIndex < templateArgs.length) {
+ return toDebugString(templateArgs[shiftedIndex]);
}
+
return match;
});
- message = message + '\nhttp://errors.angularjs.org/1.3.18/' +
+ message += '\nhttp://errors.angularjs.org/1.4.10/' +
(module ? module + '/' : '') + code;
- for (i = 2; i < arguments.length; i++) {
- message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' +
- encodeURIComponent(toDebugString(arguments[i]));
+
+ for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
+ message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
+ encodeURIComponent(toDebugString(templateArgs[i]));
}
+
return new ErrorConstructor(message);
};
}
@@ -86,20 +91,21 @@ function minErr(module, ErrorConstructor) {
nodeName_: true,
isArrayLike: true,
forEach: true,
- sortedKeys: true,
forEachSorted: true,
reverseParams: true,
nextUid: true,
setHashKey: true,
extend: true,
- int: true,
+ toInt: true,
inherit: true,
+ merge: true,
noop: true,
identity: true,
valueFn: true,
isUndefined: true,
isDefined: true,
isObject: true,
+ isBlankObject: true,
isString: true,
isNumber: true,
isDate: true,
@@ -123,12 +129,15 @@ function minErr(module, ErrorConstructor) {
shallowCopy: true,
equals: true,
csp: true,
+ jq: true,
concat: true,
sliceArgs: true,
bind: true,
toJsonReplacer: true,
toJson: true,
fromJson: true,
+ convertTimezoneToLocal: true,
+ timezoneToOffset: true,
startingTag: true,
tryDecodeURIComponent: true,
parseKeyValue: true,
@@ -236,6 +245,7 @@ var
splice = [].splice,
push = [].push,
toString = Object.prototype.toString,
+ getPrototypeOf = Object.getPrototypeOf,
ngMinErr = minErr('ng'),
/** @name angular */
@@ -257,20 +267,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 existance 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;
}
/**
@@ -290,7 +305,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
@@ -328,23 +343,32 @@ function forEach(obj, iterator, context) {
}
} else if (obj.forEach && obj.forEach !== forEach) {
obj.forEach(iterator, context, obj);
- } else {
+ } else if (isBlankObject(obj)) {
+ // createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
+ for (key in obj) {
+ iterator.call(context, obj[key], key, obj);
+ }
+ } else if (typeof obj.hasOwnProperty === 'function') {
+ // Slow path for objects inheriting Object.prototype, hasOwnProperty check needed
for (key in obj) {
if (obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key, obj);
}
}
+ } else {
+ // Slow path for objects which do not have a method `hasOwnProperty`
+ for (key in obj) {
+ if (hasOwnProperty.call(obj, key)) {
+ iterator.call(context, obj[key], key, obj);
+ }
+ }
}
}
return obj;
}
-function sortedKeys(obj) {
- return Object.keys(obj).sort();
-}
-
function forEachSorted(obj, iterator, context) {
- var keys = sortedKeys(obj);
+ var keys = Object.keys(obj).sort();
for (var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
@@ -358,7 +382,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);};
}
/**
@@ -389,6 +413,41 @@ function setHashKey(obj, h) {
}
}
+
+function baseExtend(dst, objs, deep) {
+ var h = dst.$$hashKey;
+
+ for (var i = 0, ii = objs.length; i < ii; ++i) {
+ var obj = objs[i];
+ if (!isObject(obj) && !isFunction(obj)) continue;
+ var keys = Object.keys(obj);
+ for (var j = 0, jj = keys.length; j < jj; j++) {
+ var key = keys[j];
+ var src = obj[key];
+
+ 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);
+ }
+ } else {
+ dst[key] = src;
+ }
+ }
+ }
+
+ setHashKey(dst, h);
+ return dst;
+}
+
/**
* @ngdoc function
* @name angular.extend
@@ -399,31 +458,44 @@ function setHashKey(obj, h) {
* Extends the destination object `dst` by copying own enumerable properties from the `src` object(s)
* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so
* by passing an empty object as the target: `var object = angular.extend({}, object1, object2)`.
- * Note: Keep in mind that `angular.extend` does not support recursive merge (deep copy).
+ *
+ * **Note:** Keep in mind that `angular.extend` does not support recursive merge (deep copy). Use
+ * {@link angular.merge} for this.
*
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @returns {Object} Reference to `dst`.
*/
function extend(dst) {
- var h = dst.$$hashKey;
+ return baseExtend(dst, slice.call(arguments, 1), false);
+}
- for (var i = 1, ii = arguments.length; i < ii; i++) {
- var obj = arguments[i];
- if (obj) {
- var keys = Object.keys(obj);
- for (var j = 0, jj = keys.length; j < jj; j++) {
- var key = keys[j];
- dst[key] = obj[key];
- }
- }
- }
- setHashKey(dst, h);
- return dst;
+/**
+* @ngdoc function
+* @name angular.merge
+* @module ng
+* @kind function
+*
+* @description
+* Deeply extends the destination object `dst` by copying own enumerable properties from the `src` object(s)
+* to `dst`. You can specify multiple `src` objects. If you want to preserve original objects, you can do so
+* by passing an empty object as the target: `var object = angular.merge({}, object1, object2)`.
+*
+* Unlike {@link angular.extend extend()}, `merge()` recursively descends into object properties of source
+* objects, performing a deep copy.
+*
+* @param {Object} dst Destination object.
+* @param {...Object} src Source object(s).
+* @returns {Object} Reference to `dst`.
+*/
+function merge(dst) {
+ return baseExtend(dst, slice.call(arguments, 1), true);
}
-function int(str) {
+
+
+function toInt(str) {
return parseInt(str, 10);
}
@@ -476,6 +548,11 @@ identity.$inject = [];
function valueFn(value) {return function() {return value;};}
+function hasCustomToString(obj) {
+ return isFunction(obj.toString) && obj.toString !== toString;
+}
+
+
/**
* @ngdoc function
* @name angular.isUndefined
@@ -526,6 +603,16 @@ function isObject(value) {
/**
+ * Determine if a value is an object with a null prototype
+ *
+ * @returns {boolean} True if `value` is an `Object` with a null prototype
+ */
+function isBlankObject(value) {
+ return value !== null && typeof value === 'object' && !getPrototypeOf(value);
+}
+
+
+/**
* @ngdoc function
* @name angular.isString
* @module ng
@@ -661,6 +748,12 @@ function isPromiseLike(obj) {
}
+var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
+function isTypedArray(value) {
+ return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
+}
+
+
var trim = function(value) {
return isString(value) ? value.trim() : value;
};
@@ -697,9 +790,10 @@ function isElement(node) {
* @returns {object} in the form of {key1:true, key2:true, ...}
*/
function makeMap(str) {
- var obj = {}, items = str.split(","), i;
- for (i = 0; i < items.length; i++)
+ var obj = {}, items = str.split(','), i;
+ for (i = 0; i < items.length; i++) {
obj[items[i]] = true;
+ }
return obj;
}
@@ -714,9 +808,10 @@ function includes(array, obj) {
function arrayRemove(array, value) {
var index = array.indexOf(value);
- if (index >= 0)
+ if (index >= 0) {
array.splice(index, 1);
- return value;
+ }
+ return index;
}
/**
@@ -777,77 +872,113 @@ 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.");
- }
+function copy(source, destination) {
+ var stackSource = [];
+ var stackDest = [];
- if (!destination) {
- destination = source;
- if (source) {
- if (isArray(source)) {
- destination = copy(source, [], stackSource, stackDest);
- } else if (isDate(source)) {
- destination = new Date(source.getTime());
- } else if (isRegExp(source)) {
- destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
- destination.lastIndex = source.lastIndex;
- } else if (isObject(source)) {
- var emptyObject = Object.create(Object.getPrototypeOf(source));
- destination = copy(source, emptyObject, stackSource, stackDest);
- }
+ if (destination) {
+ if (isTypedArray(destination)) {
+ throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
+ }
+ if (source === destination) {
+ throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
}
- } else {
- if (source === destination) throw ngMinErr('cpi',
- "Can't copy! Source and destination are identical.");
- stackSource = stackSource || [];
- stackDest = stackDest || [];
+ // Empty the destination object
+ if (isArray(destination)) {
+ destination.length = 0;
+ } else {
+ forEach(destination, function(value, key) {
+ if (key !== '$$hashKey') {
+ delete destination[key];
+ }
+ });
+ }
- if (isObject(source)) {
- var index = stackSource.indexOf(source);
- if (index !== -1) return stackDest[index];
+ stackSource.push(source);
+ stackDest.push(destination);
+ return copyRecurse(source, destination);
+ }
- stackSource.push(source);
- stackDest.push(destination);
- }
+ return copyElement(source);
- var result;
+ function copyRecurse(source, destination) {
+ var h = destination.$$hashKey;
+ var result, key;
if (isArray(source)) {
- destination.length = 0;
- for (var i = 0; i < source.length; i++) {
- result = copy(source[i], null, stackSource, stackDest);
- if (isObject(source[i])) {
- stackSource.push(source[i]);
- stackDest.push(result);
- }
- destination.push(result);
+ for (var i = 0, ii = source.length; i < ii; i++) {
+ destination.push(copyElement(source[i]));
}
- } else {
- var h = destination.$$hashKey;
- if (isArray(destination)) {
- destination.length = 0;
- } else {
- forEach(destination, function(value, key) {
- delete destination[key];
- });
+ } 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]);
}
- for (var key in source) {
+ } else if (source && typeof source.hasOwnProperty === 'function') {
+ // Slow path, which must rely on hasOwnProperty
+ for (key in source) {
if (source.hasOwnProperty(key)) {
- result = copy(source[key], null, stackSource, stackDest);
- if (isObject(source[key])) {
- stackSource.push(source[key]);
- stackDest.push(result);
- }
- destination[key] = result;
+ 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] = 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;
+
+ if (isArray(source)) {
+ destination = [];
+ needsRecurse = true;
+ } else if (isTypedArray(source)) {
+ destination = new source.constructor(source);
+ } else if (isDate(source)) {
+ destination = new Date(source.getTime());
+ } else if (isRegExp(source)) {
+ destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
+ destination.lastIndex = source.lastIndex;
+ } else if (isBlob(source)) {
+ destination = new source.constructor([source], {type: source.type});
+ } else if (isFunction(source.cloneNode)) {
+ destination = source.cloneNode(true);
+ } else {
+ destination = Object.create(getPrototypeOf(source));
+ needsRecurse = true;
+ }
+
+ stackSource.push(source);
+ stackDest.push(destination);
+
+ return needsRecurse
+ ? copyRecurse(source, destination)
+ : destination;
}
- return destination;
}
/**
@@ -928,16 +1059,16 @@ function equals(o1, o2) {
} else {
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
- keySet = {};
+ 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 (!keySet.hasOwnProperty(key) &&
+ if (!(key in keySet) &&
key.charAt(0) !== '$' &&
- o2[key] !== undefined &&
+ isDefined(o2[key]) &&
!isFunction(o2[key])) return false;
}
return true;
@@ -948,25 +1079,93 @@ function equals(o1, o2) {
}
var csp = function() {
- if (isDefined(csp.isActive_)) return csp.isActive_;
+ if (!isDefined(csp.rules)) {
+
+
+ var ngCspElement = (document.querySelector('[ng-csp]') ||
+ document.querySelector('[data-ng-csp]'));
+
+ 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
+ };
+ }
+ }
- var active = !!(document.querySelector('[ng-csp]') ||
- document.querySelector('[data-ng-csp]'));
+ return csp.rules;
- if (!active) {
+ function noUnsafeEval() {
try {
/* jshint -W031, -W054 */
new Function('');
/* jshint +W031, +W054 */
+ return false;
} catch (e) {
- active = true;
+ return true;
}
}
-
- return (csp.isActive_ = active);
};
+/**
+ * @ngdoc directive
+ * @module ng
+ * @name ngJq
+ *
+ * @element ANY
+ * @param {string=} ngJq the name of the library available under `window`
+ * to be used for angular.element
+ * @description
+ * Use this directive to force the angular.element library. This should be
+ * used to force either jqLite by leaving ng-jq blank or setting the name of
+ * the jquery variable under window (eg. jQuery).
+ *
+ * Since angular looks for this directive when it is loaded (doesn't wait for the
+ * DOMContentLoaded event), it must be placed on an element that comes before the script
+ * which loads angular. Also, only the first instance of `ng-jq` will be used and all
+ * others ignored.
+ *
+ * @example
+ * This example shows how to force jqLite using the `ngJq` directive to the `html` tag.
+ ```html
+ <!doctype html>
+ <html ng-app ng-jq>
+ ...
+ ...
+ </html>
+ ```
+ * @example
+ * This example shows how to use a jQuery based library of a different name.
+ * The library name must be available at the top most 'window'.
+ ```html
+ <!doctype html>
+ <html ng-app ng-jq="jQueryLib">
+ ...
+ ...
+ </html>
+ ```
+ */
+var jq = function() {
+ if (isDefined(jq.name_)) return jq.name_;
+ var el;
+ var i, ii = ngAttrPrefixes.length, prefix, name;
+ for (i = 0; i < ii; ++i) {
+ prefix = ngAttrPrefixes[i];
+ if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ name = el.getAttribute(prefix + 'jq');
+ break;
+ }
+ }
+ return (jq.name_ = name);
+};
function concat(array1, array2, index) {
return array1.concat(slice.call(array2, index));
@@ -1050,7 +1249,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;
}
@@ -1077,6 +1276,30 @@ 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;
+}
+
+
+function addDateMinutes(date, minutes) {
+ date = new Date(date.getTime());
+ date.setMinutes(date.getMinutes() + minutes);
+ return date;
+}
+
+
+function convertTimezoneToLocal(date, timezone, reverse) {
+ reverse = reverse ? -1 : 1;
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
+}
+
+
/**
* @returns {string} Returns the string representation of the element.
*/
@@ -1092,7 +1315,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);
}
@@ -1124,13 +1347,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])) {
@@ -1205,10 +1434,9 @@ var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];
function getNgAttribute(element, ngAttr) {
var attr, i, ii = ngAttrPrefixes.length;
- element = jqLite(element);
for (i = 0; i < ii; ++i) {
attr = ngAttrPrefixes[i] + ngAttr;
- if (isString(attr = element.attr(attr))) {
+ if (isString(attr = element.getAttribute(attr))) {
return attr;
}
}
@@ -1539,7 +1767,11 @@ function bindJQuery() {
}
// bind to jQuery if present;
- jQuery = window.jQuery;
+ var jqName = jq();
+ 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.
// Angular 1.3+ technically requires at least jQuery 2.1+ but it may work with older
@@ -1643,22 +1875,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;
}
@@ -1722,8 +1956,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
@@ -1760,7 +1994,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) {
@@ -1830,7 +2064,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#provider $provide.provider()}.
*/
- provider: invokeLater('$provide', 'provider'),
+ provider: invokeLaterAndSetModuleName('$provide', 'provider'),
/**
* @ngdoc method
@@ -1841,7 +2075,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#factory $provide.factory()}.
*/
- factory: invokeLater('$provide', 'factory'),
+ factory: invokeLaterAndSetModuleName('$provide', 'factory'),
/**
* @ngdoc method
@@ -1852,7 +2086,7 @@ function setupModuleLoader(window) {
* @description
* See {@link auto.$provide#service $provide.service()}.
*/
- service: invokeLater('$provide', 'service'),
+ service: invokeLaterAndSetModuleName('$provide', 'service'),
/**
* @ngdoc method
@@ -1872,11 +2106,23 @@ 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'),
+ /**
+ * @ngdoc method
+ * @name angular.Module#decorator
+ * @module ng
+ * @param {string} The name of the service to decorate.
+ * @param {Function} This function will be invoked when the service needs to be
+ * instantiated and should return the decorated service instance.
+ * @description
+ * See {@link auto.$provide#decorator $provide.decorator()}.
+ */
+ decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
+
/**
* @ngdoc method
* @name angular.Module#animation
@@ -1890,7 +2136,7 @@ function setupModuleLoader(window) {
*
*
* Defines an animation hook that can be later used with
- * {@link ngAnimate.$animate $animate} service and directives that use this service.
+ * {@link $animate $animate} service and directives that use this service.
*
* ```js
* module.animation('.animation-name', function($inject1, $inject2) {
@@ -1909,7 +2155,7 @@ function setupModuleLoader(window) {
* See {@link ng.$animateProvider#register $animateProvider.register()} and
* {@link ngAnimate ngAnimate module} for more information.
*/
- animation: invokeLater('$animateProvider', 'register'),
+ animation: invokeLaterAndSetModuleName('$animateProvider', 'register'),
/**
* @ngdoc method
@@ -1927,7 +2173,7 @@ function setupModuleLoader(window) {
* (`myapp_subsection_filterx`).
* </div>
*/
- filter: invokeLater('$filterProvider', 'register'),
+ filter: invokeLaterAndSetModuleName('$filterProvider', 'register'),
/**
* @ngdoc method
@@ -1939,7 +2185,7 @@ function setupModuleLoader(window) {
* @description
* See {@link ng.$controllerProvider#register $controllerProvider.register()}.
*/
- controller: invokeLater('$controllerProvider', 'register'),
+ controller: invokeLaterAndSetModuleName('$controllerProvider', 'register'),
/**
* @ngdoc method
@@ -1952,7 +2198,7 @@ function setupModuleLoader(window) {
* @description
* See {@link ng.$compileProvider#directive $compileProvider.directive()}.
*/
- directive: invokeLater('$compileProvider', 'directive'),
+ directive: invokeLaterAndSetModuleName('$compileProvider', 'directive'),
/**
* @ngdoc method
@@ -2002,6 +2248,19 @@ function setupModuleLoader(window) {
return moduleInstance;
};
}
+
+ /**
+ * @param {string} provider
+ * @param {string} method
+ * @returns {angular.Module}
+ */
+ function invokeLaterAndSetModuleName(provider, method) {
+ return function(recipeName, factoryFunction) {
+ if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
+ invokeQueue.push([provider, method, arguments]);
+ return moduleInstance;
+ };
+ }
});
};
});
@@ -2017,7 +2276,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);
}
@@ -2028,7 +2287,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);
@@ -2039,7 +2298,6 @@ function toDebugString(obj) {
/* global angularModule: true,
version: true,
- $LocaleProvider,
$CompileProvider,
htmlAnchorDirective,
@@ -2056,7 +2314,6 @@ function toDebugString(obj) {
ngClassDirective,
ngClassEvenDirective,
ngClassOddDirective,
- ngCspDirective,
ngCloakDirective,
ngControllerDirective,
ngFormDirective,
@@ -2093,16 +2350,26 @@ function toDebugString(obj) {
$AnchorScrollProvider,
$AnimateProvider,
+ $CoreAnimateCssProvider,
+ $$CoreAnimateJsProvider,
+ $$CoreAnimateQueueProvider,
+ $$AnimateRunnerFactoryProvider,
+ $$AnimateAsyncRunFactoryProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
$DocumentProvider,
$ExceptionHandlerProvider,
$FilterProvider,
+ $$ForceReflowProvider,
$InterpolateProvider,
$IntervalProvider,
+ $$HashMapProvider,
$HttpProvider,
+ $HttpParamSerializerProvider,
+ $HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
+ $xhrFactoryProvider,
$LocationProvider,
$LogProvider,
$ParseProvider,
@@ -2118,9 +2385,9 @@ function toDebugString(obj) {
$$TestabilityProvider,
$TimeoutProvider,
$$RAFProvider,
- $$AsyncCallbackProvider,
$WindowProvider,
- $$jqLiteProvider
+ $$jqLiteProvider,
+ $$CookieReaderProvider
*/
@@ -2129,8 +2396,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".
@@ -2139,11 +2407,11 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.3.18', // all of these placeholder strings will be replaced by grunt's
+ full: '1.4.10', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task
- minor: 3,
- dot: 18,
- codeName: 'collective-penmanship'
+ minor: 4,
+ dot: 10,
+ codeName: 'benignant-oscillation'
};
@@ -2152,6 +2420,7 @@ function publishExternalAPI(angular) {
'bootstrap': bootstrap,
'copy': copy,
'extend': extend,
+ 'merge': merge,
'equals': equals,
'element': jqLite,
'forEach': forEach,
@@ -2181,11 +2450,6 @@ function publishExternalAPI(angular) {
});
angularModule = setupModuleLoader(window);
- try {
- angularModule('ngLocale');
- } catch (e) {
- angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
- }
angularModule('ng', ['ngLocale'], ['$provide',
function ngModule($provide) {
@@ -2248,16 +2512,25 @@ function publishExternalAPI(angular) {
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
+ $animateCss: $CoreAnimateCssProvider,
+ $$animateJs: $$CoreAnimateJsProvider,
+ $$animateQueue: $$CoreAnimateQueueProvider,
+ $$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,
@@ -2273,8 +2546,9 @@ function publishExternalAPI(angular) {
$timeout: $TimeoutProvider,
$window: $WindowProvider,
$$rAF: $$RAFProvider,
- $$asyncCallback: $$AsyncCallbackProvider,
- $$jqLite: $$jqLiteProvider
+ $$jqLite: $$jqLiteProvider,
+ $$HashMap: $$HashMapProvider,
+ $$cookieReader: $$CookieReaderProvider
});
}
]);
@@ -2313,16 +2587,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:
@@ -2335,7 +2615,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/)
@@ -2345,7 +2626,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/)
@@ -2359,7 +2640,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/)
*
@@ -2431,10 +2712,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>'],
@@ -2462,6 +2743,13 @@ function jqLiteAcceptsData(node) {
return nodeType === NODE_TYPE_ELEMENT || !nodeType || nodeType === NODE_TYPE_DOCUMENT;
}
+function jqLiteHasData(node) {
+ for (var key in jqCache[node.ng339]) {
+ return true;
+ }
+ return false;
+}
+
function jqLiteBuildFragment(html, context) {
var tmp, tag, wrap,
fragment = context.createDocumentFragment(),
@@ -2514,6 +2802,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) {
@@ -2572,17 +2878,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]);
+ }
});
}
}
@@ -2723,7 +3035,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
@@ -2829,14 +3141,14 @@ 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
+ removeData: jqLiteRemoveData,
+ hasData: jqLiteHasData
}, function(fn, name) {
JQLite[name] = fn;
});
@@ -2967,7 +3279,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
@@ -2988,7 +3300,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;
@@ -3037,6 +3349,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);
@@ -3044,7 +3359,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]);
}
}
};
@@ -3055,6 +3370,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
@@ -3083,35 +3414,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);
+ }
}
},
@@ -3146,8 +3470,9 @@ forEach({
children: function(element) {
var children = [];
forEach(element.childNodes, function(element) {
- if (element.nodeType === NODE_TYPE_ELEMENT)
+ if (element.nodeType === NODE_TYPE_ELEMENT) {
children.push(element);
+ }
});
return children;
},
@@ -3178,12 +3503,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,
@@ -3393,6 +3713,12 @@ HashMap.prototype = {
}
};
+var $$HashMapProvider = [function() {
+ this.$get = [function() {
+ return HashMap;
+ }];
+}];
+
/**
* @ngdoc function
* @module ng
@@ -3455,7 +3781,7 @@ HashMap.prototype = {
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
+var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
@@ -3880,8 +4206,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.
@@ -3921,14 +4259,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.
@@ -3953,8 +4290,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}.
*
@@ -3982,7 +4322,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.
*
@@ -4038,7 +4378,7 @@ function createInjector(modulesToLoad, strictDi) {
}));
- forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });
+ forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
return instanceInjector;
@@ -4111,6 +4451,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;
@@ -4281,9 +4622,10 @@ function $AnchorScrollProvider() {
* @requires $rootScope
*
* @description
- * When called, it checks the current value of {@link ng.$location#hash $location.hash()} and
- * scrolls to the related element, according to the rules specified in the
- * [Html5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
+ * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
+ * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
+ * in the
+ * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
*
* 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
@@ -4292,6 +4634,9 @@ function $AnchorScrollProvider() {
* Additionally, you can use its {@link ng.$anchorScroll#yOffset yOffset} property to specify a
* vertical scroll-offset (either fixed or dynamic).
*
+ * @param {string=} hash The hash specifying the element to scroll to. If omitted, the value of
+ * {@link ng.$location#hash $location.hash()} will be used.
+ *
* @property {(number|function|jqLite)} yOffset
* If set, specifies a vertical scroll-offset. This is often useful when there are fixed
* positioned elements at the top of the page, such as navbars, headers etc.
@@ -4475,8 +4820,9 @@ function $AnchorScrollProvider() {
}
}
- function scroll() {
- var hash = $location.hash(), elm;
+ function scroll(hash) {
+ hash = isString(hash) ? hash : $location.hash();
+ var elm;
// empty hash, scroll to the top of the page
if (!hash) scrollTo(null);
@@ -4510,6 +4856,159 @@ function $AnchorScrollProvider() {
}
var $animateMinErr = minErr('$animate');
+var ELEMENT_NODE = 1;
+var NG_ANIMATE_CLASSNAME = 'ng-animate';
+
+function mergeClasses(a,b) {
+ if (!a && !b) return '';
+ if (!a) return b;
+ if (!b) return a;
+ if (isArray(a)) a = a.join(' ');
+ if (isArray(b)) b = b.join(' ');
+ return a + ' ' + b;
+}
+
+function extractElementNode(element) {
+ for (var i = 0; i < element.length; i++) {
+ var elm = element[i];
+ if (elm.nodeType === ELEMENT_NODE) {
+ return elm;
+ }
+ }
+}
+
+function splitClasses(classes) {
+ if (isString(classes)) {
+ classes = classes.split(' ');
+ }
+
+ // Use createMap() to prevent class assumptions involving property names in
+ // Object.prototype
+ var obj = createMap();
+ forEach(classes, function(klass) {
+ // sometimes the split leaves empty string values
+ // incase extra spaces were applied to the options
+ if (klass.length) {
+ obj[klass] = true;
+ }
+ });
+ return obj;
+}
+
+// if any other type of options value besides an Object value is
+// passed into the $animate.method() animation then this helper code
+// will be run which will ignore it. While this patch is not the
+// greatest solution to this, a lot of existing plugins depend on
+// $animate to either call the callback (< 1.2) or return a promise
+// that can be changed. This helper function ensures that the options
+// are wiped clean incase a callback function is provided.
+function prepareAnimateOptions(options) {
+ return isObject(options)
+ ? options
+ : {};
+}
+
+var $$CoreAnimateJsProvider = function() {
+ this.$get = function() {};
+};
+
+// this is prefixed with Core since it conflicts with
+// the animateQueueProvider defined in ngAnimate/animateQueue.js
+var $$CoreAnimateQueueProvider = function() {
+ var postDigestQueue = new HashMap();
+ var postDigestElements = [];
+
+ this.$get = ['$$AnimateRunner', '$rootScope',
+ function($$AnimateRunner, $rootScope) {
+ return {
+ enabled: noop,
+ on: noop,
+ off: noop,
+ pin: noop,
+
+ push: function(element, event, options, domOperation) {
+ domOperation && domOperation();
+
+ options = options || {};
+ options.from && element.css(options.from);
+ options.to && element.css(options.to);
+
+ if (options.addClass || options.removeClass) {
+ addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
+ }
+
+ 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 updateData(data, classes, value) {
+ var changed = false;
+ if (classes) {
+ classes = isString(classes) ? classes.split(' ') :
+ isArray(classes) ? classes : [];
+ forEach(classes, function(className) {
+ if (className) {
+ 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;
+ }
+ }
+ });
+
+ forEach(element, function(elm) {
+ toAdd && jqLiteAddClass(elm, toAdd);
+ toRemove && jqLiteRemoveClass(elm, toRemove);
+ });
+ postDigestQueue.remove(element);
+ }
+ });
+ postDigestElements.length = 0;
+ }
+
+
+ function addRemoveClassesPostDigest(element, add, remove) {
+ var data = postDigestQueue.get(element) || {};
+
+ var classesAdded = updateData(data, add, true);
+ var classesRemoved = updateData(data, remove, false);
+
+ if (classesAdded || classesRemoved) {
+
+ postDigestQueue.put(element, data);
+ postDigestElements.push(element);
+
+ if (postDigestElements.length === 1) {
+ $rootScope.$$postDigest(handleCSSClassChanges);
+ }
+ }
+ }
+ }];
+};
/**
* @ngdoc provider
@@ -4517,20 +5016,18 @@ var $animateMinErr = minErr('$animate');
*
* @description
* Default implementation of $animate that doesn't perform any animations, instead just
- * synchronously performs DOM
- * updates and calls done() callbacks.
+ * synchronously performs DOM updates and resolves the returned runner promise.
*
- * In order to enable animations the ngAnimate module has to be loaded.
+ * In order to enable animations the `ngAnimate` module has to be loaded.
*
- * To see the functional implementation check out src/ngAnimate/animate.js
+ * To see the functional implementation check out `src/ngAnimate/animate.js`.
*/
var $AnimateProvider = ['$provide', function($provide) {
+ var provider = this;
+ this.$$registeredAnimations = Object.create(null);
- this.$$selectors = {};
-
-
- /**
+ /**
* @ngdoc method
* @name $animateProvider#register
*
@@ -4539,33 +5036,43 @@ var $AnimateProvider = ['$provide', function($provide) {
* animation object which contains callback functions for each event that is expected to be
* animated.
*
- * * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction`
- * must be called once the element animation is complete. If a function is returned then the
- * animation service will use this function to cancel the animation whenever a cancel event is
- * triggered.
+ * * `eventFn`: `function(element, ... , doneFunction, options)`
+ * The element to animate, the `doneFunction` and the options fed into the animation. Depending
+ * on the type of animation additional arguments will be injected into the animation function. The
+ * list below explains the function signatures for the different animation methods:
*
+ * - setClass: function(element, addedClasses, removedClasses, doneFunction, options)
+ * - addClass: function(element, addedClasses, doneFunction, options)
+ * - removeClass: function(element, removedClasses, doneFunction, options)
+ * - enter, leave, move: function(element, doneFunction, options)
+ * - animate: function(element, fromStyles, toStyles, doneFunction, options)
+ *
+ * Make sure to trigger the `doneFunction` once the animation is fully complete.
*
* ```js
* return {
- * eventFn : function(element, done) {
- * //code to run the animation
- * //once complete, then run done()
- * return function cancellationFunction() {
- * //code to cancel the animation
- * }
- * }
- * }
+ * //enter, leave, move signature
+ * eventFn : function(element, done, options) {
+ * //code to run the animation
+ * //once complete, then run done()
+ * return function endFunction(wasCancelled) {
+ * //code to cancel the animation
+ * }
+ * }
+ * }
* ```
*
- * @param {string} name The name of the animation.
+ * @param {string} name The name of the animation (this is what the class-based CSS value will be compared to).
* @param {Function} factory The factory function that will be executed to return the animation
* object.
*/
this.register = function(name, factory) {
+ if (name && name.charAt(0) !== '.') {
+ throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
+ }
+
var key = name + '-animation';
- if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel',
- "Expecting class selector starting with '.' got '{0}'.", name);
- this.$$selectors[name.substr(1)] = key;
+ provider.$$registeredAnimations[name.substr(1)] = key;
$provide.factory(key, factory);
};
@@ -4576,8 +5083,8 @@ var $AnimateProvider = ['$provide', function($provide) {
* @description
* Sets and/or returns the CSS class regular expression that is checked when performing
* an animation. Upon bootstrap the classNameFilter value is not set at all and will
- * therefore enable $animate to attempt to perform an animation on any element.
- * When setting the classNameFilter value, animations will only be performed on elements
+ * therefore enable $animate to attempt to perform an animation on any element that is triggered.
+ * When setting the `classNameFilter` value, animations will only be performed on elements
* that successfully match the filter expression. This in turn can boost performance
* for low-powered devices as well as applications containing a lot of structural operations.
* @param {RegExp=} expression The className expression which will be checked against all animations
@@ -4586,102 +5093,167 @@ var $AnimateProvider = ['$provide', function($provide) {
this.classNameFilter = function(expression) {
if (arguments.length === 1) {
this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
- }
- return this.$$classNameFilter;
- };
-
- this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
-
- var currentDefer;
-
- function runAnimationPostDigest(fn) {
- var cancelFn, defer = $$q.defer();
- defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
- cancelFn && cancelFn();
- };
-
- $rootScope.$$postDigest(function ngAnimatePostDigest() {
- cancelFn = fn(function ngAnimateNotifyComplete() {
- defer.resolve();
- });
- });
-
- return defer.promise;
- }
+ if (this.$$classNameFilter) {
+ var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
+ if (reservedRegex.test(this.$$classNameFilter.toString())) {
+ throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
- function resolveElementClasses(element, classes) {
- var toAdd = [], toRemove = [];
-
- var hasClasses = createMap();
- forEach((element.attr('class') || '').split(/\s+/), function(className) {
- hasClasses[className] = true;
- });
-
- forEach(classes, function(status, className) {
- var hasClass = hasClasses[className];
-
- // If the most recent class manipulation (via $animate) was to remove the class, and the
- // element currently has the class, the class is scheduled for removal. Otherwise, if
- // the most recent class manipulation (via $animate) was to add the class, and the
- // element does not currently have the class, the class is scheduled to be added.
- if (status === false && hasClass) {
- toRemove.push(className);
- } else if (status === true && !hasClass) {
- toAdd.push(className);
}
- });
-
- return (toAdd.length + toRemove.length) > 0 &&
- [toAdd.length ? toAdd : null, toRemove.length ? toRemove : null];
- }
-
- function cachedClassManipulation(cache, classes, op) {
- for (var i=0, ii = classes.length; i < ii; ++i) {
- var className = classes[i];
- cache[className] = op;
}
}
+ return this.$$classNameFilter;
+ };
- function asyncPromise() {
- // only serve one instance of a promise in order to save CPU cycles
- if (!currentDefer) {
- currentDefer = $$q.defer();
- $$asyncCallback(function() {
- currentDefer.resolve();
- currentDefer = null;
- });
- }
- return currentDefer.promise;
- }
-
- function applyStyles(element, options) {
- if (angular.isObject(options)) {
- var styles = extend(options.from || {}, options.to || {});
- element.css(styles);
+ this.$get = ['$$animateQueue', function($$animateQueue) {
+ function domInsert(element, parentElement, afterElement) {
+ // if for some reason the previous element was removed
+ // from the dom sometime before this code runs then let's
+ // just stick to using the parent element as the anchor
+ if (afterElement) {
+ var afterNode = extractElementNode(afterElement);
+ if (afterNode && !afterNode.parentNode && !afterNode.previousElementSibling) {
+ afterElement = null;
+ }
}
+ afterElement ? afterElement.after(element) : parentElement.prepend(element);
}
/**
- *
* @ngdoc service
* @name $animate
- * @description The $animate service provides rudimentary DOM manipulation functions to
- * insert, remove and move elements within the DOM, as well as adding and removing classes.
- * This service is the core service used by the ngAnimate $animator service which provides
- * high-level animation hooks for CSS and JavaScript.
+ * @description The $animate service exposes a series of DOM utility methods that provide support
+ * for animation hooks. The default behavior is the application of DOM operations, however,
+ * 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.
*
- * $animate is available in the AngularJS core, however, the ngAnimate module must be included
- * to enable full out animation support. Otherwise, $animate will only perform simple DOM
- * manipulation operations.
+ * 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`,
+ * `ngShow`, `ngHide` and `ngMessages` also provide support for animations.
*
- * To learn more about enabling animation support, click here to visit the {@link ngAnimate
- * ngAnimate module page} as well as the {@link ngAnimate.$animate ngAnimate $animate service
- * page}.
+ * It is recommended that the`$animate` service is always used when executing DOM-related procedures within directives.
+ *
+ * To learn more about enabling animation support, click here to visit the
+ * {@link ngAnimate ngAnimate module page}.
*/
return {
- animate: function(element, from, to) {
- applyStyles(element, { from: from, to: to });
- return asyncPromise();
+ // we don't call it directly since non-existant arguments may
+ // be interpreted as null within the sub enabled function
+
+ /**
+ *
+ * @ngdoc method
+ * @name $animate#on
+ * @kind function
+ * @description Sets up an event listener to fire whenever the animation event (enter, leave, move, etc...)
+ * has fired on the given element or among any of its children. Once the listener is fired, the provided callback
+ * is fired with the following params:
+ *
+ * ```js
+ * $animate.on('enter', container,
+ * function callback(element, phase) {
+ * // cool we detected an enter animation within the container
+ * }
+ * );
+ * ```
+ *
+ * @param {string} event the animation event that will be captured (e.g. enter, leave, move, addClass, removeClass, etc...)
+ * @param {DOMElement} container the container element that will capture each of the animation events that are fired on itself
+ * as well as among its children
+ * @param {Function} callback the callback function that will be fired when the listener is triggered
+ *
+ * The arguments present in the callback function are:
+ * * `element` - The captured DOM element that the animation was fired on.
+ * * `phase` - The phase of the animation. The two possible phases are **start** (when the animation starts) and **close** (when it ends).
+ */
+ on: $$animateQueue.on,
+
+ /**
+ *
+ * @ngdoc method
+ * @name $animate#off
+ * @kind function
+ * @description Deregisters an event listener based on the event which has been associated with the provided element. This method
+ * can be used in three different ways depending on the arguments:
+ *
+ * ```js
+ * // remove all the animation event listeners listening for `enter`
+ * $animate.off('enter');
+ *
+ * // 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
+ * $animate.off('enter', container, callback);
+ * ```
+ *
+ * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...)
+ * @param {DOMElement=} container the container element the event listener was placed on
+ * @param {Function=} callback the callback function that was registered as the listener
+ */
+ off: $$animateQueue.off,
+
+ /**
+ * @ngdoc method
+ * @name $animate#pin
+ * @kind function
+ * @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
+ * outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
+ * element despite being outside the realm of the application or within another application. Say for example if the application
+ * was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
+ * as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
+ * that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association.
+ *
+ * Note that this feature is only active when the `ngAnimate` module is used.
+ *
+ * @param {DOMElement} element the external element that will be pinned
+ * @param {DOMElement} parentElement the host parent element that will be associated with the external element
+ */
+ pin: $$animateQueue.pin,
+
+ /**
+ *
+ * @ngdoc method
+ * @name $animate#enabled
+ * @kind function
+ * @description Used to get and set whether animations are enabled or not on the entire application or on an element and its children. This
+ * function can be called in four ways:
+ *
+ * ```js
+ * // returns true or false
+ * $animate.enabled();
+ *
+ * // changes the enabled state for all animations
+ * $animate.enabled(false);
+ * $animate.enabled(true);
+ *
+ * // returns true or false if animations are enabled for an element
+ * $animate.enabled(element);
+ *
+ * // changes the enabled state for an element and its children
+ * $animate.enabled(element, true);
+ * $animate.enabled(element, false);
+ * ```
+ *
+ * @param {DOMElement=} element the element that will be considered for checking/setting the enabled state
+ * @param {boolean=} enabled whether or not the animations will be enabled for the element
+ *
+ * @return {boolean} whether or not animations are enabled
+ */
+ enabled: $$animateQueue.enabled,
+
+ /**
+ * @ngdoc method
+ * @name $animate#cancel
+ * @kind function
+ * @description Cancels the provided animation.
+ *
+ * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ */
+ cancel: function(runner) {
+ runner.end && runner.end();
},
/**
@@ -4689,206 +5261,448 @@ var $AnimateProvider = ['$provide', function($provide) {
* @ngdoc method
* @name $animate#enter
* @kind function
- * @description Inserts the element into the DOM either after the `after` element or
- * as the first child within the `parent` element. When the function is called a promise
- * is returned that will be resolved at a later time.
+ * @description Inserts the element into the DOM either after the `after` element (if provided) or
+ * as the first child within the `parent` element and then triggers an animation.
+ * A promise is returned that will be resolved during the next digest once the animation
+ * has completed.
+ *
* @param {DOMElement} element the element which will be inserted into the DOM
* @param {DOMElement} parent the parent element which will append the element as
- * a child (if the after element is not present)
- * @param {DOMElement} after the sibling element which will append the element
- * after itself
- * @param {object=} options an optional collection of styles that will be applied to the element.
+ * a child (so long as the after element is not present)
+ * @param {DOMElement=} after the sibling element after which the element will be appended
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
enter: function(element, parent, after, options) {
- applyStyles(element, options);
- after ? after.after(element)
- : parent.prepend(element);
- return asyncPromise();
+ parent = parent && jqLite(parent);
+ after = after && jqLite(after);
+ parent = parent || after.parent();
+ domInsert(element, parent, after);
+ return $$animateQueue.push(element, 'enter', prepareAnimateOptions(options));
},
/**
*
* @ngdoc method
- * @name $animate#leave
+ * @name $animate#move
* @kind function
- * @description Removes the element from the DOM. When the function is called a promise
- * is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will be removed from the DOM
- * @param {object=} options an optional collection of options that will be applied to the element.
+ * @description Inserts (moves) the element into its new position in the DOM either after
+ * the `after` element (if provided) or as the first child within the `parent` element
+ * and then triggers an animation. A promise is returned that will be resolved
+ * during the next digest once the animation has completed.
+ *
+ * @param {DOMElement} element the element which will be moved into the new DOM position
+ * @param {DOMElement} parent the parent element which will append the element as
+ * a child (so long as the after element is not present)
+ * @param {DOMElement=} after the sibling element after which the element will be appended
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
- leave: function(element, options) {
- applyStyles(element, options);
- element.remove();
- return asyncPromise();
+ move: function(element, parent, after, options) {
+ parent = parent && jqLite(parent);
+ after = after && jqLite(after);
+ parent = parent || after.parent();
+ domInsert(element, parent, after);
+ return $$animateQueue.push(element, 'move', prepareAnimateOptions(options));
},
/**
- *
* @ngdoc method
- * @name $animate#move
+ * @name $animate#leave
* @kind function
- * @description Moves the position of the provided element within the DOM to be placed
- * either after the `after` element or inside of the `parent` element. When the function
- * is called a promise is returned that will be resolved at a later time.
+ * @description Triggers an animation and then removes the element from the DOM.
+ * When the function is called a promise is returned that will be resolved during the next
+ * digest once the animation has completed.
+ *
+ * @param {DOMElement} element the element which will be removed from the DOM
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
*
- * @param {DOMElement} element the element which will be moved around within the
- * DOM
- * @param {DOMElement} parent the parent element where the element will be
- * inserted into (if the after element is not present)
- * @param {DOMElement} after the sibling element where the element will be
- * positioned next to
- * @param {object=} options an optional collection of options that will be applied to the element.
* @return {Promise} the animation callback promise
*/
- move: function(element, parent, after, options) {
- // Do not remove element before insert. Removing will cause data associated with the
- // element to be dropped. Insert will implicitly do the remove.
- return this.enter(element, parent, after, options);
+ leave: function(element, options) {
+ return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
+ element.remove();
+ });
},
/**
- *
* @ngdoc method
* @name $animate#addClass
* @kind function
- * @description Adds the provided className CSS class value to the provided element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have the className value
- * added to it
- * @param {string} className the CSS class which will be added to the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Triggers an addClass animation surrounding the addition of the provided CSS class(es). Upon
+ * execution, the addClass operation will only be handled after the next digest and it will not trigger an
+ * animation if element already contains the CSS class or if the class is removed at a later step.
+ * Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
addClass: function(element, className, options) {
- return this.setClass(element, className, [], options);
- },
-
- $$addClassImmediately: function(element, className, options) {
- element = jqLite(element);
- className = !isString(className)
- ? (isArray(className) ? className.join(' ') : '')
- : className;
- forEach(element, function(element) {
- jqLiteAddClass(element, className);
- });
- applyStyles(element, options);
- return asyncPromise();
+ options = prepareAnimateOptions(options);
+ options.addClass = mergeClasses(options.addclass, className);
+ return $$animateQueue.push(element, 'addClass', options);
},
/**
- *
* @ngdoc method
* @name $animate#removeClass
* @kind function
- * @description Removes the provided className CSS class value from the provided element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have the className value
- * removed from it
- * @param {string} className the CSS class which will be removed from the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Triggers a removeClass animation surrounding the removal of the provided CSS class(es). Upon
+ * execution, the removeClass operation will only be handled after the next digest and it will not trigger an
+ * animation if element does not contain the CSS class or if the class is added at a later step.
+ * Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
removeClass: function(element, className, options) {
- return this.setClass(element, [], className, options);
- },
-
- $$removeClassImmediately: function(element, className, options) {
- element = jqLite(element);
- className = !isString(className)
- ? (isArray(className) ? className.join(' ') : '')
- : className;
- forEach(element, function(element) {
- jqLiteRemoveClass(element, className);
- });
- applyStyles(element, options);
- return asyncPromise();
+ options = prepareAnimateOptions(options);
+ options.removeClass = mergeClasses(options.removeClass, className);
+ return $$animateQueue.push(element, 'removeClass', options);
},
/**
- *
* @ngdoc method
* @name $animate#setClass
* @kind function
- * @description Adds and/or removes the given CSS classes to and from the element.
- * When the function is called a promise is returned that will be resolved at a later time.
- * @param {DOMElement} element the element which will have its CSS classes changed
- * removed from it
- * @param {string} add the CSS classes which will be added to the element
- * @param {string} remove the CSS class which will be removed from the element
- * @param {object=} options an optional collection of options that will be applied to the element.
+ *
+ * @description Performs both the addition and removal of a CSS classes on an element and (during the process)
+ * triggers an animation surrounding the class addition/removal. Much like `$animate.addClass` and
+ * `$animate.removeClass`, `setClass` will only evaluate the classes being added/removed once a digest has
+ * passed. Note that class-based animations are treated differently compared to structural animations
+ * (like enter, move and leave) since the CSS classes may be added/removed at different points
+ * depending if CSS or JavaScript animations are used.
+ *
+ * @param {DOMElement} element the element which the CSS classes will be applied to
+ * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces)
+ * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
* @return {Promise} the animation callback promise
*/
setClass: function(element, add, remove, options) {
- var self = this;
- var STORAGE_KEY = '$$animateClasses';
- var createdCache = false;
- element = jqLite(element);
-
- var cache = element.data(STORAGE_KEY);
- if (!cache) {
- cache = {
- classes: {},
- options: options
- };
- createdCache = true;
- } else if (options && cache.options) {
- cache.options = angular.extend(cache.options || {}, options);
- }
-
- var classes = cache.classes;
-
- add = isArray(add) ? add : add.split(' ');
- remove = isArray(remove) ? remove : remove.split(' ');
- cachedClassManipulation(classes, add, true);
- cachedClassManipulation(classes, remove, false);
-
- if (createdCache) {
- cache.promise = runAnimationPostDigest(function(done) {
- var cache = element.data(STORAGE_KEY);
- element.removeData(STORAGE_KEY);
-
- // in the event that the element is removed before postDigest
- // is run then the cache will be undefined and there will be
- // no need anymore to add or remove and of the element classes
- if (cache) {
- var classes = resolveElementClasses(element, cache.classes);
- if (classes) {
- self.$$setClassImmediately(element, classes[0], classes[1], cache.options);
- }
- }
+ options = prepareAnimateOptions(options);
+ options.addClass = mergeClasses(options.addClass, add);
+ options.removeClass = mergeClasses(options.removeClass, remove);
+ return $$animateQueue.push(element, 'setClass', options);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $animate#animate
+ * @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 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.)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element
+ *
+ * @return {Promise} the animation callback promise
+ */
+ animate: function(element, from, to, className, options) {
+ options = prepareAnimateOptions(options);
+ options.from = options.from ? extend(options.from, from) : from;
+ options.to = options.to ? extend(options.to, to) : to;
+
+ className = className || 'ng-inline-animate';
+ options.tempClasses = mergeClasses(options.tempClasses, className);
+ return $$animateQueue.push(element, 'animate', options);
+ }
+ };
+ }];
+}];
+
+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,
- done();
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ status === false ? reject() : resolve();
+ });
});
- element.data(STORAGE_KEY, cache);
}
+ return this.promise;
+ },
- return cache.promise;
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
},
- $$setClassImmediately: function(element, add, remove, options) {
- add && this.$$addClassImmediately(element, add);
- remove && this.$$removeClassImmediately(element, remove);
- applyStyles(element, options);
- return asyncPromise();
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
},
- enabled: noop,
- cancel: noop
+ '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;
}];
-}];
+};
-function $$AsyncCallbackProvider() {
- this.$get = ['$$rAF', '$timeout', function($$rAF, $timeout) {
- return $$rAF.supported
- ? function(fn) { return $$rAF(fn); }
- : function(fn) {
- return $timeout(fn, 0, false);
+/**
+ * @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 */
@@ -4964,11 +5778,6 @@ function Browser(window, document, $log, $sniffer) {
* @param {function()} callback Function that will be called when no outstanding request
*/
self.notifyWhenNoOutstandingRequests = function(callback) {
- // force browser to execute all pollFns - this is needed so that cookies and other pollers fire
- // at some deterministic time in respect to the test runner's actions. Leaving things up to the
- // regular poller would result in flaky tests.
- forEach(pollFns, function(pollFn) { pollFn(); });
-
if (outstandingRequestCount === 0) {
callback();
} else {
@@ -4977,51 +5786,13 @@ function Browser(window, document, $log, $sniffer) {
};
//////////////////////////////////////////////////////////////
- // Poll Watcher API
- //////////////////////////////////////////////////////////////
- var pollFns = [],
- pollTimeout;
-
- /**
- * @name $browser#addPollFn
- *
- * @param {function()} fn Poll function to add
- *
- * @description
- * Adds a function to the list of functions that poller periodically executes,
- * and starts polling if not started yet.
- *
- * @returns {function()} the added function
- */
- self.addPollFn = function(fn) {
- if (isUndefined(pollTimeout)) startPoller(100, setTimeout);
- pollFns.push(fn);
- return fn;
- };
-
- /**
- * @param {number} interval How often should browser call poll functions (ms)
- * @param {function()} setTimeout Reference to a real or fake `setTimeout` function.
- *
- * @description
- * Configures the poller to run in the specified intervals, using the specified
- * setTimeout fn and kicks it off.
- */
- function startPoller(interval, setTimeout) {
- (function check() {
- forEach(pollFns, function(pollFn) { pollFn(); });
- pollTimeout = setTimeout(check, interval);
- })();
- }
-
- //////////////////////////////////////////////////////////////
// URL API
//////////////////////////////////////////////////////////////
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
- reloadLocation = null;
+ pendingLocation = null;
cacheState();
lastHistoryState = cachedState;
@@ -5081,8 +5852,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);
@@ -5091,14 +5862,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,"'");
}
};
@@ -5120,6 +5895,7 @@ function Browser(window, document, $log, $sniffer) {
urlChangeInit = false;
function cacheStateAndFireUrlChange() {
+ pendingLocation = null;
cacheState();
fireUrlChange();
}
@@ -5199,6 +5975,16 @@ function Browser(window, document, $log, $sniffer) {
};
/**
+ * @private
+ * Remove popstate and hashchange handler from window.
+ *
+ * NOTE: this api is intended for use only by $rootScope.
+ */
+ self.$$applicationDestroyed = function() {
+ jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange);
+ };
+
+ /**
* Checks whether the url has changed outside of Angular.
* Needs to be exported to be able to check for changes that have been done in sync,
* as hashchange/popstate events fire in async.
@@ -5223,89 +6009,6 @@ function Browser(window, document, $log, $sniffer) {
return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : '';
};
- //////////////////////////////////////////////////////////////
- // Cookies API
- //////////////////////////////////////////////////////////////
- var lastCookies = {};
- var lastCookieString = '';
- var cookiePath = self.baseHref();
-
- function safeDecodeURIComponent(str) {
- try {
- return decodeURIComponent(str);
- } catch (e) {
- return str;
- }
- }
-
- /**
- * @name $browser#cookies
- *
- * @param {string=} name Cookie name
- * @param {string=} value Cookie value
- *
- * @description
- * The cookies method provides a 'private' low level access to browser cookies.
- * It is not meant to be used directly, use the $cookie service instead.
- *
- * The return values vary depending on the arguments that the method was called with as follows:
- *
- * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify
- * it
- * - cookies(name, value) -> set name to value, if value is undefined delete the cookie
- * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that
- * way)
- *
- * @returns {Object} Hash of all cookies (if called without any parameter)
- */
- self.cookies = function(name, value) {
- var cookieLength, cookieArray, cookie, i, index;
-
- if (name) {
- if (value === undefined) {
- rawDocument.cookie = encodeURIComponent(name) + "=;path=" + cookiePath +
- ";expires=Thu, 01 Jan 1970 00:00:00 GMT";
- } else {
- if (isString(value)) {
- cookieLength = (rawDocument.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value) +
- ';path=' + cookiePath).length + 1;
-
- // per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum:
- // - 300 cookies
- // - 20 cookies per unique domain
- // - 4096 bytes per cookie
- if (cookieLength > 4096) {
- $log.warn("Cookie '" + name +
- "' possibly not set or overflowed because it was too large (" +
- cookieLength + " > 4096 bytes)!");
- }
- }
- }
- } else {
- if (rawDocument.cookie !== lastCookieString) {
- lastCookieString = rawDocument.cookie;
- cookieArray = lastCookieString.split("; ");
- lastCookies = {};
-
- for (i = 0; i < cookieArray.length; i++) {
- cookie = cookieArray[i];
- index = cookie.indexOf('=');
- if (index > 0) { //ignore nameless cookies
- name = safeDecodeURIComponent(cookie.substring(0, index));
- // the first value that is seen for a cookie is the most
- // specific one. values for the same cookie name that
- // follow are for less specific paths.
- if (lastCookies[name] === undefined) {
- lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
- }
- }
- }
- }
- return lastCookies;
- }
- };
-
-
/**
* @name $browser#defer
* @param {function()} fn A function, who's execution should be deferred.
@@ -5428,10 +6131,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>
@@ -5454,9 +6157,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;
@@ -5520,13 +6223,13 @@ function $CacheFactoryProvider() {
* @returns {*} the value stored.
*/
put: function(key, value) {
+ if (isUndefined(value)) return;
if (capacity < Number.MAX_VALUE) {
var lruEntry = lruHash[key] || (lruHash[key] = {key: key});
refresh(lruEntry);
}
- if (isUndefined(value)) return;
if (!(key in data)) size++;
data[key] = value;
@@ -5584,6 +6287,8 @@ function $CacheFactoryProvider() {
delete lruHash[key];
}
+ if (!(key in data)) return;
+
delete data[key];
size--;
},
@@ -5598,9 +6303,9 @@ function $CacheFactoryProvider() {
* Clears the cache object of any entries.
*/
removeAll: function() {
- data = {};
+ data = createMap();
size = 0;
- lruHash = {};
+ lruHash = createMap();
freshEnd = staleEnd = null;
},
@@ -5907,59 +6612,95 @@ 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:
+ *
+ * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
*
- * **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.
+ * * **`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.
*
- * 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:
+ * * **`{...}` (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
- * 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})`.
+ * 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` - 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
+ * 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. When the controller
* is instantiated, the initial values of the isolate scope bindings are already available.
*
+ * 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:
*
@@ -5998,9 +6739,11 @@ function $TemplateCacheProvider() {
*
*
* #### `controllerAs`
- * Controller alias at the directive scope. An alias for the controller so it
- * can be referenced at 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.
+ * Identifier name for a reference to the controller in the directive's scope.
+ * 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`
@@ -6111,7 +6854,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
@@ -6119,7 +6862,7 @@ function $TemplateCacheProvider() {
* `templateUrl` declaration or manual compilation inside the compile function.
* </div>
*
- * <div class="alert alert-error">
+ * <div class="alert alert-danger">
* **Note:** The `transclude` function that is passed to the compile function is deprecated, as it
* e.g. does not know about the right outer scope. Please use the transclude function that is passed
* to the link function instead.
@@ -6159,13 +6902,16 @@ function $TemplateCacheProvider() {
* * `controller` - the directive's required controller instance(s) - Instances are shared
* among all directives, which allows the directives to use the controllers as a communication
* channel. The exact value depends on the directive's `require` property:
+ * * no controller(s) required: the directive's own controller, or `undefined` if it doesn't have one
* * `string`: the controller instance
* * `array`: array of controller instances
- * * no controller(s) required: `undefined`
*
* If a required controller cannot be found, and it is optional, the instance is `null`,
* 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
+ * any other controller.
+ *
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
* This is the same as the `$transclude`
* parameter of directive controllers, see there for details.
@@ -6257,7 +7003,7 @@ function $TemplateCacheProvider() {
*
* <div class="alert alert-info">
* **Best Practice**: if you intend to add and remove transcluded content manually in your directive
- * (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it),
+ * (by calling the transclude function to get the DOM and calling `element.remove()` to remove it),
* then you are also responsible for calling `$destroy` on the transclusion scope.
* </div>
*
@@ -6287,19 +7033,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
@@ -6307,10 +7053,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
@@ -6381,8 +7126,8 @@ function $TemplateCacheProvider() {
}]);
</script>
<div ng-controller="GreeterController">
- <input ng-model="name"> <br>
- <textarea ng-model="html"></textarea> <br>
+ <input ng-model="name"> <br/>
+ <textarea ng-model="html"></textarea> <br/>
<div compile="html"></div>
</div>
</file>
@@ -6404,7 +7149,7 @@ function $TemplateCacheProvider() {
* @param {string|DOMElement} element Element or HTML string to compile into a template function.
* @param {function(angular.Scope, cloneAttachFn=)} transclude function available to directives - DEPRECATED.
*
- * <div class="alert alert-error">
+ * <div class="alert alert-danger">
* **Note:** Passing a `transclude` function to the $compile function is deprecated, as it
* e.g. will not use the right outer scope. Please pass the transclude function as a
* `parentBoundTranscludeFn` to the link function instead.
@@ -6419,7 +7164,7 @@ function $TemplateCacheProvider() {
* * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the
* `template` and call the `cloneAttachFn` function allowing the caller to attach the
* cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is
- * called as: <br> `cloneAttachFn(clonedElement, scope)` where:
+ * called as: <br/> `cloneAttachFn(clonedElement, scope)` where:
*
* * `clonedElement` - is a clone of the original `element` passed into the compiler.
* * `scope` - is the current scope with which the linking function is working with.
@@ -6431,8 +7176,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.
@@ -6491,20 +7243,27 @@ 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) {
+ function parseIsolateBindings(scope, directiveName, isController) {
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) {
throw $compileMinErr('iscp',
- "Invalid isolate scope definition for directive '{0}'." +
+ "Invalid {3} for directive '{0}'." +
" Definition: {... {1}: '{2}' ...}",
- directiveName, scopeName, definition);
+ directiveName, scopeName, definition,
+ (isController ? "controller bindings definition" :
+ "isolate scope definition"));
}
bindings[scopeName] = {
@@ -6513,17 +7272,61 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
optional: match[3] === '?',
attrName: match[4] || scopeName
};
+ if (match[4]) {
+ bindingCache[definition] = bindings[scopeName];
+ }
});
return bindings;
}
+ function parseDirectiveBindings(directive, directiveName) {
+ var bindings = {
+ isolateScope: null,
+ bindToController: null
+ };
+ if (isObject(directive.scope)) {
+ if (directive.bindToController === true) {
+ bindings.bindToController = parseIsolateBindings(directive.scope,
+ directiveName, true);
+ bindings.isolateScope = {};
+ } else {
+ bindings.isolateScope = parseIsolateBindings(directive.scope,
+ directiveName, false);
+ }
+ }
+ if (isObject(directive.bindToController)) {
+ bindings.bindToController =
+ parseIsolateBindings(directive.bindToController, directiveName, true);
+ }
+ if (isObject(bindings.bindToController)) {
+ var controller = directive.controller;
+ var controllerAs = directive.controllerAs;
+ if (!controller) {
+ // There is no controller, there may or may not be a controllerAs property
+ throw $compileMinErr('noctrl',
+ "Cannot bind to controller without directive '{0}'s controller.",
+ directiveName);
+ } else if (!identifierForController(controller, controllerAs)) {
+ // There is a controller, but no identifier or controllerAs property
+ throw $compileMinErr('noident',
+ "Cannot bind to controller without identifier for directive '{0}'.",
+ directiveName);
+ }
+ }
+ return bindings;
+ }
+
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);
}
- return name;
+ if (name !== name.trim()) {
+ throw $compileMinErr('baddir',
+ "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
+ name);
+ }
}
/**
@@ -6564,9 +7367,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directive.name = directive.name || name;
directive.require = directive.require || (directive.controller && directive.name);
directive.restrict = directive.restrict || 'EA';
- if (isObject(directive.scope)) {
- directive.$$isolateBindings = parseIsolateBindings(directive.scope, directive.name);
- }
+ directive.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
} catch (e) {
$exceptionHandler(e);
@@ -6675,9 +7476,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
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 Attributes = function(element, attributesToCopy) {
if (attributesToCopy) {
@@ -6788,7 +7589,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
- aliasedKey = getAliasedAttrName(node, key),
+ aliasedKey = getAliasedAttrName(key),
observer = key,
nodeName;
@@ -6855,7 +7656,7 @@ 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);
@@ -6889,7 +7690,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) {
@@ -6899,7 +7701,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]);
}
@@ -6924,12 +7726,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') || [];
@@ -6967,13 +7770,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);
@@ -6982,6 +7791,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,
@@ -7133,8 +7950,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (nodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(
- scope, nodeLinkFn.transclude, parentBoundTranscludeFn,
- nodeLinkFn.elementTranscludeOnThisElement);
+ scope, nodeLinkFn.transclude, parentBoundTranscludeFn);
} else if (!nodeLinkFn.templateOnThisElement && parentBoundTranscludeFn) {
childBoundTranscludeFn = parentBoundTranscludeFn;
@@ -7155,7 +7971,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
- function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn, elementTransclusion) {
+ function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
@@ -7215,13 +8031,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());
@@ -7254,6 +8068,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
break;
case NODE_TYPE_TEXT: /* Text Node */
+ if (msie === 11) {
+ // Workaround for #11781
+ while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
+ node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
+ node.parentNode.removeChild(node.nextSibling);
+ }
+ }
addTextInterpolateDirective(directives, node.nodeValue);
break;
case NODE_TYPE_COMMENT: /* Comment */
@@ -7353,9 +8174,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
previousCompileContext = previousCompileContext || {};
var terminalPriority = -Number.MAX_VALUE,
- newScopeDirective,
+ newScopeDirective = previousCompileContext.newScopeDirective,
controllerDirectives = previousCompileContext.controllerDirectives,
- controllers,
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective,
templateDirective = previousCompileContext.templateDirective,
nonTlbTranscludeDirective = previousCompileContext.nonTlbTranscludeDirective,
@@ -7413,7 +8233,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!directive.templateUrl && directive.controller) {
directiveValue = directive.controller;
- controllerDirectives = controllerDirectives || {};
+ controllerDirectives = controllerDirectives || createMap();
assertNoDuplicate("'" + directiveName + "' controller",
controllerDirectives[directiveName], directive, $compileNode);
controllerDirectives[directiveName] = directive;
@@ -7454,7 +8274,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
$template = jqLite(jqLiteClone(compileNode)).contents();
$compileNode.empty(); // clear contents
- childTranscludeFn = compile($template, transcludeFn);
+ childTranscludeFn = compile($template, transcludeFn, undefined,
+ undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
}
}
@@ -7496,8 +8317,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);
@@ -7520,6 +8344,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
+ newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
newIsolateScopeDirective: newIsolateScopeDirective,
templateDirective: templateDirective,
nonTlbTranscludeDirective: nonTlbTranscludeDirective
@@ -7547,7 +8372,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope === true;
nodeLinkFn.transcludeOnThisElement = hasTranscludeDirective;
- nodeLinkFn.elementTranscludeOnThisElement = hasElementTranscludeDirective;
nodeLinkFn.templateOnThisElement = hasTemplate;
nodeLinkFn.transclude = childTranscludeFn;
@@ -7581,55 +8405,75 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function getControllers(directiveName, require, $element, elementControllers) {
- var value, retrievalMethod = 'data', optional = false;
- var $searchElement = $element;
- var match;
- if (isString(require)) {
- match = require.match(REQUIRE_PREFIX_REGEXP);
- require = require.substring(match[0].length);
+ var value;
- if (match[3]) {
- if (match[1]) match[3] = null;
- else match[1] = match[3];
- }
- if (match[1] === '^') {
- retrievalMethod = 'inheritedData';
- } else if (match[1] === '^^') {
- retrievalMethod = 'inheritedData';
- $searchElement = $element.parent();
- }
- if (match[2] === '?') {
- optional = true;
+ 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;
}
- value = null;
-
- if (elementControllers && retrievalMethod === 'data') {
- if (value = elementControllers[require]) {
- value = value.instance;
- }
+ if (!value) {
+ var dataName = '$' + name + 'Controller';
+ value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
}
- value = value || $searchElement[retrievalMethod]('$' + require + 'Controller');
if (!value && !optional) {
throw $compileMinErr('ctreq',
"Controller '{0}', required by directive '{1}', can't be found!",
- require, directiveName);
+ name, directiveName);
}
- return value || null;
} else if (isArray(require)) {
value = [];
- forEach(require, function(require) {
- value.push(getControllers(directiveName, require, $element, elementControllers));
- });
+ for (var i = 0, ii = require.length; i < ii; i++) {
+ value[i] = getControllers(directiveName, require[i], $element, elementControllers);
+ }
}
- return value;
+
+ return value || null;
}
+ function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
+ var elementControllers = createMap();
+ for (var controllerKey in controllerDirectives) {
+ var directive = controllerDirectives[controllerKey];
+ var locals = {
+ $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
+ $element: $element,
+ $attrs: attrs,
+ $transclude: transcludeFn
+ };
+
+ var controller = directive.controller;
+ if (controller == '@') {
+ controller = attrs[directive.name];
+ }
+
+ var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
+
+ // For directives with element transclusion the element is a comment.
+ // In this case .data will not attach any data.
+ // Instead, we save the controllers for the element in a local hash and attach to .data
+ // later, once we have the actual element.
+ elementControllers[directive.name] = controllerInstance;
+ $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
+ }
+ return elementControllers;
+ }
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
- attrs;
+ var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
+ attrs, removeScopeBindingWatches, removeControllerBindingWatches;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -7639,8 +8483,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) {
@@ -7651,126 +8498,45 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (controllerDirectives) {
- // TODO: merge `controllers` and `elementControllers` into single object.
- controllers = {};
- elementControllers = {};
- forEach(controllerDirectives, function(directive) {
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- }, controllerInstance;
-
- controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- 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);
- }
-
- controllers[directive.name] = controllerInstance;
- });
+ elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
}
if (newIsolateScopeDirective) {
+ // Initialize isolate scope bindings for new isolate scope directive.
compile.$$addScopeInfo($element, isolateScope, true, !(templateDirective && (templateDirective === newIsolateScopeDirective ||
templateDirective === newIsolateScopeDirective.$$originalDirective)));
compile.$$addScopeClass($element, true);
-
- var isolateScopeController = controllers && controllers[newIsolateScopeDirective.name];
- var isolateBindingContext = isolateScope;
- if (isolateScopeController && isolateScopeController.identifier &&
- newIsolateScopeDirective.bindToController === true) {
- isolateBindingContext = isolateScopeController.instance;
+ isolateScope.$$isolateBindings =
+ newIsolateScopeDirective.$$isolateBindings;
+ removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
+ isolateScope.$$isolateBindings,
+ newIsolateScopeDirective);
+ if (removeScopeBindingWatches) {
+ isolateScope.$on('$destroy', removeScopeBindingWatches);
}
+ }
- forEach(isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings, function(definition, scopeName) {
- var attrName = definition.attrName,
- optional = definition.optional,
- mode = definition.mode, // @, =, or &
- lastValue,
- parentGet, parentSet, compare;
-
- switch (mode) {
-
- case '@':
- attrs.$observe(attrName, function(value) {
- isolateBindingContext[scopeName] = value;
- });
- attrs.$$observers[attrName].$$scope = scope;
- if (attrs[attrName]) {
- // If the attribute has been provided then we trigger an interpolation to ensure
- // the value is there for use in the link fn
- isolateBindingContext[scopeName] = $interpolate(attrs[attrName])(scope);
- }
- break;
+ // Initialize bindToController bindings
+ for (var name in elementControllers) {
+ var controllerDirective = controllerDirectives[name];
+ var controller = elementControllers[name];
+ var bindings = controllerDirective.$$bindings.bindToController;
- case '=':
- if (optional && !attrs[attrName]) {
- return;
- }
- parentGet = $parse(attrs[attrName]);
- if (parentGet.literal) {
- compare = equals;
- } else {
- compare = function(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 = isolateBindingContext[scopeName] = parentGet(scope);
- throw $compileMinErr('nonassign',
- "Expression '{0}' used with directive '{1}' is non-assignable!",
- attrs[attrName], newIsolateScopeDirective.name);
- };
- lastValue = isolateBindingContext[scopeName] = parentGet(scope);
- var parentValueWatch = function parentValueWatch(parentValue) {
- if (!compare(parentValue, isolateBindingContext[scopeName])) {
- // we are out of sync and need to copy
- if (!compare(parentValue, lastValue)) {
- // parent changed and it has precedence
- isolateBindingContext[scopeName] = parentValue;
- } else {
- // if the parent can be assigned then do so
- parentSet(scope, parentValue = isolateBindingContext[scopeName]);
- }
- }
- return lastValue = parentValue;
- };
- parentValueWatch.$stateful = true;
- var unwatch;
- if (definition.collection) {
- unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
- } else {
- unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
- }
- isolateScope.$on('$destroy', unwatch);
- break;
+ if (controller.identifier && bindings) {
+ removeControllerBindingWatches =
+ initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ }
- case '&':
- parentGet = $parse(attrs[attrName]);
- isolateBindingContext[scopeName] = function(locals) {
- return parentGet(scope, locals);
- };
- break;
- }
- });
- }
- if (controllers) {
- forEach(controllers, function(controller) {
- controller();
- });
- controllers = null;
+ 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);
+ }
}
// PRELINKING
@@ -7829,10 +8595,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
- function markDirectivesAsIsolate(directives) {
- // mark all directives as needing isolate scope.
+ // Depending upon the context in which a directive finds itself it might need to have a new isolated
+ // or child scope created. For instance:
+ // * if the directive has been pulled into a template because another directive with a higher priority
+ // asked for element transclusion
+ // * if the directive itself asks for transclusion but it is at the root of a template and the original
+ // element was replaced. See https://github.com/angular/angular.js/issues/12936
+ function markDirectiveScope(directives, isolateScope, newScope) {
for (var j = 0, jj = directives.length; j < jj; j++) {
- directives[j] = inherit(directives[j], {$$isolateScope: true});
+ directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
}
}
@@ -7859,11 +8630,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;
}
@@ -7979,7 +8757,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);
@@ -8061,11 +8841,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return a.index - b.index;
}
-
function assertNoDuplicate(what, previousDirective, directive, element) {
+
+ function wrapModuleNameIfDefined(moduleName) {
+ return moduleName ?
+ (' (module: ' + moduleName + ')') :
+ '';
+ }
+
if (previousDirective) {
- throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}',
- previousDirective.name, directive.name, what, startingTag(element));
+ throw $compileMinErr('multidir', 'Multiple directives [{0}{1}, {2}{3}] asking for {4} on: {5}',
+ previousDirective.name, wrapModuleNameIfDefined(previousDirective.$$moduleName),
+ directive.name, wrapModuleNameIfDefined(directive.$$moduleName), what, startingTag(element));
}
}
@@ -8147,7 +8934,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',
@@ -8246,26 +9033,28 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var fragment = document.createDocumentFragment();
fragment.appendChild(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());
-
- // 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]);
+ 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.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]);
+ }
}
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
@@ -8292,6 +9081,107 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
$exceptionHandler(e, startingTag($element));
}
}
+
+
+ // 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) {
+ var removeWatchCollection = [];
+ forEach(bindings, function(definition, scopeName) {
+ var attrName = definition.attrName,
+ optional = definition.optional,
+ mode = definition.mode, // @, =, or &
+ lastValue,
+ parentGet, parentSet, compare;
+
+ switch (mode) {
+
+ case '@':
+ if (!optional && !hasOwnProperty.call(attrs, attrName)) {
+ destination[scopeName] = attrs[attrName] = void 0;
+ }
+ attrs.$observe(attrName, function(value) {
+ if (isString(value)) {
+ destination[scopeName] = value;
+ }
+ });
+ attrs.$$observers[attrName].$$scope = scope;
+ 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(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 (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
+ }
+ 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); };
+ }
+ 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}' 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) {
+ if (!compare(parentValue, destination[scopeName])) {
+ // we are out of sync and need to copy
+ if (!compare(parentValue, lastValue)) {
+ // parent changed and it has precedence
+ destination[scopeName] = parentValue;
+ } else {
+ // if the parent can be assigned then do so
+ parentSet(scope, parentValue = destination[scopeName]);
+ }
+ }
+ return lastValue = parentValue;
+ };
+ parentValueWatch.$stateful = true;
+ var removeWatch;
+ if (definition.collection) {
+ removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
+ } else {
+ removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, 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;
+
+ destination[scopeName] = function(locals) {
+ return parentGet(scope, locals);
+ };
+ break;
+ }
+ });
+
+ return removeWatchCollection.length && function removeWatches() {
+ for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
+ removeWatchCollection[i]();
+ }
+ };
+ }
}];
}
@@ -8399,6 +9289,17 @@ function removeComments(jqNodes) {
var $controllerMinErr = minErr('$controller');
+
+var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
+function identifierForController(controller, ident) {
+ if (ident && isString(ident)) return ident;
+ if (isString(controller)) {
+ var match = CNTRL_REG.exec(controller);
+ if (match) return match[3];
+ }
+}
+
+
/**
* @ngdoc provider
* @name $controllerProvider
@@ -8411,9 +9312,7 @@ var $controllerMinErr = minErr('$controller');
*/
function $ControllerProvider() {
var controllers = {},
- globals = false,
- CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
-
+ globals = false;
/**
* @ngdoc method
@@ -8521,8 +9420,16 @@ function $ControllerProvider() {
addIdentifier(locals, identifier, instance, constructor || expression.name);
}
- return extend(function() {
- $injector.invoke(expression, instance, locals, constructor);
+ var instantiate;
+ return instantiate = extend(function() {
+ var result = $injector.invoke(expression, instance, locals, constructor);
+ if (result !== instance && (isObject(result) || isFunction(result))) {
+ instance = result;
+ if (identifier) {
+ // If result changed, re-assign controllerAs value to scope.
+ addIdentifier(locals, identifier, instance, constructor || expression.name);
+ }
+ }
return instance;
}, {
instance: instance,
@@ -8630,6 +9537,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 = /^\[|^\{(?!\{)/;
@@ -8638,6 +9568,129 @@ 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)) {
+ return isDate(v) ? v.toISOString() : toJson(v);
+ }
+ return v;
+}
+
+
+function $HttpParamSerializerProvider() {
+ /**
+ * @ngdoc service
+ * @name $httpParamSerializer
+ * @description
+ *
+ * Default {@link $http `$http`} params serializer that converts objects to strings
+ * according to the following rules:
+ *
+ * * `{'foo': 'bar'}` results in `foo=bar`
+ * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
+ * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
+ * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
+ *
+ * Note that serializer will sort the request parameters alphabetically.
+ * */
+
+ this.$get = function() {
+ return function ngParamSerializer(params) {
+ if (!params) return '';
+ var parts = [];
+ forEachSorted(params, function(value, key) {
+ if (value === null || isUndefined(value)) return;
+ if (isArray(value)) {
+ forEach(value, function(v, k) {
+ parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
+ });
+ } else {
+ parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
+ }
+ });
+
+ return parts.join('&');
+ };
+ };
+}
+
+function $HttpParamSerializerJQLikeProvider() {
+ /**
+ * @ngdoc service
+ * @name $httpParamSerializerJQLike
+ * @description
+ *
+ * Alternative {@link $http `$http`} params serializer that follows
+ * jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic.
+ * The serializer will also sort the params alphabetically.
+ *
+ * To use it for serializing `$http` request parameters, set it as the `paramSerializer` property:
+ *
+ * ```js
+ * $http({
+ * url: myUrl,
+ * method: 'GET',
+ * params: myParams,
+ * paramSerializer: '$httpParamSerializerJQLike'
+ * });
+ * ```
+ *
+ * It is also possible to set it as the default `paramSerializer` in the
+ * {@link $httpProvider#defaults `$httpProvider`}.
+ *
+ * Additionally, you can inject the serializer and use it explicitly, for example to serialize
+ * form data for submission:
+ *
+ * ```js
+ * .controller(function($http, $httpParamSerializerJQLike) {
+ * //...
+ *
+ * $http({
+ * url: myUrl,
+ * method: 'POST',
+ * data: $httpParamSerializerJQLike(myData),
+ * headers: {
+ * 'Content-Type': 'application/x-www-form-urlencoded'
+ * }
+ * });
+ *
+ * });
+ * ```
+ *
+ * */
+ this.$get = function() {
+ return function jQueryLikeParamSerializer(params) {
+ if (!params) return '';
+ var parts = [];
+ serialize(params, '', true);
+ return parts.join('&');
+
+ function serialize(toSerialize, prefix, topLevel) {
+ if (toSerialize === null || isUndefined(toSerialize)) return;
+ if (isArray(toSerialize)) {
+ forEach(toSerialize, function(value, index) {
+ serialize(value, prefix + '[' + (isObject(value) ? index : '') + ']');
+ });
+ } else if (isObject(toSerialize) && !isDate(toSerialize)) {
+ forEachSorted(toSerialize, function(value, key) {
+ serialize(value, prefix +
+ (topLevel ? '' : '[') +
+ key +
+ (topLevel ? '' : ']'));
+ });
+ } else {
+ parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
+ }
+ }
+ };
+ };
+}
function defaultHttpResponseTransform(data, headers) {
if (isString(data)) {
@@ -8667,19 +9720,24 @@ function isJsonLike(str) {
* @returns {Object} Parsed headers as key value object
*/
function parseHeaders(headers) {
- var parsed = createMap(), key, val, i;
-
- if (!headers) return parsed;
-
- forEach(headers.split('\n'), function(line) {
- i = line.indexOf(':');
- key = lowercase(trim(line.substr(0, i)));
- val = trim(line.substr(i + 1));
+ var parsed = createMap(), i;
+ function fillInParsed(key, val) {
if (key) {
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
}
- });
+ }
+
+ if (isString(headers)) {
+ forEach(headers.split('\n'), function(line) {
+ i = line.indexOf(':');
+ fillInParsed(lowercase(trim(line.substr(0, i))), trim(line.substr(i + 1)));
+ });
+ } else if (isObject(headers)) {
+ forEach(headers, function(headerVal, headerKey) {
+ fillInParsed(lowercase(headerKey), trim(headerVal));
+ });
+ }
return parsed;
}
@@ -8698,7 +9756,7 @@ function parseHeaders(headers) {
* - if called with no arguments returns an object containing all headers.
*/
function headersGetter(headers) {
- var headersObj = isObject(headers) ? headers : undefined;
+ var headersObj;
return function(name) {
if (!headersObj) headersObj = parseHeaders(headers);
@@ -8728,8 +9786,9 @@ function headersGetter(headers) {
* @returns {*} Transformed data.
*/
function transformData(data, headers, status, fns) {
- if (isFunction(fns))
+ if (isFunction(fns)) {
return fns(data, headers, status);
+ }
forEach(fns, function(fn) {
data = fn(data, headers, status);
@@ -8758,10 +9817,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 `default.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'`.
@@ -8777,6 +9835,12 @@ function $HttpProvider() {
* - **`defaults.headers.put`**
* - **`defaults.headers.patch`**
*
+ *
+ * - **`defaults.paramSerializer`** - `{string|function(Object<string,string>):string}` - A function
+ * used to the prepare string representation of request parameters (specified as an object).
+ * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
+ * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
+ *
**/
var defaults = this.defaults = {
// transform incoming response data
@@ -8798,7 +9862,9 @@ function $HttpProvider() {
},
xsrfCookieName: 'XSRF-TOKEN',
- xsrfHeaderName: 'X-XSRF-TOKEN'
+ xsrfHeaderName: 'X-XSRF-TOKEN',
+
+ paramSerializer: '$httpParamSerializer'
};
var useApplyAsync = false;
@@ -8812,7 +9878,7 @@ function $HttpProvider() {
* significant performance improvement for bigger applications that make many HTTP requests
* concurrently (common during application bootstrap).
*
- * Defaults to false. If no value is specifed, returns the current configured value.
+ * Defaults to false. If no value is specified, returns the current configured value.
*
* @param {boolean=} value If true, when requests are loaded, they will schedule a deferred
* "apply" on the next tick, giving time for subsequent requests in a roughly ~10ms window
@@ -8829,6 +9895,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
@@ -8844,12 +9934,18 @@ function $HttpProvider() {
**/
var interceptorFactories = this.interceptors = [];
- this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
- function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {
+ this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
+ function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
var defaultCache = $cacheFactory('$http');
/**
+ * Make sure that default param serializer is exposed as a function
+ */
+ defaults.paramSerializer = isString(defaults.paramSerializer) ?
+ $injector.get(defaults.paramSerializer) : defaults.paramSerializer;
+
+ /**
* Interceptors stored in reverse order. Inner interceptors before outer interceptors.
* The reversal is needed so that we can build up the interception chain around the
* server request.
@@ -8888,66 +9984,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`.
- *
- * ```js
- * // Simple GET request example :
- * $http.get('/someUrl').
- * 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 `$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 POST request example (passing data) :
- * $http.post('/someUrl', {msg:'hello word!'}).
- * 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.
* });
* ```
*
+ * 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:
@@ -8961,6 +10038,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
@@ -8977,7 +10073,7 @@ function $HttpProvider() {
* To add or overwrite these defaults, simply add or remove a property from these configuration
* objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
* with the lowercased HTTP method name as the key, e.g.
- * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }.
+ * `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`.
*
* The defaults can also be set at runtime via the `$http.defaults` object in the same
* fashion. For example:
@@ -9004,7 +10100,7 @@ function $HttpProvider() {
* data: { test: 'test' }
* }
*
- * $http(req).success(function(){...}).error(function(){...});
+ * $http(req).then(function(){...}, function(){...});
* ```
*
* ## Transforming Requests and Responses
@@ -9014,6 +10110,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
@@ -9071,26 +10176,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
*
@@ -9110,7 +10224,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
@@ -9212,13 +10326,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
@@ -9233,19 +10347,20 @@ function $HttpProvider() {
* properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
* or the per-request config object.
*
+ * In order to prevent collisions in environments where multiple Angular apps share the
+ * same domain or subdomain, we recommend that each application uses unique cookie name.
*
* @param {object} config Object describing the request to be made and how it should be
* processed. The object has following properties:
*
* - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
* - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
- * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned
- * to `?key1=value1&key2=value2` after the url. If the value is not a string, it will be
- * JSONified.
+ * - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
+ * with the `paramSerializer` and appended as GET parameters.
* - **data** – `{string|Object}` – Data to be sent as the request message data.
* - **headers** – `{Object}` – Map of strings or functions which return strings representing
* HTTP headers to send to the server. If the return value of a function is null, the
- * header will not be sent.
+ * header will not be sent. Functions accept a config object as an argument.
* - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
* - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
* - **transformRequest** –
@@ -9260,32 +10375,27 @@ function $HttpProvider() {
* 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 Transformations}
- * - **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.
+ * - **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
+ * {@link $injector $injector}, which means you can create your own serializer
+ * 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|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
* XHR object. See [requests with credentials](https://developer.mozilla.org/docs/Web/HTTP/Access_control_CORS#Requests_with_credentials)
* for more information.
* - **responseType** - `{string}` - see
- * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
+ * [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.
@@ -9295,11 +10405,11 @@ function $HttpProvider() {
<example module="httpExample">
<file name="index.html">
<div ng-controller="FetchController">
- <select ng-model="method">
+ <select ng-model="method" aria-label="Request method">
<option>GET</option>
<option>JSONP</option>
</select>
- <input type="text" ng-model="url" size="80"/>
+ <input type="text" ng-model="url" size="80" aria-label="URL" />
<button id="fetchbtn" ng-click="fetch()">fetch</button><br>
<button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
<button id="samplejsonpbtn"
@@ -9327,13 +10437,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;
});
};
@@ -9385,14 +10494,21 @@ function $HttpProvider() {
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,
- transformResponse: defaults.transformResponse
+ transformResponse: defaults.transformResponse,
+ paramSerializer: defaults.paramSerializer
}, requestConfig);
config.headers = mergeHeaders(requestConfig);
config.method = uppercase(config.method);
+ config.paramSerializer = isString(config.paramSerializer) ?
+ $injector.get(config.paramSerializer) : config.paramSerializer;
var serverRequest = function(config) {
var headers = config.headers;
@@ -9435,45 +10551,47 @@ 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);
}
- function executeHeaderFns(headers) {
+ function executeHeaderFns(headers, config) {
var headerContent, processedHeaders = {};
forEach(headers, function(headerFn, header) {
if (isFunction(headerFn)) {
- headerContent = headerFn();
+ headerContent = headerFn(config);
if (headerContent != null) {
processedHeaders[header] = headerContent;
}
@@ -9507,7 +10625,7 @@ function $HttpProvider() {
}
// execute if header value is a function for merged headers
- return executeHeaderFns(reqHeaders);
+ return executeHeaderFns(reqHeaders, shallowCopy(config));
}
}
@@ -9622,7 +10740,7 @@ function $HttpProvider() {
function createShortMethods(names) {
forEach(arguments, function(name) {
$http[name] = function(url, config) {
- return $http(extend(config || {}, {
+ return $http(extend({}, config || {}, {
method: name,
url: url
}));
@@ -9634,7 +10752,7 @@ function $HttpProvider() {
function createShortMethodsWithData(name) {
forEach(arguments, function(name) {
$http[name] = function(url, data, config) {
- return $http(extend(config || {}, {
+ return $http(extend({}, config || {}, {
method: name,
url: url,
data: data
@@ -9656,7 +10774,7 @@ function $HttpProvider() {
cache,
cachedResp,
reqHeaders = config.headers,
- url = buildUrl(config.url, config.params);
+ url = buildUrl(config.url, config.paramSerializer(config.params));
$http.pendingRequests.push(config);
promise.then(removePendingReq, removePendingReq);
@@ -9694,7 +10812,7 @@ function $HttpProvider() {
// send the request to the backend
if (isUndefined(cachedResp)) {
var xsrfValue = urlIsSameOrigin(config.url)
- ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
+ ? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
: undefined;
if (xsrfValue) {
reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
@@ -9740,8 +10858,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,
@@ -9763,35 +10881,42 @@ function $HttpProvider() {
}
- function buildUrl(url, params) {
- if (!params) return url;
- var parts = [];
- forEachSorted(params, function(value, key) {
- if (value === null || isUndefined(value)) return;
- if (!isArray(value)) value = [value];
-
- forEach(value, function(v) {
- if (isObject(v)) {
- if (isDate(v)) {
- v = v.toISOString();
- } else {
- v = toJson(v);
- }
- }
- parts.push(encodeUriQuery(key) + '=' +
- encodeUriQuery(v));
- });
- });
- if (parts.length > 0) {
- url += ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&');
+ function buildUrl(url, serializedParams) {
+ if (serializedParams.length > 0) {
+ url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams;
}
return url;
}
}];
}
-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();
+ };
+ };
}
/**
@@ -9799,6 +10924,7 @@ function createXhr() {
* @name $httpBackend
* @requires $window
* @requires $document
+ * @requires $xhrFactory
*
* @description
* HTTP backend used by the {@link ng.$http service} that delegates to
@@ -9811,8 +10937,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]);
}];
}
@@ -9836,7 +10962,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) {
@@ -9899,7 +11025,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
}
- xhr.send(post || null);
+ xhr.send(isUndefined(post) ? null : post);
}
if (timeout > 0) {
@@ -9916,7 +11042,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;
@@ -9963,7 +11089,17 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
}
-var $interpolateMinErr = minErr('$interpolate');
+var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
+$interpolateMinErr.throwNoconcat = function(text) {
+ throw $interpolateMinErr('noconcat',
+ "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
+ "interpolations that concatenate multiple expressions when a trusted value is " +
+ "required. See http://docs.angularjs.org/api/ng.$sce", text);
+};
+
+$interpolateMinErr.interr = function(text, err) {
+ return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
+};
/**
* @ngdoc provider
@@ -9974,7 +11110,7 @@ var $interpolateMinErr = minErr('$interpolate');
* Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
*
* @example
-<example module="customInterpolationApp">
+<example name="custom-interpolation-markup" module="customInterpolationApp">
<file name="index.html">
<script>
var customInterpolationApp = angular.module('customInterpolationApp', []);
@@ -9989,7 +11125,7 @@ var $interpolateMinErr = minErr('$interpolate');
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>
@@ -10051,6 +11187,28 @@ function $InterpolateProvider() {
return '\\\\\\' + ch;
}
+ function unescapeText(text) {
+ return text.replace(escapedStartRegexp, startSymbol).
+ replace(escapedEndRegexp, endSymbol);
+ }
+
+ function stringify(value) {
+ if (value == null) { // null || undefined
+ return '';
+ }
+ switch (typeof value) {
+ case 'string':
+ break;
+ case 'number':
+ value = '' + value;
+ break;
+ default:
+ value = toJson(value);
+ }
+
+ return value;
+ }
+
/**
* @ngdoc service
* @name $interpolate
@@ -10070,7 +11228,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
@@ -10185,10 +11343,7 @@ function $InterpolateProvider() {
// make it obvious that you bound the value to some user controlled value. This helps reduce
// the load when auditing for XSS issues.
if (trustedContext && concat.length > 1) {
- throw $interpolateMinErr('noconcat',
- "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
- "interpolations that concatenate multiple expressions when a trusted value is " +
- "required. See http://docs.angularjs.org/api/ng.$sce", text);
+ $interpolateMinErr.throwNoconcat(text);
}
if (!mustHaveExpression || expressions.length) {
@@ -10206,23 +11361,6 @@ function $InterpolateProvider() {
$sce.valueOf(value);
};
- var stringify = function(value) {
- if (value == null) { // null || undefined
- return '';
- }
- switch (typeof value) {
- case 'string':
- break;
- case 'number':
- value = '' + value;
- break;
- default:
- value = toJson(value);
- }
-
- return value;
- };
-
return extend(function interpolationFn(context) {
var i = 0;
var ii = expressions.length;
@@ -10235,16 +11373,14 @@ function $InterpolateProvider() {
return compute(values);
} catch (err) {
- var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
- err.toString());
- $exceptionHandler(newErr);
+ $exceptionHandler($interpolateMinErr.interr(text, err));
}
}, {
// all of these properties are undocumented for now
exp: text, //just for compatibility with regular watchers created via $watch
expressions: expressions,
- $$watchDelegate: function(scope, listener, objectEquality) {
+ $$watchDelegate: function(scope, listener) {
var lastValue;
return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
var currValue = compute(values);
@@ -10252,24 +11388,17 @@ function $InterpolateProvider() {
listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
}
lastValue = currValue;
- }, objectEquality);
+ });
}
});
}
- function unescapeText(text) {
- return text.replace(escapedStartRegexp, startSymbol).
- replace(escapedEndRegexp, endSymbol);
- }
-
function parseStringifyInterceptor(value) {
try {
value = getValue(value);
return allOrNothing && !isDefined(value) ? value : stringify(value);
} catch (err) {
- var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text,
- err.toString());
- $exceptionHandler(newErr);
+ $exceptionHandler($interpolateMinErr.interr(text, err));
}
}
}
@@ -10348,6 +11477,7 @@ function $IntervalProvider() {
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed function.
* @returns {promise} A promise which will be notified on each iteration.
*
* @example
@@ -10426,7 +11556,7 @@ function $IntervalProvider() {
*
* <div>
* <div ng-controller="ExampleController">
- * Date format: <input ng-model="format"> <hr/>
+ * <label>Date format: <input ng-model="format"></label> <hr/>
* Current time is: <span my-current-time="format"></span>
* <hr/>
* Blood 1 : <font color='red'>{{blood_1}}</font>
@@ -10441,7 +11571,9 @@ function $IntervalProvider() {
* </example>
*/
function interval(fn, delay, count, invokeApply) {
- var setInterval = $window.setInterval,
+ var hasParams = arguments.length > 4,
+ args = hasParams ? sliceArgs(arguments, 4) : [],
+ setInterval = $window.setInterval,
clearInterval = $window.clearInterval,
iteration = 0,
skipApply = (isDefined(invokeApply) && !invokeApply),
@@ -10450,7 +11582,9 @@ function $IntervalProvider() {
count = isDefined(count) ? count : 0;
- promise.then(null, null, fn);
+ promise.then(null, null, (!hasParams) ? fn : function() {
+ fn.apply(null, args);
+ });
promise.$$intervalId = setInterval(function tick() {
deferred.notify(iteration++);
@@ -10478,7 +11612,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) {
@@ -10505,75 +11639,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};
@@ -10602,7 +11667,7 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) {
locationObj.$$protocol = parsedUrl.protocol;
locationObj.$$host = parsedUrl.hostname;
- locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
+ locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}
@@ -10664,12 +11729,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);
@@ -10716,14 +11781,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;
@@ -10743,10 +11808,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);
@@ -10855,14 +11920,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:
@@ -10956,8 +12020,9 @@ var locationPrototype = {
* @return {string} url
*/
url: function(url) {
- if (isUndefined(url))
+ if (isUndefined(url)) {
return this.$$url;
+ }
var match = PATH_MATCH.exec(url);
if (match[1] || url === '') this.path(decodeURIComponent(match[1]));
@@ -11149,9 +12214,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
@@ -11172,8 +12237,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;
@@ -11204,8 +12269,9 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
* @return {object} state
*/
Location.prototype.state = function(state) {
- if (!arguments.length)
+ if (!arguments.length) {
return this.$$state;
+ }
if (Location !== LocationHtml5Url || !this.$$html5) {
throw $locationMinErr('nostate', 'History API state support is available only ' +
@@ -11230,8 +12296,9 @@ function locationGetter(property) {
function locationGetterSetter(property, preprocess) {
return function(value) {
- if (isUndefined(value))
+ if (isUndefined(value)) {
return this[property];
+ }
this[property] = preprocess(value);
this.$$compose();
@@ -11398,7 +12465,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();
@@ -11478,11 +12547,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;
@@ -11580,8 +12656,8 @@ function $LocationProvider() {
<file name="index.html">
<div ng-controller="LogController">
<p>Reload this page with open console, enter text and hit the log button...</p>
- Message:
- <input type="text" ng-model="message"/>
+ <label>Message:
+ <input type="text" ng-model="message" /></label>
<button ng-click="$log.log(message)">log</button>
<button ng-click="$log.warn(message)">warn</button>
<button ng-click="$log.info(message)">info</button>
@@ -11765,6 +12841,25 @@ function ensureSafeMemberName(name, fullExpression) {
return name;
}
+function getStringValue(name, fullExpression) {
+ // From the JavaScript docs:
+ // 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.
+ //
+ // So, to ensure that we are checking the same `name` that JavaScript would use,
+ // we cast it to a string, if possible.
+ // Doing `name + ''` can cause a repl error if the result to `toString` is not a string,
+ // this is, this will handle objects that misbehave.
+ name = name + '';
+ if (!isString(name)) {
+ throw $parseMinErr('iseccst',
+ 'Cannot convert object to primitive value! '
+ + 'Expression: {0}', fullExpression);
+ }
+ return name;
+}
+
function ensureSafeObject(obj, fullExpression) {
// nifty check if obj is Function that is fast and works across iframes and other contexts
if (obj) {
@@ -11810,57 +12905,18 @@ function ensureSafeFunction(obj, fullExpression) {
}
}
-//Keyword constants
-var CONSTANTS = createMap();
-forEach({
- 'null': function() { return null; },
- 'true': function() { return true; },
- 'false': function() { return false; },
- 'undefined': function() {}
-}, function(constantGetter, name) {
- constantGetter.constant = constantGetter.literal = constantGetter.sharedGetter = true;
- CONSTANTS[name] = constantGetter;
-});
-
-//Not quite a constant, but can be lex/parsed the same
-CONSTANTS['this'] = function(self) { return self; };
-CONSTANTS['this'].sharedGetter = true;
-
+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);
+ }
+ }
+}
-//Operators - will be wrapped by binaryFn/unaryFn/assignment/filter
-var OPERATORS = extend(createMap(), {
- '+':function(self, locals, a, b) {
- a=a(self, locals); b=b(self, locals);
- if (isDefined(a)) {
- if (isDefined(b)) {
- return a + b;
- }
- return a;
- }
- return isDefined(b) ? b : undefined;},
- '-':function(self, locals, a, b) {
- a=a(self, locals); b=b(self, locals);
- return (isDefined(a) ? a : 0) - (isDefined(b) ? b : 0);
- },
- '*':function(self, locals, a, b) {return a(self, locals) * b(self, locals);},
- '/':function(self, locals, a, b) {return a(self, locals) / b(self, locals);},
- '%':function(self, locals, a, b) {return a(self, locals) % b(self, locals);},
- '===':function(self, locals, a, b) {return a(self, locals) === b(self, locals);},
- '!==':function(self, locals, a, b) {return a(self, locals) !== b(self, locals);},
- '==':function(self, locals, a, b) {return a(self, locals) == b(self, locals);},
- '!=':function(self, locals, a, b) {return a(self, locals) != b(self, locals);},
- '<':function(self, locals, a, b) {return a(self, locals) < b(self, locals);},
- '>':function(self, locals, a, b) {return a(self, locals) > b(self, locals);},
- '<=':function(self, locals, a, b) {return a(self, locals) <= b(self, locals);},
- '>=':function(self, locals, a, b) {return a(self, locals) >= b(self, locals);},
- '&&':function(self, locals, a, b) {return a(self, locals) && b(self, locals);},
- '||':function(self, locals, a, b) {return a(self, locals) || b(self, locals);},
- '!':function(self, locals, a) {return !a(self, locals);},
-
- //Tokenized as operators but parsed as assignment/filters
- '=':true,
- '|':true
-});
+var OPERATORS = createMap();
+forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
@@ -12012,8 +13068,9 @@ Lexer.prototype = {
if (escape) {
if (ch === 'u') {
var hex = this.text.substring(this.index + 1, this.index + 5);
- if (!hex.match(/[\da-f]{4}/i))
+ if (!hex.match(/[\da-f]{4}/i)) {
this.throwError('Invalid unicode escape [\\u' + hex + ']');
+ }
this.index += 4;
string += String.fromCharCode(parseInt(hex, 16));
} else {
@@ -12041,194 +13098,59 @@ Lexer.prototype = {
}
};
-
-function isConstant(exp) {
- return exp.constant;
-}
-
-/**
- * @constructor
- */
-var Parser = function(lexer, $filter, options) {
+var AST = function(lexer, options) {
this.lexer = lexer;
- this.$filter = $filter;
this.options = options;
};
-Parser.ZERO = extend(function() {
- return 0;
-}, {
- sharedGetter: true,
- constant: true
-});
-
-Parser.prototype = {
- constructor: Parser,
-
- parse: function(text) {
+AST.Program = 'Program';
+AST.ExpressionStatement = 'ExpressionStatement';
+AST.AssignmentExpression = 'AssignmentExpression';
+AST.ConditionalExpression = 'ConditionalExpression';
+AST.LogicalExpression = 'LogicalExpression';
+AST.BinaryExpression = 'BinaryExpression';
+AST.UnaryExpression = 'UnaryExpression';
+AST.CallExpression = 'CallExpression';
+AST.MemberExpression = 'MemberExpression';
+AST.Identifier = 'Identifier';
+AST.Literal = 'Literal';
+AST.ArrayExpression = 'ArrayExpression';
+AST.Property = 'Property';
+AST.ObjectExpression = 'ObjectExpression';
+AST.ThisExpression = 'ThisExpression';
+
+// Internal use only
+AST.NGValueParameter = 'NGValueParameter';
+
+AST.prototype = {
+ ast: function(text) {
this.text = text;
this.tokens = this.lexer.lex(text);
- var value = this.statements();
+ var value = this.program();
if (this.tokens.length !== 0) {
this.throwError('is an unexpected token', this.tokens[0]);
}
- value.literal = !!value.literal;
- value.constant = !!value.constant;
-
return value;
},
- primary: function() {
- var primary;
- if (this.expect('(')) {
- primary = this.filterChain();
- this.consume(')');
- } else if (this.expect('[')) {
- primary = this.arrayDeclaration();
- } else if (this.expect('{')) {
- primary = this.object();
- } else if (this.peek().identifier && this.peek().text in CONSTANTS) {
- primary = CONSTANTS[this.consume().text];
- } else if (this.peek().identifier) {
- primary = this.identifier();
- } else if (this.peek().constant) {
- primary = this.constant();
- } else {
- this.throwError('not a primary expression', this.peek());
- }
-
- var next, context;
- while ((next = this.expect('(', '[', '.'))) {
- if (next.text === '(') {
- primary = this.functionCall(primary, context);
- context = null;
- } else if (next.text === '[') {
- context = primary;
- primary = this.objectIndex(primary);
- } else if (next.text === '.') {
- context = primary;
- primary = this.fieldAccess(primary);
- } else {
- this.throwError('IMPOSSIBLE');
- }
- }
- return primary;
- },
-
- throwError: function(msg, token) {
- throw $parseMinErr('syntax',
- 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].',
- token.text, msg, (token.index + 1), this.text, this.text.substring(token.index));
- },
-
- peekToken: function() {
- if (this.tokens.length === 0)
- throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
- return this.tokens[0];
- },
-
- peek: function(e1, e2, e3, e4) {
- return this.peekAhead(0, e1, e2, e3, e4);
- },
- peekAhead: function(i, e1, e2, e3, e4) {
- if (this.tokens.length > i) {
- var token = this.tokens[i];
- var t = token.text;
- if (t === e1 || t === e2 || t === e3 || t === e4 ||
- (!e1 && !e2 && !e3 && !e4)) {
- return token;
- }
- }
- return false;
- },
-
- expect: function(e1, e2, e3, e4) {
- var token = this.peek(e1, e2, e3, e4);
- if (token) {
- this.tokens.shift();
- return token;
- }
- return false;
- },
-
- consume: function(e1) {
- if (this.tokens.length === 0) {
- throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
- }
-
- var token = this.expect(e1);
- if (!token) {
- this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
- }
- return token;
- },
-
- unaryFn: function(op, right) {
- var fn = OPERATORS[op];
- return extend(function $parseUnaryFn(self, locals) {
- return fn(self, locals, right);
- }, {
- constant:right.constant,
- inputs: [right]
- });
- },
-
- binaryFn: function(left, op, right, isBranching) {
- var fn = OPERATORS[op];
- return extend(function $parseBinaryFn(self, locals) {
- return fn(self, locals, left, right);
- }, {
- constant: left.constant && right.constant,
- inputs: !isBranching && [left, right]
- });
- },
-
- identifier: function() {
- var id = this.consume().text;
-
- //Continue reading each `.identifier` unless it is a method invocation
- while (this.peek('.') && this.peekAhead(1).identifier && !this.peekAhead(2, '(')) {
- id += this.consume().text + this.consume().text;
- }
-
- return getterFn(id, this.options, this.text);
- },
-
- constant: function() {
- var value = this.consume().value;
-
- return extend(function $parseConstant() {
- return value;
- }, {
- constant: true,
- literal: true
- });
- },
-
- statements: function() {
- var statements = [];
+ program: function() {
+ var body = [];
while (true) {
if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
- statements.push(this.filterChain());
+ body.push(this.expressionStatement());
if (!this.expect(';')) {
- // optimize for the common case where there is only one statement.
- // TODO(size): maybe we should not support multiple statements?
- return (statements.length === 1)
- ? statements[0]
- : function $parseStatements(self, locals) {
- var value;
- for (var i = 0, ii = statements.length; i < ii; i++) {
- value = statements[i](self, locals);
- }
- return value;
- };
+ return { type: AST.Program, body: body};
}
}
},
+ expressionStatement: function() {
+ return { type: AST.ExpressionStatement, expression: this.filterChain() };
+ },
+
filterChain: function() {
var left = this.expression();
var token;
@@ -12238,98 +13160,44 @@ Parser.prototype = {
return left;
},
- filter: function(inputFn) {
- var fn = this.$filter(this.consume().text);
- var argsFn;
- var args;
-
- if (this.peek(':')) {
- argsFn = [];
- args = []; // we can safely reuse the array
- while (this.expect(':')) {
- argsFn.push(this.expression());
- }
- }
-
- var inputs = [inputFn].concat(argsFn || []);
-
- return extend(function $parseFilter(self, locals) {
- var input = inputFn(self, locals);
- if (args) {
- args[0] = input;
-
- var i = argsFn.length;
- while (i--) {
- args[i + 1] = argsFn[i](self, locals);
- }
-
- return fn.apply(undefined, args);
- }
-
- return fn(input);
- }, {
- constant: !fn.$stateful && inputs.every(isConstant),
- inputs: !fn.$stateful && inputs
- });
- },
-
expression: function() {
return this.assignment();
},
assignment: function() {
- var left = this.ternary();
- var right;
- var token;
- if ((token = this.expect('='))) {
- if (!left.assign) {
- this.throwError('implies assignment but [' +
- this.text.substring(0, token.index) + '] can not be assigned to', token);
- }
- right = this.ternary();
- return extend(function $parseAssignment(scope, locals) {
- return left.assign(scope, right(scope, locals), locals);
- }, {
- inputs: [left, right]
- });
+ var result = this.ternary();
+ if (this.expect('=')) {
+ result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
}
- return left;
+ return result;
},
ternary: function() {
- var left = this.logicalOR();
- var middle;
- var token;
- if ((token = this.expect('?'))) {
- middle = this.assignment();
+ var test = this.logicalOR();
+ var alternate;
+ var consequent;
+ if (this.expect('?')) {
+ alternate = this.expression();
if (this.consume(':')) {
- var right = this.assignment();
-
- return extend(function $parseTernary(self, locals) {
- return left(self, locals) ? middle(self, locals) : right(self, locals);
- }, {
- constant: left.constant && middle.constant && right.constant
- });
+ consequent = this.expression();
+ return { type: AST.ConditionalExpression, test: test, alternate: alternate, consequent: consequent};
}
}
-
- return left;
+ return test;
},
logicalOR: function() {
var left = this.logicalAND();
- var token;
- while ((token = this.expect('||'))) {
- left = this.binaryFn(left, token.text, this.logicalAND(), true);
+ while (this.expect('||')) {
+ left = { type: AST.LogicalExpression, operator: '||', left: left, right: this.logicalAND() };
}
return left;
},
logicalAND: function() {
var left = this.equality();
- var token;
- while ((token = this.expect('&&'))) {
- left = this.binaryFn(left, token.text, this.equality(), true);
+ while (this.expect('&&')) {
+ left = { type: AST.LogicalExpression, operator: '&&', left: left, right: this.equality()};
}
return left;
},
@@ -12338,7 +13206,7 @@ Parser.prototype = {
var left = this.relational();
var token;
while ((token = this.expect('==','!=','===','!=='))) {
- left = this.binaryFn(left, token.text, this.relational());
+ left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.relational() };
}
return left;
},
@@ -12347,7 +13215,7 @@ Parser.prototype = {
var left = this.additive();
var token;
while ((token = this.expect('<', '>', '<=', '>='))) {
- left = this.binaryFn(left, token.text, this.additive());
+ left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.additive() };
}
return left;
},
@@ -12356,7 +13224,7 @@ Parser.prototype = {
var left = this.multiplicative();
var token;
while ((token = this.expect('+','-'))) {
- left = this.binaryFn(left, token.text, this.multiplicative());
+ left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.multiplicative() };
}
return left;
},
@@ -12365,318 +13233,1237 @@ Parser.prototype = {
var left = this.unary();
var token;
while ((token = this.expect('*','/','%'))) {
- left = this.binaryFn(left, token.text, this.unary());
+ left = { type: AST.BinaryExpression, operator: token.text, left: left, right: this.unary() };
}
return left;
},
unary: function() {
var token;
- if (this.expect('+')) {
- return this.primary();
- } else if ((token = this.expect('-'))) {
- return this.binaryFn(Parser.ZERO, token.text, this.unary());
- } else if ((token = this.expect('!'))) {
- return this.unaryFn(token.text, this.unary());
+ if ((token = this.expect('+', '-', '!'))) {
+ return { type: AST.UnaryExpression, operator: token.text, prefix: true, argument: this.unary() };
} else {
return this.primary();
}
},
- fieldAccess: function(object) {
- var getter = this.identifier();
+ primary: function() {
+ var primary;
+ if (this.expect('(')) {
+ primary = this.filterChain();
+ this.consume(')');
+ } else if (this.expect('[')) {
+ 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.peek().identifier) {
+ primary = this.identifier();
+ } else if (this.peek().constant) {
+ primary = this.constant();
+ } else {
+ this.throwError('not a primary expression', this.peek());
+ }
- return extend(function $parseFieldAccess(scope, locals, self) {
- var o = self || object(scope, locals);
- return (o == null) ? undefined : getter(o);
- }, {
- assign: function(scope, value, locals) {
- var o = object(scope, locals);
- if (!o) object.assign(scope, o = {}, locals);
- return getter.assign(o, value);
+ var next;
+ while ((next = this.expect('(', '[', '.'))) {
+ if (next.text === '(') {
+ primary = {type: AST.CallExpression, callee: primary, arguments: this.parseArguments() };
+ this.consume(')');
+ } else if (next.text === '[') {
+ primary = { type: AST.MemberExpression, object: primary, property: this.expression(), computed: true };
+ this.consume(']');
+ } else if (next.text === '.') {
+ primary = { type: AST.MemberExpression, object: primary, property: this.identifier(), computed: false };
+ } else {
+ this.throwError('IMPOSSIBLE');
}
- });
+ }
+ return primary;
},
- objectIndex: function(obj) {
- var expression = this.text;
+ filter: function(baseExpression) {
+ var args = [baseExpression];
+ var result = {type: AST.CallExpression, callee: this.identifier(), arguments: args, filter: true};
- var indexFn = this.expression();
- this.consume(']');
+ while (this.expect(':')) {
+ args.push(this.expression());
+ }
- return extend(function $parseObjectIndex(self, locals) {
- var o = obj(self, locals),
- i = indexFn(self, locals),
- v;
-
- ensureSafeMemberName(i, expression);
- if (!o) return undefined;
- v = ensureSafeObject(o[i], expression);
- return v;
- }, {
- assign: function(self, value, locals) {
- var key = ensureSafeMemberName(indexFn(self, locals), expression);
- // prevent overwriting of Function.constructor which would break ensureSafeObject check
- var o = ensureSafeObject(obj(self, locals), expression);
- if (!o) obj.assign(self, o = {}, locals);
- return o[key] = value;
- }
- });
+ return result;
},
- functionCall: function(fnGetter, contextGetter) {
- var argsFn = [];
+ parseArguments: function() {
+ var args = [];
if (this.peekToken().text !== ')') {
do {
- argsFn.push(this.expression());
+ args.push(this.expression());
} while (this.expect(','));
}
- this.consume(')');
-
- var expressionText = this.text;
- // we can safely reuse the array across invocations
- var args = argsFn.length ? [] : null;
-
- return function $parseFunctionCall(scope, locals) {
- var context = contextGetter ? contextGetter(scope, locals) : isDefined(contextGetter) ? undefined : scope;
- var fn = fnGetter(scope, locals, context) || noop;
-
- if (args) {
- var i = argsFn.length;
- while (i--) {
- args[i] = ensureSafeObject(argsFn[i](scope, locals), expressionText);
- }
- }
-
- ensureSafeObject(context, expressionText);
- ensureSafeFunction(fn, expressionText);
-
- // IE doesn't have apply for some native functions
- var v = fn.apply
- ? fn.apply(context, args)
- : fn(args[0], args[1], args[2], args[3], args[4]);
+ return args;
+ },
- if (args) {
- // Free-up the memory (arguments of the last function call).
- args.length = 0;
- }
+ identifier: function() {
+ var token = this.consume();
+ if (!token.identifier) {
+ this.throwError('is not a valid identifier', token);
+ }
+ return { type: AST.Identifier, name: token.text };
+ },
- return ensureSafeObject(v, expressionText);
- };
+ constant: function() {
+ // TODO check that it is a constant
+ return { type: AST.Literal, value: this.consume().value };
},
- // This is used with json array declaration
arrayDeclaration: function() {
- var elementFns = [];
+ var elements = [];
if (this.peekToken().text !== ']') {
do {
if (this.peek(']')) {
// Support trailing commas per ES5.1.
break;
}
- elementFns.push(this.expression());
+ elements.push(this.expression());
} while (this.expect(','));
}
this.consume(']');
- return extend(function $parseArrayLiteral(self, locals) {
- var array = [];
- for (var i = 0, ii = elementFns.length; i < ii; i++) {
- array.push(elementFns[i](self, locals));
- }
- return array;
- }, {
- literal: true,
- constant: elementFns.every(isConstant),
- inputs: elementFns
- });
+ return { type: AST.ArrayExpression, elements: elements };
},
object: function() {
- var keys = [], valueFns = [];
+ var properties = [], property;
if (this.peekToken().text !== '}') {
do {
if (this.peek('}')) {
// Support trailing commas per ES5.1.
break;
}
- var token = this.consume();
- if (token.constant) {
- keys.push(token.value);
- } else if (token.identifier) {
- keys.push(token.text);
+ property = {type: AST.Property, kind: 'init'};
+ if (this.peek().constant) {
+ property.key = this.constant();
+ } else if (this.peek().identifier) {
+ property.key = this.identifier();
} else {
- this.throwError("invalid key", token);
+ this.throwError("invalid key", this.peek());
}
this.consume(':');
- valueFns.push(this.expression());
+ property.value = this.expression();
+ properties.push(property);
} while (this.expect(','));
}
this.consume('}');
- return extend(function $parseObjectLiteral(self, locals) {
- var object = {};
- for (var i = 0, ii = valueFns.length; i < ii; i++) {
- object[keys[i]] = valueFns[i](self, locals);
+ return {type: AST.ObjectExpression, properties: properties };
+ },
+
+ throwError: function(msg, token) {
+ throw $parseMinErr('syntax',
+ 'Syntax Error: Token \'{0}\' {1} at column {2} of the expression [{3}] starting at [{4}].',
+ token.text, msg, (token.index + 1), this.text, this.text.substring(token.index));
+ },
+
+ consume: function(e1) {
+ if (this.tokens.length === 0) {
+ throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
+ }
+
+ var token = this.expect(e1);
+ if (!token) {
+ this.throwError('is unexpected, expecting [' + e1 + ']', this.peek());
+ }
+ return token;
+ },
+
+ peekToken: function() {
+ if (this.tokens.length === 0) {
+ throw $parseMinErr('ueoe', 'Unexpected end of expression: {0}', this.text);
+ }
+ return this.tokens[0];
+ },
+
+ peek: function(e1, e2, e3, e4) {
+ return this.peekAhead(0, e1, e2, e3, e4);
+ },
+
+ peekAhead: function(i, e1, e2, e3, e4) {
+ if (this.tokens.length > i) {
+ var token = this.tokens[i];
+ var t = token.text;
+ if (t === e1 || t === e2 || t === e3 || t === e4 ||
+ (!e1 && !e2 && !e3 && !e4)) {
+ return token;
}
- return object;
- }, {
- literal: true,
- constant: valueFns.every(isConstant),
- inputs: valueFns
- });
+ }
+ return false;
+ },
+
+ expect: function(e1, e2, e3, e4) {
+ var token = this.peek(e1, e2, e3, e4);
+ if (token) {
+ this.tokens.shift();
+ return token;
+ }
+ 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 }
}
};
+function ifDefined(v, d) {
+ return typeof v !== 'undefined' ? v : d;
+}
-//////////////////////////////////////////////////
-// Parser helper functions
-//////////////////////////////////////////////////
+function plusFn(l, r) {
+ if (typeof l === 'undefined') return r;
+ if (typeof r === 'undefined') return l;
+ return l + r;
+}
-function setter(obj, locals, path, setValue, fullExp) {
- ensureSafeObject(obj, fullExp);
- ensureSafeObject(locals, fullExp);
+function isStateless($filter, filterName) {
+ var fn = $filter(filterName);
+ return !fn.$stateful;
+}
- var element = path.split('.'), key;
- for (var i = 0; element.length > 1; i++) {
- key = ensureSafeMemberName(element.shift(), fullExp);
- var propertyObj = (i === 0 && locals && locals[key]) || obj[key];
- if (!propertyObj) {
- propertyObj = {};
- obj[key] = propertyObj;
- }
- obj = ensureSafeObject(propertyObj, fullExp);
+function findConstantAndWatchExpressions(ast, $filter) {
+ var allConstants;
+ var argsToWatch;
+ switch (ast.type) {
+ case AST.Program:
+ allConstants = true;
+ forEach(ast.body, function(expr) {
+ findConstantAndWatchExpressions(expr.expression, $filter);
+ allConstants = allConstants && expr.expression.constant;
+ });
+ ast.constant = allConstants;
+ break;
+ case AST.Literal:
+ ast.constant = true;
+ ast.toWatch = [];
+ break;
+ case AST.UnaryExpression:
+ findConstantAndWatchExpressions(ast.argument, $filter);
+ ast.constant = ast.argument.constant;
+ ast.toWatch = ast.argument.toWatch;
+ break;
+ case AST.BinaryExpression:
+ findConstantAndWatchExpressions(ast.left, $filter);
+ findConstantAndWatchExpressions(ast.right, $filter);
+ ast.constant = ast.left.constant && ast.right.constant;
+ ast.toWatch = ast.left.toWatch.concat(ast.right.toWatch);
+ break;
+ case AST.LogicalExpression:
+ findConstantAndWatchExpressions(ast.left, $filter);
+ findConstantAndWatchExpressions(ast.right, $filter);
+ ast.constant = ast.left.constant && ast.right.constant;
+ ast.toWatch = ast.constant ? [] : [ast];
+ break;
+ case AST.ConditionalExpression:
+ findConstantAndWatchExpressions(ast.test, $filter);
+ findConstantAndWatchExpressions(ast.alternate, $filter);
+ findConstantAndWatchExpressions(ast.consequent, $filter);
+ ast.constant = ast.test.constant && ast.alternate.constant && ast.consequent.constant;
+ ast.toWatch = ast.constant ? [] : [ast];
+ break;
+ case AST.Identifier:
+ ast.constant = false;
+ ast.toWatch = [ast];
+ break;
+ case AST.MemberExpression:
+ findConstantAndWatchExpressions(ast.object, $filter);
+ if (ast.computed) {
+ findConstantAndWatchExpressions(ast.property, $filter);
+ }
+ ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
+ ast.toWatch = [ast];
+ break;
+ case AST.CallExpression:
+ allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ argsToWatch = [];
+ forEach(ast.arguments, function(expr) {
+ findConstantAndWatchExpressions(expr, $filter);
+ allConstants = allConstants && expr.constant;
+ if (!expr.constant) {
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
+ }
+ });
+ ast.constant = allConstants;
+ ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast];
+ break;
+ case AST.AssignmentExpression:
+ findConstantAndWatchExpressions(ast.left, $filter);
+ findConstantAndWatchExpressions(ast.right, $filter);
+ ast.constant = ast.left.constant && ast.right.constant;
+ ast.toWatch = [ast];
+ break;
+ case AST.ArrayExpression:
+ allConstants = true;
+ argsToWatch = [];
+ forEach(ast.elements, function(expr) {
+ findConstantAndWatchExpressions(expr, $filter);
+ allConstants = allConstants && expr.constant;
+ if (!expr.constant) {
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
+ }
+ });
+ ast.constant = allConstants;
+ ast.toWatch = argsToWatch;
+ break;
+ case AST.ObjectExpression:
+ allConstants = true;
+ argsToWatch = [];
+ forEach(ast.properties, function(property) {
+ findConstantAndWatchExpressions(property.value, $filter);
+ allConstants = allConstants && property.value.constant;
+ if (!property.value.constant) {
+ argsToWatch.push.apply(argsToWatch, property.value.toWatch);
+ }
+ });
+ ast.constant = allConstants;
+ ast.toWatch = argsToWatch;
+ break;
+ case AST.ThisExpression:
+ ast.constant = false;
+ ast.toWatch = [];
+ break;
}
- key = ensureSafeMemberName(element.shift(), fullExp);
- ensureSafeObject(obj[key], fullExp);
- obj[key] = setValue;
- return setValue;
}
-var getterFnCacheDefault = createMap();
-var getterFnCacheExpensive = createMap();
+function getInputs(body) {
+ if (body.length != 1) return;
+ var lastExpression = body[0].expression;
+ var candidate = lastExpression.toWatch;
+ if (candidate.length !== 1) return candidate;
+ return candidate[0] !== lastExpression ? candidate : undefined;
+}
-function isPossiblyDangerousMemberName(name) {
- return name == 'constructor';
+function isAssignable(ast) {
+ return ast.type === AST.Identifier || ast.type === AST.MemberExpression;
}
-/**
- * Implementation of the "Black Hole" variant from:
- * - http://jsperf.com/angularjs-parse-getter/4
- * - http://jsperf.com/path-evaluation-simplified/7
- */
-function cspSafeGetterFn(key0, key1, key2, key3, key4, fullExp, expensiveChecks) {
- ensureSafeMemberName(key0, fullExp);
- ensureSafeMemberName(key1, fullExp);
- ensureSafeMemberName(key2, fullExp);
- ensureSafeMemberName(key3, fullExp);
- ensureSafeMemberName(key4, fullExp);
- var eso = function(o) {
- return ensureSafeObject(o, fullExp);
- };
- var eso0 = (expensiveChecks || isPossiblyDangerousMemberName(key0)) ? eso : identity;
- var eso1 = (expensiveChecks || isPossiblyDangerousMemberName(key1)) ? eso : identity;
- var eso2 = (expensiveChecks || isPossiblyDangerousMemberName(key2)) ? eso : identity;
- var eso3 = (expensiveChecks || isPossiblyDangerousMemberName(key3)) ? eso : identity;
- var eso4 = (expensiveChecks || isPossiblyDangerousMemberName(key4)) ? eso : identity;
+function assignableAST(ast) {
+ if (ast.body.length === 1 && isAssignable(ast.body[0].expression)) {
+ return {type: AST.AssignmentExpression, left: ast.body[0].expression, right: {type: AST.NGValueParameter}, operator: '='};
+ }
+}
- return function cspSafeGetter(scope, locals) {
- var pathVal = (locals && locals.hasOwnProperty(key0)) ? locals : scope;
+function isLiteral(ast) {
+ return ast.body.length === 0 ||
+ ast.body.length === 1 && (
+ ast.body[0].expression.type === AST.Literal ||
+ ast.body[0].expression.type === AST.ArrayExpression ||
+ ast.body[0].expression.type === AST.ObjectExpression);
+}
- if (pathVal == null) return pathVal;
- pathVal = eso0(pathVal[key0]);
+function isConstant(ast) {
+ return ast.constant;
+}
- if (!key1) return pathVal;
- if (pathVal == null) return undefined;
- pathVal = eso1(pathVal[key1]);
+function ASTCompiler(astBuilder, $filter) {
+ this.astBuilder = astBuilder;
+ this.$filter = $filter;
+}
- if (!key2) return pathVal;
- if (pathVal == null) return undefined;
- pathVal = eso2(pathVal[key2]);
+ASTCompiler.prototype = {
+ compile: function(expression, expensiveChecks) {
+ var self = this;
+ var ast = this.astBuilder.ast(expression);
+ this.state = {
+ nextId: 0,
+ filters: {},
+ expensiveChecks: expensiveChecks,
+ fn: {vars: [], body: [], own: {}},
+ assign: {vars: [], body: [], own: {}},
+ inputs: []
+ };
+ findConstantAndWatchExpressions(ast, self.$filter);
+ var extra = '';
+ var assignable;
+ this.stage = 'assign';
+ if ((assignable = assignableAST(ast))) {
+ 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);
+ self.stage = 'inputs';
+ forEach(toWatch, function(watch, key) {
+ var fnKey = 'fn' + key;
+ self.state[fnKey] = {vars: [], body: [], own: {}};
+ self.state.computing = fnKey;
+ var intoId = self.nextId();
+ self.recurse(watch, intoId);
+ self.return_(intoId);
+ self.state.inputs.push(fnKey);
+ watch.watchId = key;
+ });
+ this.state.computing = 'fn';
+ this.stage = 'main';
+ this.recurse(ast);
+ var fnString =
+ // The build and minification steps remove the string "use strict" from the code, but this is done using a regex.
+ // This is a workaround for this until we do a better job at only removing the prefix only when we should.
+ '"' + this.USE + ' ' + this.STRICT + '";\n' +
+ this.filterPrefix() +
+ 'var fn=' + this.generateFunction('fn', 's,l,a,i') +
+ extra +
+ this.watchFns() +
+ 'return fn;';
- if (!key3) return pathVal;
- if (pathVal == null) return undefined;
- pathVal = eso3(pathVal[key3]);
+ /* jshint -W054 */
+ var fn = (new Function('$filter',
+ 'ensureSafeMemberName',
+ 'ensureSafeObject',
+ 'ensureSafeFunction',
+ 'getStringValue',
+ 'ensureSafeAssignContext',
+ 'ifDefined',
+ 'plus',
+ 'text',
+ fnString))(
+ this.$filter,
+ ensureSafeMemberName,
+ ensureSafeObject,
+ ensureSafeFunction,
+ getStringValue,
+ ensureSafeAssignContext,
+ ifDefined,
+ plusFn,
+ expression);
+ /* jshint +W054 */
+ this.state = this.stage = undefined;
+ fn.literal = isLiteral(ast);
+ fn.constant = isConstant(ast);
+ return fn;
+ },
- if (!key4) return pathVal;
- if (pathVal == null) return undefined;
- pathVal = eso4(pathVal[key4]);
+ USE: 'use',
- return pathVal;
- };
-}
+ STRICT: 'strict',
-function getterFnWithEnsureSafeObject(fn, fullExpression) {
- return function(s, l) {
- return fn(s, l, ensureSafeObject, fullExpression);
- };
-}
+ watchFns: function() {
+ var result = [];
+ var fns = this.state.inputs;
+ var self = this;
+ forEach(fns, function(name) {
+ result.push('var ' + name + '=' + self.generateFunction(name, 's'));
+ });
+ if (fns.length) {
+ result.push('fn.inputs=[' + fns.join(',') + '];');
+ }
+ return result.join('');
+ },
+
+ generateFunction: function(name, params) {
+ return 'function(' + params + '){' +
+ this.varsPrefix(name) +
+ this.body(name) +
+ '};';
+ },
-function getterFn(path, options, fullExp) {
- var expensiveChecks = options.expensiveChecks;
- var getterFnCache = (expensiveChecks ? getterFnCacheExpensive : getterFnCacheDefault);
- var fn = getterFnCache[path];
- if (fn) return fn;
+ filterPrefix: function() {
+ var parts = [];
+ var self = this;
+ forEach(this.state.filters, function(id, filter) {
+ parts.push(id + '=$filter(' + self.escape(filter) + ')');
+ });
+ if (parts.length) return 'var ' + parts.join(',') + ';';
+ return '';
+ },
+ varsPrefix: function(section) {
+ return this.state[section].vars.length ? 'var ' + this.state[section].vars.join(',') + ';' : '';
+ },
- var pathKeys = path.split('.'),
- pathKeysLength = pathKeys.length;
+ body: function(section) {
+ return this.state[section].body.join('');
+ },
+
+ recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
+ var left, right, self = this, args, expression;
+ recursionFn = recursionFn || noop;
+ if (!skipWatchIdCheck && isDefined(ast.watchId)) {
+ intoId = intoId || this.nextId();
+ this.if_('i',
+ this.lazyAssign(intoId, this.computedMember('i', ast.watchId)),
+ this.lazyRecurse(ast, intoId, nameId, recursionFn, create, true)
+ );
+ return;
+ }
+ switch (ast.type) {
+ case AST.Program:
+ forEach(ast.body, function(expression, pos) {
+ self.recurse(expression.expression, undefined, undefined, function(expr) { right = expr; });
+ if (pos !== ast.body.length - 1) {
+ self.current().body.push(right, ';');
+ } else {
+ self.return_(right);
+ }
+ });
+ break;
+ case AST.Literal:
+ expression = this.escape(ast.value);
+ this.assign(intoId, expression);
+ recursionFn(expression);
+ break;
+ case AST.UnaryExpression:
+ this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
+ expression = ast.operator + '(' + this.ifDefined(right, 0) + ')';
+ this.assign(intoId, expression);
+ recursionFn(expression);
+ break;
+ case AST.BinaryExpression:
+ this.recurse(ast.left, undefined, undefined, function(expr) { left = expr; });
+ this.recurse(ast.right, undefined, undefined, function(expr) { right = expr; });
+ if (ast.operator === '+') {
+ expression = this.plus(left, right);
+ } else if (ast.operator === '-') {
+ expression = this.ifDefined(left, 0) + ast.operator + this.ifDefined(right, 0);
+ } else {
+ expression = '(' + left + ')' + ast.operator + '(' + right + ')';
+ }
+ this.assign(intoId, expression);
+ recursionFn(expression);
+ break;
+ case AST.LogicalExpression:
+ intoId = intoId || this.nextId();
+ self.recurse(ast.left, intoId);
+ self.if_(ast.operator === '&&' ? intoId : self.not(intoId), self.lazyRecurse(ast.right, intoId));
+ recursionFn(intoId);
+ break;
+ case AST.ConditionalExpression:
+ intoId = intoId || this.nextId();
+ self.recurse(ast.test, intoId);
+ self.if_(intoId, self.lazyRecurse(ast.alternate, intoId), self.lazyRecurse(ast.consequent, intoId));
+ recursionFn(intoId);
+ break;
+ case AST.Identifier:
+ intoId = intoId || this.nextId();
+ if (nameId) {
+ nameId.context = self.stage === 'inputs' ? 's' : this.assign(this.nextId(), this.getHasOwnProperty('l', ast.name) + '?l:s');
+ nameId.computed = false;
+ nameId.name = ast.name;
+ }
+ ensureSafeMemberName(ast.name);
+ self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
+ function() {
+ self.if_(self.stage === 'inputs' || 's', function() {
+ if (create && create !== 1) {
+ self.if_(
+ self.not(self.nonComputedMember('s', ast.name)),
+ self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
+ }
+ self.assign(intoId, self.nonComputedMember('s', ast.name));
+ });
+ }, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
+ );
+ if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
+ self.addEnsureSafeObject(intoId);
+ }
+ recursionFn(intoId);
+ break;
+ case AST.MemberExpression:
+ left = nameId && (nameId.context = this.nextId()) || this.nextId();
+ 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), '{}'));
+ }
+ expression = self.ensureSafeObject(self.computedMember(left, right));
+ self.assign(intoId, expression);
+ if (nameId) {
+ nameId.computed = true;
+ nameId.name = right;
+ }
+ } else {
+ ensureSafeMemberName(ast.property.name);
+ if (create && create !== 1) {
+ self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
+ }
+ expression = self.nonComputedMember(left, ast.property.name);
+ if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
+ expression = self.ensureSafeObject(expression);
+ }
+ self.assign(intoId, expression);
+ if (nameId) {
+ nameId.computed = false;
+ nameId.name = ast.property.name;
+ }
+ }
+ }, function() {
+ self.assign(intoId, 'undefined');
+ });
+ recursionFn(intoId);
+ }, !!create);
+ break;
+ case AST.CallExpression:
+ intoId = intoId || this.nextId();
+ if (ast.filter) {
+ right = self.filter(ast.callee.name);
+ args = [];
+ forEach(ast.arguments, function(expr) {
+ var argument = self.nextId();
+ self.recurse(expr, argument);
+ args.push(argument);
+ });
+ expression = right + '(' + args.join(',') + ')';
+ self.assign(intoId, expression);
+ recursionFn(intoId);
+ } else {
+ right = self.nextId();
+ left = {};
+ args = [];
+ self.recurse(ast.callee, right, left, function() {
+ self.if_(self.notNull(right), function() {
+ self.addEnsureSafeFunction(right);
+ forEach(ast.arguments, function(expr) {
+ self.recurse(expr, self.nextId(), undefined, function(argument) {
+ args.push(self.ensureSafeObject(argument));
+ });
+ });
+ if (left.name) {
+ if (!self.state.expensiveChecks) {
+ self.addEnsureSafeObject(left.context);
+ }
+ expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
+ } else {
+ expression = right + '(' + args.join(',') + ')';
+ }
+ expression = self.ensureSafeObject(expression);
+ self.assign(intoId, expression);
+ }, function() {
+ self.assign(intoId, 'undefined');
+ });
+ recursionFn(intoId);
+ });
+ }
+ break;
+ case AST.AssignmentExpression:
+ right = this.nextId();
+ left = {};
+ if (!isAssignable(ast.left)) {
+ 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);
+ });
+ }, 1);
+ break;
+ case AST.ArrayExpression:
+ args = [];
+ forEach(ast.elements, function(expr) {
+ self.recurse(expr, self.nextId(), undefined, function(argument) {
+ args.push(argument);
+ });
+ });
+ expression = '[' + args.join(',') + ']';
+ this.assign(intoId, expression);
+ recursionFn(expression);
+ break;
+ case AST.ObjectExpression:
+ args = [];
+ forEach(ast.properties, function(property) {
+ self.recurse(property.value, self.nextId(), undefined, function(expr) {
+ args.push(self.escape(
+ property.key.type === AST.Identifier ? property.key.name :
+ ('' + property.key.value)) +
+ ':' + expr);
+ });
+ });
+ expression = '{' + args.join(',') + '}';
+ this.assign(intoId, expression);
+ recursionFn(expression);
+ break;
+ case AST.ThisExpression:
+ this.assign(intoId, 's');
+ recursionFn('s');
+ break;
+ case AST.NGValueParameter:
+ this.assign(intoId, 'v');
+ recursionFn('v');
+ break;
+ }
+ },
+
+ getHasOwnProperty: function(element, property) {
+ var key = element + '.' + property;
+ var own = this.current().own;
+ if (!own.hasOwnProperty(key)) {
+ own[key] = this.nextId(false, element + '&&(' + this.escape(property) + ' in ' + element + ')');
+ }
+ return own[key];
+ },
+
+ assign: function(id, value) {
+ if (!id) return;
+ this.current().body.push(id, '=', value, ';');
+ return id;
+ },
- // http://jsperf.com/angularjs-parse-getter/6
- if (options.csp) {
- if (pathKeysLength < 6) {
- fn = cspSafeGetterFn(pathKeys[0], pathKeys[1], pathKeys[2], pathKeys[3], pathKeys[4], fullExp, expensiveChecks);
+ filter: function(filterName) {
+ if (!this.state.filters.hasOwnProperty(filterName)) {
+ this.state.filters[filterName] = this.nextId(true);
+ }
+ return this.state.filters[filterName];
+ },
+
+ ifDefined: function(id, defaultValue) {
+ return 'ifDefined(' + id + ',' + this.escape(defaultValue) + ')';
+ },
+
+ plus: function(left, right) {
+ return 'plus(' + left + ',' + right + ')';
+ },
+
+ return_: function(id) {
+ this.current().body.push('return ', id, ';');
+ },
+
+ if_: function(test, alternate, consequent) {
+ if (test === true) {
+ alternate();
} else {
- fn = function cspSafeGetter(scope, locals) {
- var i = 0, val;
- do {
- val = cspSafeGetterFn(pathKeys[i++], pathKeys[i++], pathKeys[i++], pathKeys[i++],
- pathKeys[i++], fullExp, expensiveChecks)(scope, locals);
+ var body = this.current().body;
+ body.push('if(', test, '){');
+ alternate();
+ body.push('}');
+ if (consequent) {
+ body.push('else{');
+ consequent();
+ body.push('}');
+ }
+ }
+ },
- locals = undefined; // clear after first iteration
- scope = val;
- } while (i < pathKeysLength);
- return val;
- };
+ not: function(expression) {
+ return '!(' + expression + ')';
+ },
+
+ notNull: function(expression) {
+ return expression + '!=null';
+ },
+
+ nonComputedMember: function(left, right) {
+ return left + '.' + right;
+ },
+
+ computedMember: function(left, right) {
+ return left + '[' + right + ']';
+ },
+
+ member: function(left, right, computed) {
+ if (computed) return this.computedMember(left, right);
+ return this.nonComputedMember(left, right);
+ },
+
+ addEnsureSafeObject: function(item) {
+ this.current().body.push(this.ensureSafeObject(item), ';');
+ },
+
+ addEnsureSafeMemberName: function(item) {
+ this.current().body.push(this.ensureSafeMemberName(item), ';');
+ },
+
+ addEnsureSafeFunction: function(item) {
+ this.current().body.push(this.ensureSafeFunction(item), ';');
+ },
+
+ addEnsureSafeAssignContext: function(item) {
+ this.current().body.push(this.ensureSafeAssignContext(item), ';');
+ },
+
+ ensureSafeObject: function(item) {
+ return 'ensureSafeObject(' + item + ',text)';
+ },
+
+ ensureSafeMemberName: function(item) {
+ return 'ensureSafeMemberName(' + item + ',text)';
+ },
+
+ ensureSafeFunction: function(item) {
+ return 'ensureSafeFunction(' + item + ',text)';
+ },
+
+ getStringValue: function(item) {
+ this.assign(item, 'getStringValue(' + item + ',text)');
+ },
+
+ ensureSafeAssignContext: function(item) {
+ return 'ensureSafeAssignContext(' + item + ',text)';
+ },
+
+ lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
+ var self = this;
+ return function() {
+ self.recurse(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck);
+ };
+ },
+
+ lazyAssign: function(id, value) {
+ var self = this;
+ return function() {
+ self.assign(id, value);
+ };
+ },
+
+ stringEscapeRegex: /[^ a-zA-Z0-9]/g,
+
+ stringEscapeFn: function(c) {
+ return '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
+ },
+
+ escape: function(value) {
+ if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'";
+ if (isNumber(value)) return value.toString();
+ if (value === true) return 'true';
+ if (value === false) return 'false';
+ if (value === null) return 'null';
+ if (typeof value === 'undefined') return 'undefined';
+
+ throw $parseMinErr('esc', 'IMPOSSIBLE');
+ },
+
+ nextId: function(skip, init) {
+ var id = 'v' + (this.state.nextId++);
+ if (!skip) {
+ this.current().vars.push(id + (init ? '=' + init : ''));
}
- } else {
- var code = '';
- if (expensiveChecks) {
- code += 's = eso(s, fe);\nl = eso(l, fe);\n';
- }
- var needsEnsureSafeObject = expensiveChecks;
- forEach(pathKeys, function(key, index) {
- ensureSafeMemberName(key, fullExp);
- var lookupJs = (index
- // we simply dereference 's' on any .dot notation
- ? 's'
- // but if we are first then we check locals first, and if so read it first
- : '((l&&l.hasOwnProperty("' + key + '"))?l:s)') + '.' + key;
- if (expensiveChecks || isPossiblyDangerousMemberName(key)) {
- lookupJs = 'eso(' + lookupJs + ', fe)';
- needsEnsureSafeObject = true;
- }
- code += 'if(s == null) return undefined;\n' +
- 's=' + lookupJs + ';\n';
+ return id;
+ },
+
+ current: function() {
+ return this.state[this.state.computing];
+ }
+};
+
+
+function ASTInterpreter(astBuilder, $filter) {
+ this.astBuilder = astBuilder;
+ this.$filter = $filter;
+}
+
+ASTInterpreter.prototype = {
+ compile: function(expression, expensiveChecks) {
+ var self = this;
+ var ast = this.astBuilder.ast(expression);
+ this.expression = expression;
+ this.expensiveChecks = expensiveChecks;
+ findConstantAndWatchExpressions(ast, self.$filter);
+ var assignable;
+ var assign;
+ if ((assignable = assignableAST(ast))) {
+ assign = this.recurse(assignable);
+ }
+ var toWatch = getInputs(ast.body);
+ var inputs;
+ if (toWatch) {
+ inputs = [];
+ forEach(toWatch, function(watch, key) {
+ var input = self.recurse(watch);
+ watch.input = input;
+ inputs.push(input);
+ watch.watchId = key;
+ });
+ }
+ var expressions = [];
+ forEach(ast.body, function(expression) {
+ expressions.push(self.recurse(expression.expression));
});
- code += 'return s;';
+ var fn = ast.body.length === 0 ? function() {} :
+ ast.body.length === 1 ? expressions[0] :
+ function(scope, locals) {
+ var lastValue;
+ forEach(expressions, function(exp) {
+ lastValue = exp(scope, locals);
+ });
+ return lastValue;
+ };
+ if (assign) {
+ fn.assign = function(scope, value, locals) {
+ return assign(scope, locals, value);
+ };
+ }
+ if (inputs) {
+ fn.inputs = inputs;
+ }
+ fn.literal = isLiteral(ast);
+ fn.constant = isConstant(ast);
+ return fn;
+ },
- /* jshint -W054 */
- var evaledFnGetter = new Function('s', 'l', 'eso', 'fe', code); // s=scope, l=locals, eso=ensureSafeObject
- /* jshint +W054 */
- evaledFnGetter.toString = valueFn(code);
- if (needsEnsureSafeObject) {
- evaledFnGetter = getterFnWithEnsureSafeObject(evaledFnGetter, fullExp);
+ recurse: function(ast, context, create) {
+ var left, right, self = this, args, expression;
+ if (ast.input) {
+ return this.inputs(ast.input, ast.watchId);
+ }
+ switch (ast.type) {
+ case AST.Literal:
+ return this.value(ast.value, context);
+ case AST.UnaryExpression:
+ right = this.recurse(ast.argument);
+ return this['unary' + ast.operator](right, context);
+ case AST.BinaryExpression:
+ left = this.recurse(ast.left);
+ right = this.recurse(ast.right);
+ return this['binary' + ast.operator](left, right, context);
+ case AST.LogicalExpression:
+ left = this.recurse(ast.left);
+ right = this.recurse(ast.right);
+ return this['binary' + ast.operator](left, right, context);
+ case AST.ConditionalExpression:
+ return this['ternary?:'](
+ this.recurse(ast.test),
+ this.recurse(ast.alternate),
+ this.recurse(ast.consequent),
+ context
+ );
+ case AST.Identifier:
+ ensureSafeMemberName(ast.name, self.expression);
+ return self.identifier(ast.name,
+ self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
+ context, create, self.expression);
+ case AST.MemberExpression:
+ left = this.recurse(ast.object, false, !!create);
+ if (!ast.computed) {
+ ensureSafeMemberName(ast.property.name, self.expression);
+ right = ast.property.name;
+ }
+ if (ast.computed) right = this.recurse(ast.property);
+ return ast.computed ?
+ this.computedMember(left, right, context, create, self.expression) :
+ this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
+ case AST.CallExpression:
+ args = [];
+ forEach(ast.arguments, function(expr) {
+ args.push(self.recurse(expr));
+ });
+ if (ast.filter) right = this.$filter(ast.callee.name);
+ if (!ast.filter) right = this.recurse(ast.callee, true);
+ return ast.filter ?
+ function(scope, locals, assign, inputs) {
+ var values = [];
+ for (var i = 0; i < args.length; ++i) {
+ values.push(args[i](scope, locals, assign, inputs));
+ }
+ var value = right.apply(undefined, values, inputs);
+ return context ? {context: undefined, name: undefined, value: value} : value;
+ } :
+ function(scope, locals, assign, inputs) {
+ var rhs = right(scope, locals, assign, inputs);
+ var value;
+ if (rhs.value != null) {
+ ensureSafeObject(rhs.context, self.expression);
+ ensureSafeFunction(rhs.value, self.expression);
+ var values = [];
+ for (var i = 0; i < args.length; ++i) {
+ values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
+ }
+ value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
+ }
+ return context ? {value: value} : value;
+ };
+ case AST.AssignmentExpression:
+ left = this.recurse(ast.left, true, 1);
+ right = this.recurse(ast.right);
+ return function(scope, locals, assign, inputs) {
+ var lhs = left(scope, locals, assign, inputs);
+ var rhs = right(scope, locals, assign, inputs);
+ ensureSafeObject(lhs.value, self.expression);
+ ensureSafeAssignContext(lhs.context);
+ lhs.context[lhs.name] = rhs;
+ return context ? {value: rhs} : rhs;
+ };
+ case AST.ArrayExpression:
+ args = [];
+ forEach(ast.elements, function(expr) {
+ args.push(self.recurse(expr));
+ });
+ return function(scope, locals, assign, inputs) {
+ var value = [];
+ for (var i = 0; i < args.length; ++i) {
+ value.push(args[i](scope, locals, assign, inputs));
+ }
+ return context ? {value: value} : value;
+ };
+ case AST.ObjectExpression:
+ args = [];
+ forEach(ast.properties, function(property) {
+ args.push({key: property.key.type === AST.Identifier ?
+ property.key.name :
+ ('' + property.key.value),
+ value: self.recurse(property.value)
+ });
+ });
+ return function(scope, locals, assign, inputs) {
+ var value = {};
+ for (var i = 0; i < args.length; ++i) {
+ value[args[i].key] = args[i].value(scope, locals, assign, inputs);
+ }
+ return context ? {value: value} : value;
+ };
+ case AST.ThisExpression:
+ return function(scope) {
+ return context ? {value: scope} : scope;
+ };
+ case AST.NGValueParameter:
+ return function(scope, locals, assign, inputs) {
+ return context ? {value: assign} : assign;
+ };
}
- fn = evaledFnGetter;
+ },
+
+ 'unary+': function(argument, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = argument(scope, locals, assign, inputs);
+ if (isDefined(arg)) {
+ arg = +arg;
+ } else {
+ arg = 0;
+ }
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'unary-': function(argument, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = argument(scope, locals, assign, inputs);
+ if (isDefined(arg)) {
+ arg = -arg;
+ } else {
+ arg = 0;
+ }
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'unary!': function(argument, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = !argument(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary+': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var lhs = left(scope, locals, assign, inputs);
+ var rhs = right(scope, locals, assign, inputs);
+ var arg = plusFn(lhs, rhs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary-': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var lhs = left(scope, locals, assign, inputs);
+ var rhs = right(scope, locals, assign, inputs);
+ var arg = (isDefined(lhs) ? lhs : 0) - (isDefined(rhs) ? rhs : 0);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary*': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) * right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary/': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) / right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary%': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) % right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary===': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) === right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary!==': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) !== right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary==': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary!=': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary<': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) < right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary>': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) > right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary<=': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) <= right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary>=': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) >= right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary&&': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) && right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'binary||': function(left, right, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = left(scope, locals, assign, inputs) || right(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ 'ternary?:': function(test, alternate, consequent, context) {
+ return function(scope, locals, assign, inputs) {
+ var arg = test(scope, locals, assign, inputs) ? alternate(scope, locals, assign, inputs) : consequent(scope, locals, assign, inputs);
+ return context ? {value: arg} : arg;
+ };
+ },
+ value: function(value, context) {
+ return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
+ },
+ identifier: function(name, expensiveChecks, context, create, expression) {
+ return function(scope, locals, assign, inputs) {
+ var base = locals && (name in locals) ? locals : scope;
+ if (create && create !== 1 && base && !(base[name])) {
+ base[name] = {};
+ }
+ var value = base ? base[name] : undefined;
+ if (expensiveChecks) {
+ ensureSafeObject(value, expression);
+ }
+ if (context) {
+ return {context: base, name: name, value: value};
+ } else {
+ return value;
+ }
+ };
+ },
+ computedMember: function(left, right, context, create, expression) {
+ return function(scope, locals, assign, inputs) {
+ var lhs = left(scope, locals, assign, inputs);
+ var rhs;
+ var value;
+ if (lhs != null) {
+ rhs = right(scope, locals, assign, inputs);
+ rhs = getStringValue(rhs);
+ ensureSafeMemberName(rhs, expression);
+ if (create && create !== 1) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[rhs])) {
+ lhs[rhs] = {};
+ }
+ }
+ value = lhs[rhs];
+ ensureSafeObject(value, expression);
+ }
+ if (context) {
+ return {context: lhs, name: rhs, value: value};
+ } else {
+ return value;
+ }
+ };
+ },
+ 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) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[right])) {
+ lhs[right] = {};
+ }
+ }
+ var value = lhs != null ? lhs[right] : undefined;
+ if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
+ ensureSafeObject(value, expression);
+ }
+ if (context) {
+ return {context: lhs, name: right, value: value};
+ } else {
+ return value;
+ }
+ };
+ },
+ inputs: function(input, watchId) {
+ return function(scope, value, locals, inputs) {
+ if (inputs) return inputs[watchId];
+ return input(scope, value, locals);
+ };
}
+};
- fn.sharedGetter = true;
- fn.assign = function(self, value, locals) {
- return setter(self, locals, path, value, path);
- };
- getterFnCache[path] = fn;
- return fn;
+/**
+ * @constructor
+ */
+var Parser = function(lexer, $filter, options) {
+ this.lexer = lexer;
+ this.$filter = $filter;
+ this.options = options;
+ this.ast = new AST(this.lexer);
+ this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
+ new ASTCompiler(this.ast, $filter);
+};
+
+Parser.prototype = {
+ constructor: Parser,
+
+ parse: function(text) {
+ return this.astCompiler.compile(text, this.options.expensiveChecks);
+ }
+};
+
+function isPossiblyDangerousMemberName(name) {
+ return name == 'constructor';
}
var objectValueOf = Object.prototype.valueOf;
@@ -12740,39 +14527,33 @@ function $ParseProvider() {
var cacheDefault = createMap();
var cacheExpensive = createMap();
-
-
- this.$get = ['$filter', '$sniffer', function($filter, $sniffer) {
+ this.$get = ['$filter', function($filter) {
+ var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
- csp: $sniffer.csp,
+ csp: noUnsafeEval,
expensiveChecks: false
},
$parseOptionsExpensive = {
- csp: $sniffer.csp,
+ csp: noUnsafeEval,
expensiveChecks: true
};
+ var runningChecksEnabled = false;
- function wrapSharedExpression(exp) {
- var wrapped = exp;
-
- if (exp.sharedGetter) {
- wrapped = function $parseWrapper(self, locals) {
- return exp(self, locals);
- };
- wrapped.literal = exp.literal;
- wrapped.constant = exp.constant;
- wrapped.assign = exp.assign;
- }
+ $parse.$$runningExpensiveChecks = function() {
+ return runningChecksEnabled;
+ };
- return wrapped;
- }
+ return $parse;
- return function $parse(exp, interceptorFn, expensiveChecks) {
+ function $parse(exp, interceptorFn, expensiveChecks) {
var parsedExpression, oneTime, cacheKey;
+ expensiveChecks = expensiveChecks || runningChecksEnabled;
+
switch (typeof exp) {
case 'string':
- cacheKey = exp = exp.trim();
+ exp = exp.trim();
+ cacheKey = exp;
var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
@@ -12782,24 +14563,21 @@ function $ParseProvider() {
oneTime = true;
exp = exp.substring(2);
}
-
var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
var lexer = new Lexer(parseOptions);
var parser = new Parser(lexer, $filter, parseOptions);
parsedExpression = parser.parse(exp);
-
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
} else if (oneTime) {
- //oneTime is not part of the exp passed to the Parser so we may have to
- //wrap the parsedExpression before adding a $$watchDelegate
- parsedExpression = wrapSharedExpression(parsedExpression);
parsedExpression.$$watchDelegate = parsedExpression.literal ?
- oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
+ oneTimeLiteralWatchDelegate : oneTimeWatchDelegate;
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
-
+ if (expensiveChecks) {
+ parsedExpression = expensiveChecksInterceptor(parsedExpression);
+ }
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -12810,21 +14588,30 @@ function $ParseProvider() {
default:
return addInterceptor(noop, interceptorFn);
}
- };
+ }
- function collectExpressionInputs(inputs, list) {
- for (var i = 0, ii = inputs.length; i < ii; i++) {
- var input = inputs[i];
- if (!input.constant) {
- if (input.inputs) {
- collectExpressionInputs(input.inputs, list);
- } else if (list.indexOf(input) === -1) { // TODO(perf) can we do better?
- list.push(input);
- }
- }
+ 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 list;
+ 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) {
@@ -12852,28 +14639,28 @@ function $ParseProvider() {
return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
}
- function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var inputExpressions = parsedExpression.$$inputs ||
- (parsedExpression.$$inputs = collectExpressionInputs(parsedExpression.inputs, []));
-
+ function inputsWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
+ var inputExpressions = parsedExpression.inputs;
var lastResult;
if (inputExpressions.length === 1) {
- var oldInputValue = expressionInputDirtyCheck; // init to something unique so that equals check fails
+ var oldInputValueOf = expressionInputDirtyCheck; // init to something unique so that equals check fails
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
- if (!expressionInputDirtyCheck(newInputValue, oldInputValue)) {
- lastResult = parsedExpression(scope);
- oldInputValue = newInputValue && getValueOf(newInputValue);
+ if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
+ lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
+ oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
return lastResult;
- }, listener, objectEquality);
+ }, listener, objectEquality, prettyPrintExpression);
}
var oldInputValueOfValues = [];
+ var oldInputValues = [];
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
oldInputValueOfValues[i] = expressionInputDirtyCheck; // init to something unique so that equals check fails
+ oldInputValues[i] = null;
}
return scope.$watch(function expressionInputsWatch(scope) {
@@ -12882,16 +14669,17 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
+ oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
}
if (changed) {
- lastResult = parsedExpression(scope);
+ lastResult = parsedExpression(scope, undefined, undefined, oldInputValues);
}
return lastResult;
- }, listener, objectEquality);
+ }, listener, objectEquality, prettyPrintExpression);
}
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
@@ -12953,16 +14741,17 @@ function $ParseProvider() {
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) {
- var value = parsedExpression(scope, locals);
+ var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
+ var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value, scope, locals);
- } : function oneTimeInterceptedExpression(scope, locals) {
- var value = parsedExpression(scope, locals);
+ } : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
+ var value = parsedExpression(scope, locals, assign, inputs);
var result = interceptorFn(value, scope, locals);
// we only return the interceptor's result if the
// initial value is defined (for bind-once)
@@ -12977,7 +14766,8 @@ 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;
- fn.inputs = [parsedExpression];
+ useInputs = !parsedExpression.inputs;
+ fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
}
return fn;
@@ -13038,6 +14828,8 @@ function $ParseProvider() {
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
+ * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise.
+ *
* 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
@@ -13125,9 +14917,11 @@ function $ParseProvider() {
* provide a progress indication, before the promise is resolved or rejected.
*
* This method *returns a new promise* which is resolved or rejected via the return value of the
- * `successCallback`, `errorCallback`. It also notifies via the return value of the
- * `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback
- * method.
+ * `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved
+ * with the value which is resolved in that promise using
+ * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
+ * It also notifies via the return value of the `notifyCallback` method. The promise cannot be
+ * resolved or rejected from the notifyCallback method.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
@@ -13255,8 +15049,11 @@ function qFactory(nextTick, exceptionHandler) {
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 || [];
@@ -13277,7 +15074,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) {
@@ -13287,24 +15084,24 @@ function qFactory(nextTick, exceptionHandler) {
}
function processQueue(state) {
- var fn, promise, pending;
+ var fn, deferred, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
for (var i = 0, ii = pending.length; i < ii; ++i) {
- promise = pending[i][0];
+ deferred = pending[i][0];
fn = pending[i][state.status];
try {
if (isFunction(fn)) {
- promise.resolve(fn(state.value));
+ deferred.resolve(fn(state.value));
} else if (state.status === 1) {
- promise.resolve(state.value);
+ deferred.resolve(state.value);
} else {
- promise.reject(state.value);
+ deferred.reject(state.value);
}
} catch (e) {
- promise.reject(e);
+ deferred.reject(e);
exceptionHandler(e);
}
}
@@ -13324,7 +15121,7 @@ function qFactory(nextTick, exceptionHandler) {
this.notify = simpleBind(this, this.notify);
}
- Deferred.prototype = {
+ extend(Deferred.prototype, {
resolve: function(val) {
if (this.promise.$$state.status) return;
if (val === this.promise) {
@@ -13387,7 +15184,7 @@ function qFactory(nextTick, exceptionHandler) {
});
}
}
- };
+ });
/**
* @ngdoc method
@@ -13470,6 +15267,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
*/
@@ -13482,6 +15282,22 @@ function qFactory(nextTick, exceptionHandler) {
/**
* @ngdoc method
+ * @name $q#resolve
+ * @kind function
+ *
+ * @description
+ * 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;
+
+ /**
+ * @ngdoc method
* @name $q#all
* @kind function
*
@@ -13548,6 +15364,7 @@ function qFactory(nextTick, exceptionHandler) {
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
+ $Q.resolve = resolve;
$Q.all = all;
return $Q;
@@ -13563,7 +15380,7 @@ function $$RAFProvider() { //rAF
$window.webkitCancelRequestAnimationFrame;
var rafSupported = !!requestAnimationFrame;
- var rafFn = rafSupported
+ var raf = rafSupported
? function(fn) {
var id = requestAnimationFrame(fn);
return function() {
@@ -13577,47 +15394,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;
}];
}
@@ -13635,15 +15414,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.
*/
@@ -13685,7 +15464,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() {
@@ -13707,6 +15486,7 @@ function $RootScopeProvider() {
this.$$childHead = this.$$childTail = null;
this.$$listeners = {};
this.$$listenerCount = {};
+ this.$$watchersCount = 0;
this.$id = nextUid();
this.$$ChildScope = null;
}
@@ -13721,6 +15501,29 @@ function $RootScopeProvider() {
$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
@@ -13770,6 +15573,7 @@ function $RootScopeProvider() {
this.$$destroyed = false;
this.$$listeners = {};
this.$$listenerCount = {};
+ this.$$watchersCount = 0;
this.$$isolateBindings = null;
}
@@ -13872,10 +15676,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,
@@ -13979,11 +15783,11 @@ function $RootScopeProvider() {
* comparing for reference equality.
* @returns {function()} Returns a deregistration function for this listener.
*/
- $watch: function(watchExp, listener, objectEquality) {
+ $watch: function(watchExp, listener, objectEquality, prettyPrintExpression) {
var get = $parse(watchExp);
if (get.$$watchDelegate) {
- return get.$$watchDelegate(this, listener, objectEquality, get);
+ return get.$$watchDelegate(this, listener, objectEquality, get, watchExp);
}
var scope = this,
array = scope.$$watchers,
@@ -13991,7 +15795,7 @@ function $RootScopeProvider() {
fn: listener,
last: initWatchVal,
get: get,
- exp: watchExp,
+ exp: prettyPrintExpression || watchExp,
eq: !!objectEquality
};
@@ -14007,9 +15811,12 @@ function $RootScopeProvider() {
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
+ incrementWatchersCount(this, 1);
return function deregisterWatch() {
- arrayRemove(array, watcher);
+ if (arrayRemove(array, watcher) >= 0) {
+ incrementWatchersCount(scope, -1);
+ }
lastDirtyWatch = null;
};
},
@@ -14221,7 +16028,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];
@@ -14243,7 +16050,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];
}
@@ -14337,7 +16144,7 @@ function $RootScopeProvider() {
*
*/
$digest: function() {
- var watch, value, last,
+ var watch, value, last, fn, get,
watchers,
length,
dirty, ttl = TTL,
@@ -14383,7 +16190,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'
@@ -14391,7 +16199,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] = [];
@@ -14417,7 +16226,7 @@ function $RootScopeProvider() {
// Insanity Warning: scope depth-first traversal
// yes, this code is a bit crazy, but it works and we have tests to prove it!
// this piece should be kept in sync with the traversal in $broadcast
- if (!(next = (current.$$childHead ||
+ if (!(next = ((current.$$watchersCount && current.$$childHead) ||
(current !== target && current.$$nextSibling)))) {
while (current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
@@ -14484,22 +16293,27 @@ function $RootScopeProvider() {
* clean up DOM bindings before an element is removed from the DOM.
*/
$destroy: function() {
- // we can't destroy the root scope or a scope that has been already destroyed
+ // We can't destroy a scope that has been already destroyed.
if (this.$$destroyed) return;
var parent = this.$parent;
this.$broadcast('$destroy');
this.$$destroyed = true;
- if (this === $rootScope) return;
+ if (this === $rootScope) {
+ //Remove handlers attached to window when $rootScope is removed
+ $browser.$$applicationDestroyed();
+ }
+
+ incrementWatchersCount(this, -this.$$watchersCount);
for (var eventName in this.$$listenerCount) {
decrementListenerCount(this, this.$$listenerCount[eventName], eventName);
}
// sever all the references to parent scopes (after this cleanup, the current scope should
// not be retained by any of our references and should be eligible for garbage collection)
- if (parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
- if (parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
+ if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
+ if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
@@ -14508,16 +16322,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);
},
/**
@@ -14593,7 +16400,7 @@ function $RootScopeProvider() {
});
}
- asyncQueue.push({scope: this, expression: expr, locals: locals});
+ asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
@@ -14648,11 +16455,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) {
@@ -14682,6 +16492,7 @@ function $RootScopeProvider() {
$applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
+ expr = $parse(expr);
scheduleApplyAsync();
function $applyAsyncExpression() {
@@ -14913,6 +16724,11 @@ function $RootScopeProvider() {
$rootScope.$$phase = null;
}
+ function incrementWatchersCount(current, count) {
+ do {
+ current.$$watchersCount += count;
+ } while ((current = current.$parent));
+ }
function decrementListenerCount(current, count, name) {
do {
@@ -14952,6 +16768,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.
*/
@@ -15165,13 +16996,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.
*
@@ -15194,17 +17027,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.
*
@@ -15315,7 +17148,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
@@ -15370,7 +17203,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);
@@ -15445,7 +17278,7 @@ function $SceDelegateProvider() {
* Here's an example of a binding in a privileged context:
*
* ```
- * <input ng-model="userHtml">
+ * <input ng-model="userHtml" aria-label="User input">
* <div ng-bind-html="userHtml"></div>
* ```
*
@@ -15505,7 +17338,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*:
@@ -15563,7 +17396,7 @@ 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
* appropriate for use in a scheme, domain, etc. as it would match too much. (e.g.
@@ -15829,7 +17662,7 @@ function $SceProvider() {
* escaping.
*
* @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resource_url, html, js and css.
+ * resourceUrl, html, js and css.
* @param {*} value The value that that should be considered trusted/safe.
* @returns {*} A value that can be used to stand in for the provided `value` in places
* where Angular expects a $sce.trustAs() return value.
@@ -16098,7 +17931,7 @@ function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
android =
- int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
+ toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {},
vendorPrefix,
@@ -16125,8 +17958,8 @@ function $SnifferProvider() {
animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
if (android && (!transitions || !animations)) {
- transitions = isString(document.body.style.webkitTransition);
- animations = isString(document.body.style.webkitAnimation);
+ transitions = isString(bodyStyle.webkitTransition);
+ animations = isString(bodyStyle.webkitAnimation);
}
}
@@ -16184,7 +18017,7 @@ var $compileMinErr = minErr('$compile');
* @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} the HTTP Promise for the given.
+ * @return {Promise} a promise for the HTTP response data of the given URL.
*
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
*/
@@ -16222,12 +18055,14 @@ function $TemplateRequestProvider() {
handleRequestFn.totalPendingRequests--;
})
.then(function(response) {
+ $templateCache.put(tpl, response.data);
return response.data;
}, handleError);
function handleError(resp) {
if (!ignoreRequestError) {
- throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
+ throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
}
return $q.reject(resp);
}
@@ -16357,6 +18192,7 @@ function $$TestabilityProvider() {
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
+
var deferreds = {};
@@ -16369,31 +18205,42 @@ function $TimeoutProvider() {
* block and delegates any exceptions to
* {@link ng.$exceptionHandler $exceptionHandler} service.
*
- * The return value of registering a timeout function is a promise, which will be resolved when
- * the timeout is reached and the timeout function is executed.
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
*
* To cancel a timeout request, call `$timeout.cancel(promise)`.
*
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
* synchronously flush the queue of deferred functions.
*
- * @param {function()} fn A function, whose execution should be delayed.
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
* @param {number=} [delay=0] Delay in milliseconds.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
- * promise will be resolved with is the return value of the `fn` function.
+ * @param {...*=} Pass additional parameters to the executed function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
*
*/
function timeout(fn, delay, invokeApply) {
- var skipApply = (isDefined(invokeApply) && !invokeApply),
+ if (!isFunction(fn)) {
+ invokeApply = delay;
+ delay = fn;
+ fn = noop;
+ }
+
+ var args = sliceArgs(arguments, 3),
+ skipApply = (isDefined(invokeApply) && !invokeApply),
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise,
timeoutId;
timeoutId = $browser.defer(function() {
try {
- deferred.resolve(fn());
+ deferred.resolve(fn.apply(null, args));
} catch (e) {
deferred.reject(e);
$exceptionHandler(e);
@@ -16561,7 +18408,7 @@ function urlIsSameOrigin(requestUrl) {
}]);
</script>
<div ng-controller="ExampleController">
- <input type="text" ng-model="greeting" />
+ <input type="text" ng-model="greeting" aria-label="greeting" />
<button ng-click="doGreeting(greeting)">ALERT</button>
</div>
</file>
@@ -16578,6 +18425,61 @@ function $WindowProvider() {
this.$get = valueFn(window);
}
+/**
+ * @name $$cookieReader
+ * @requires $document
+ *
+ * @description
+ * This is a private service for reading cookies used by $http and ngCookies
+ *
+ * @return {Object} a key/value map of the current cookies
+ */
+function $$CookieReader($document) {
+ var rawDocument = $document[0] || {};
+ var lastCookies = {};
+ var lastCookieString = '';
+
+ function safeDecodeURIComponent(str) {
+ try {
+ return decodeURIComponent(str);
+ } catch (e) {
+ return str;
+ }
+ }
+
+ return function() {
+ var cookieArray, cookie, i, index, name;
+ var currentCookieString = rawDocument.cookie || '';
+
+ if (currentCookieString !== lastCookieString) {
+ lastCookieString = currentCookieString;
+ cookieArray = lastCookieString.split('; ');
+ lastCookies = {};
+
+ for (i = 0; i < cookieArray.length; i++) {
+ cookie = cookieArray[i];
+ index = cookie.indexOf('=');
+ if (index > 0) { //ignore nameless cookies
+ name = safeDecodeURIComponent(cookie.substring(0, index));
+ // the first value that is seen for a cookie is the most
+ // specific one. values for the same cookie name that
+ // follow are for less specific paths.
+ if (isUndefined(lastCookies[name])) {
+ lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
+ }
+ }
+ }
+ }
+ return lastCookies;
+ };
+}
+
+$$CookieReader.$inject = ['$document'];
+
+function $$CookieReaderProvider() {
+ this.$get = $$CookieReader;
+}
+
/* global currencyFilter: true,
dateFilter: true,
filterFilter: true,
@@ -16693,6 +18595,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.
*/
@@ -16774,9 +18677,11 @@ function $FilterProvider($provide) {
* `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but
* **will** be matched by `{$: 'John'}`.
*
- * - `function(value, index)`: A predicate function can be used to write arbitrary filters. The
- * function is called for each element of `array`. The final result is an array of those
- * elements that the predicate returned true for.
+ * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters.
+ * The function is called for each element of the array, with the element, its index, and
+ * the entire array itself as arguments.
+ *
+ * The final result is an array of those elements that the predicate returned true for.
*
* @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
* determining if the expected value (from the filter expression) and actual value (from
@@ -16794,6 +18699,9 @@ function $FilterProvider($provide) {
* - `false|undefined`: A short hand for a function which will look for a substring match in case
* insensitive way.
*
+ * Primitive values are converted to strings. Objects are not compared against primitives,
+ * unless they have a custom `toString` method (e.g. `Date` objects).
+ *
* @example
<example>
<file name="index.html">
@@ -16804,7 +18712,7 @@ function $FilterProvider($provide) {
{name:'Julie', phone:'555-8765'},
{name:'Juliette', phone:'555-5678'}]"></div>
- Search: <input ng-model="searchText">
+ <label>Search: <input ng-model="searchText"></label>
<table id="searchTextResults">
<tr><th>Name</th><th>Phone</th></tr>
<tr ng-repeat="friend in friends | filter:searchText">
@@ -16813,10 +18721,10 @@ function $FilterProvider($provide) {
</tr>
</table>
<hr>
- Any: <input ng-model="search.$"> <br>
- Name only <input ng-model="search.name"><br>
- Phone only <input ng-model="search.phone"><br>
- Equality <input type="checkbox" ng-model="strict"><br>
+ <label>Any: <input ng-model="search.$"></label> <br>
+ <label>Name only <input ng-model="search.name"></label><br>
+ <label>Phone only <input ng-model="search.phone"></label><br>
+ <label>Equality <input type="checkbox" ng-model="strict"></label><br>
<table id="searchObjResults">
<tr><th>Name</th><th>Phone</th></tr>
<tr ng-repeat="friendObj in friends | filter:search:strict">
@@ -16864,9 +18772,15 @@ function $FilterProvider($provide) {
*/
function filterFilter() {
return function(array, expression, comparator) {
- if (!isArray(array)) return array;
+ if (!isArrayLike(array)) {
+ if (array == null) {
+ return array;
+ } else {
+ throw minErr('filter')('notarray', 'Expected array but received: {0}', array);
+ }
+ }
- var expressionType = (expression !== null) ? typeof expression : 'null';
+ var expressionType = getTypeForFilter(expression);
var predicateFn;
var matchAgainstAnyProp;
@@ -16888,7 +18802,7 @@ function filterFilter() {
return array;
}
- return array.filter(predicateFn);
+ return Array.prototype.filter.call(array, predicateFn);
};
}
@@ -16909,8 +18823,8 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
// No substring matching against `null`; only match against `null`
return actual === expected;
}
- if (isObject(actual) || isObject(expected)) {
- // Prevent an object to be considered equal to a string like `'[object'`
+ if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) {
+ // Should not compare primitives against objects, unless they have custom `toString` method
return false;
}
@@ -16931,8 +18845,8 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
}
function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
- var actualType = (actual !== null) ? typeof actual : 'null';
- var expectedType = (expected !== null) ? typeof expected : 'null';
+ var actualType = getTypeForFilter(actual);
+ var expectedType = getTypeForFilter(expected);
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
@@ -16979,6 +18893,15 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
}
}
+// Used for easily differentiating between `null` and actual `object`
+function getTypeForFilter(val) {
+ return (val === null) ? 'null' : typeof val;
+}
+
+var MAX_DIGITS = 22;
+var DECIMAL_SEP = '.';
+var ZERO_CHAR = '0';
+
/**
* @ngdoc filter
* @name currency
@@ -17004,7 +18927,7 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
}]);
</script>
<div ng-controller="ExampleController">
- <input type="number" ng-model="amount"> <br>
+ <input type="number" ng-model="amount" aria-label="amount"> <br>
default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br>
custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span>
no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span>
@@ -17024,9 +18947,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
}
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>
@@ -17060,14 +18983,15 @@ 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.
*
+ *
* @param {number|string} number Number to format.
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
- * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
+ * @returns {string} Number rounded to fractionSize and places a “,” after each third digit.
*
* @example
<example module="numberFilterExample">
@@ -17079,7 +19003,7 @@ function currencyFilter($locale) {
}]);
</script>
<div ng-controller="ExampleController">
- Enter number: <input ng-model='val'><br>
+ <label>Enter number: <input ng-model='val'></label><br>
Default formatting: <span id='number-default'>{{val | number}}</span><br>
No fractions: <span>{{val | number:0}}</span><br>
Negative number: <span>{{-val | number:4}}</span>
@@ -17102,8 +19026,6 @@ function currencyFilter($locale) {
</file>
</example>
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
@@ -17117,86 +19039,194 @@ function numberFilter($locale) {
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (!isFinite(number) || isObject(number)) return '';
-
- var isNegative = number < 0;
- number = Math.abs(number);
- var numStr = number + '',
- formatedText = '',
- parts = [];
-
- var hasExponent = false;
- if (numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- number = 0;
- } else {
- formatedText = numStr;
- hasExponent = true;
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
+
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
+ }
+
+ // Exponential form?
+ if ((i = numStr.search(/e/i)) > 0) {
+ // Work out the exponent.
+ if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
+ numberOfIntegerDigits += +numStr.slice(i + 1);
+ numStr = numStr.substring(0, i);
+ } else if (numberOfIntegerDigits < 0) {
+ // There was no decimal point or exponent so it is an integer.
+ numberOfIntegerDigits = numStr.length;
+ }
+
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) == ZERO_CHAR; i++) {/* jshint noempty: false */}
+
+ if (i == (zeros = numStr.length)) {
+ // The digits are all zero.
+ digits = [0];
+ numberOfIntegerDigits = 1;
+ } else {
+ // Count the number of trailing zeros
+ zeros--;
+ while (numStr.charAt(zeros) == ZERO_CHAR) zeros--;
+
+ // Trailing zeros are insignificant so ignore them
+ numberOfIntegerDigits -= i;
+ digits = [];
+ // Convert string to array of digits without leading/trailing zeros.
+ for (j = 0; i <= zeros; i++, j++) {
+ digits[j] = +numStr.charAt(i);
}
}
- if (!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;
+
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
+
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
+
+ if (roundAt > 0) {
+ digits.splice(roundAt);
+ } else {
+ // We rounded to zero so reset the parsedNumber
+ parsedNumber.i = 1;
+ digits.length = roundAt = fractionSize + 1;
+ for (var i=0; i < roundAt; i++) digits[i] = 0;
}
- // 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);
+ if (digit >= 5) digits[roundAt - 1]++;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < fractionSize; fractionLen++) digits.push(0);
- var i, pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (i = 0; i < pos; i++) {
- if ((pos - i) % group === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
+ // 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++;
}
+}
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i) % lgroup === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
+/**
+ * Format a number into a string
+ * @param {number} number The number to format
+ * @param {{
+ * minFrac, // the minimum number of digits required in the fraction part of the number
+ * maxFrac, // the maximum number of digits required in the fraction part of the number
+ * gSize, // number of digits in each group of separated digits
+ * lgSize, // number of digits in the last group of digits before the decimal separator
+ * negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
+ * posPre, // the string to go in front of a positive number
+ * negSuf, // the string to go after a negative number (e.g. `)`)
+ * posSuf // the string to go after a positive number
+ * }} pattern
+ * @param {string} groupSep The string to separate groups of number (e.g. `,`)
+ * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
+ * @param {[type]} fractionSize The size of the fractional part of the number
+ * @return {string} The number formatted as a string
+ */
+function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
+
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
+
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
+ } else {
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
+
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
}
- // format fraction part.
- while (fraction.length < fractionSize) {
- fraction += '0';
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen);
+ } else {
+ decimals = digits;
+ digits = [0];
}
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
- } else {
- if (fractionSize > 0 && number < 1) {
- formatedText = number.toFixed(fractionSize);
- number = parseFloat(formatedText);
+ // 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);
- if (number === 0) {
- isNegative = false;
- }
+ // 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) {
@@ -17206,9 +19236,10 @@ function padNumber(num, digits, trim) {
num = -num;
}
num = '' + num;
- while (num.length < digits) num = '0' + num;
- if (trim)
+ while (num.length < digits) num = ZERO_CHAR + num;
+ if (trim) {
num = num.substr(num.length - digits);
+ }
return neg + num;
}
@@ -17217,8 +19248,9 @@ function dateGetter(name, size, offset, trim) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
- if (offset > 0 || value > -offset)
+ if (offset > 0 || value > -offset) {
value += offset;
+ }
if (value === 0 && offset == -12) value = 12;
return padNumber(value, size, trim);
};
@@ -17233,8 +19265,8 @@ function dateStrGetter(name, shortForm) {
};
}
-function timeZoneGetter(date) {
- var zone = -1 * date.getTimezoneOffset();
+function timeZoneGetter(date, formats, offset) {
+ var zone = -1 * offset;
var paddedZone = (zone >= 0) ? "+" : "";
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
@@ -17378,7 +19410,9 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
* specified in the string input, the time is considered to be in the local timezone.
* @param {string=} format Formatting rules (see Description). If not specified,
* `mediumDate` is used.
- * @param {string=} timezone Timezone to be used for formatting. Right now, only `'UTC'` is supported.
+ * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
* If not specified, the timezone of the browser will be used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
@@ -17424,13 +19458,13 @@ function dateFilter($locale) {
timeSetter = match[8] ? date.setUTCHours : date.setHours;
if (match[9]) {
- tzHour = int(match[9] + match[10]);
- tzMin = int(match[9] + match[11]);
+ tzHour = toInt(match[9] + match[10]);
+ tzMin = toInt(match[9] + match[11]);
}
- dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3]));
- var h = int(match[4] || 0) - tzHour;
- var m = int(match[5] || 0) - tzMin;
- var s = int(match[6] || 0);
+ dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
+ var h = toInt(match[4] || 0) - tzHour;
+ var m = toInt(match[5] || 0) - tzMin;
+ var s = toInt(match[6] || 0);
var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
timeSetter.call(date, h, m, s, ms);
return date;
@@ -17447,14 +19481,14 @@ function dateFilter($locale) {
format = format || 'mediumDate';
format = $locale.DATETIME_FORMATS[format] || format;
if (isString(date)) {
- date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date);
+ date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
}
if (isNumber(date)) {
date = new Date(date);
}
- if (!isDate(date)) {
+ if (!isDate(date) || !isFinite(date.getTime())) {
return date;
}
@@ -17469,14 +19503,15 @@ function dateFilter($locale) {
}
}
- if (timezone && timezone === 'UTC') {
- date = new Date(date.getTime());
- date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ if (timezone) {
+ dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ date = convertTimezoneToLocal(date, timezone, true);
}
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
- text += fn ? fn(date, $locale.DATETIME_FORMATS)
- : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
+ : value === "''" ? "'" : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
});
return text;
@@ -17561,7 +19596,10 @@ var uppercaseFilter = valueFn(uppercase);
* @param {string|number} limit The length of the returned array or string. If the `limit` number
* is positive, `limit` number of items from the beginning of the source array/string are copied.
* If the number is negative, `limit` number of items from the end of the source array/string
- * are copied. The `limit` will be trimmed if it exceeds `array.length`
+ * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
+ * the input will be returned unchanged.
+ * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin`
+ * indicates an offset from the end of `input`. Defaults to `0`.
* @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
* had less than `limit` elements.
*
@@ -17580,11 +19618,20 @@ var uppercaseFilter = valueFn(uppercase);
}]);
</script>
<div ng-controller="ExampleController">
- Limit {{numbers}} to: <input type="number" step="1" ng-model="numLimit">
+ <label>
+ Limit {{numbers}} to:
+ <input type="number" step="1" ng-model="numLimit">
+ </label>
<p>Output numbers: {{ numbers | limitTo:numLimit }}</p>
- Limit {{letters}} to: <input type="number" step="1" ng-model="letterLimit">
+ <label>
+ Limit {{letters}} to:
+ <input type="number" step="1" ng-model="letterLimit">
+ </label>
<p>Output letters: {{ letters | limitTo:letterLimit }}</p>
- Limit {{longNumber}} to: <input type="number" step="1" ng-model="longNumberLimit">
+ <label>
+ Limit {{longNumber}} to:
+ <input type="number" step="1" ng-model="longNumberLimit">
+ </label>
<p>Output long number: {{ longNumber | limitTo:longNumberLimit }}</p>
</div>
</file>
@@ -17633,21 +19680,28 @@ var uppercaseFilter = valueFn(uppercase);
</example>
*/
function limitToFilter() {
- return function(input, limit) {
- if (isNumber(input)) input = input.toString();
- if (!isArray(input) && !isString(input)) return input;
-
+ return function(input, limit, begin) {
if (Math.abs(Number(limit)) === Infinity) {
limit = Number(limit);
} else {
- limit = int(limit);
+ limit = toInt(limit);
}
+ if (isNaN(limit)) return input;
+
+ if (isNumber(input)) input = input.toString();
+ if (!isArray(input) && !isString(input)) return input;
- //NaN check on limit
- if (limit) {
- return limit > 0 ? input.slice(0, limit) : input.slice(limit);
+ begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
+ begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
+
+ if (limit >= 0) {
+ return input.slice(begin, begin + limit);
} else {
- return isString(input) ? "" : [];
+ if (begin === 0) {
+ return input.slice(limit, input.length);
+ } else {
+ return input.slice(Math.max(0, begin + limit), begin);
+ }
}
};
}
@@ -17660,7 +19714,7 @@ function limitToFilter() {
* @description
* Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
* for strings and numerically for numbers. Note: if you notice numbers are not being sorted
- * correctly, make sure they are actually being saved as numbers and not strings.
+ * as expected, make sure they are actually being saved as numbers and not strings.
*
* @param {Array} array The array to sort.
* @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
@@ -17693,17 +19747,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>
@@ -17719,6 +19762,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,
@@ -17726,28 +19780,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';
- }]);
- </script>
<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="predicate = 'name'; reverse=false">Name</a>
- (<a href="" ng-click="predicate = '-name'; reverse=false">^</a>)</th>
- <th><a href="" ng-click="predicate = 'phone'; reverse=!reverse">Phone Number</a></th>
- <th><a href="" ng-click="predicate = 'age'; reverse=!reverse">Age</a></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>
@@ -17757,6 +19807,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
@@ -17768,21 +19843,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">
@@ -17796,101 +19880,142 @@ 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;
- sortPredicate = isArray(sortPredicate) ? sortPredicate : [sortPredicate];
+
+ if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
- sortPredicate = sortPredicate.map(function(predicate) {
- var descending = false, get = predicate || identity;
- if (isString(predicate)) {
+
+ 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)
+ // See https://en.wikipedia.org/wiki/Schwartzian_transform
+ var compareValues = Array.prototype.map.call(array, getComparisonObject);
+ compareValues.sort(doComparison);
+ array = compareValues.map(function(item) { return item.value; });
+
+ return array;
+
+ function getComparisonObject(value, index) {
+ return {
+ value: value,
+ predicateValues: predicates.map(function(predicate) {
+ return getPredicateValue(predicate.get(value), index);
+ })
+ };
+ }
+
+ function doComparison(v1, v2) {
+ var result = 0;
+ for (var index=0, length = predicates.length; index < length; ++index) {
+ result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
+ if (result) break;
+ }
+ return result;
+ }
+ };
+
+ function processPredicates(sortPredicate, reverseOrder) {
+ reverseOrder = reverseOrder ? -1 : 1;
+ return sortPredicate.map(function(predicate) {
+ var descending = 1, get = identity;
+
+ if (isFunction(predicate)) {
+ get = predicate;
+ } else if (isString(predicate)) {
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-';
+ descending = predicate.charAt(0) == '-' ? -1 : 1;
predicate = predicate.substring(1);
}
- if (predicate === '') {
- // Effectively no predicate was passed so we compare identity
- return reverseComparator(compare, descending);
- }
- get = $parse(predicate);
- if (get.constant) {
- var key = get();
- return reverseComparator(function(a, b) {
- return compare(a[key], b[key]);
- }, descending);
+ if (predicate !== '') {
+ get = $parse(predicate);
+ if (get.constant) {
+ var key = get();
+ get = function(value) { return value[key]; };
+ }
}
}
- return reverseComparator(function(a, b) {
- return compare(get(a),get(b));
- }, descending);
+ return { get: get, descending: descending * reverseOrder };
});
- return slice.call(array).sort(reverseComparator(comparator, reverseOrder));
+ }
- function comparator(o1, o2) {
- for (var i = 0; i < sortPredicate.length; i++) {
- var comp = sortPredicate[i](o1, o2);
- if (comp !== 0) return comp;
- }
- return 0;
- }
- function reverseComparator(comp, descending) {
- return descending
- ? function(a, b) {return comp(b,a);}
- : comp;
+ function isPrimitive(value) {
+ switch (typeof value) {
+ case 'number': /* falls through */
+ case 'boolean': /* falls through */
+ case 'string':
+ return true;
+ default:
+ return false;
}
+ }
- function isPrimitive(value) {
- switch (typeof value) {
- case 'number': /* falls through */
- case 'boolean': /* falls through */
- case 'string':
- return true;
- default:
- return false;
- }
+ function objectValue(value, index) {
+ // If `valueOf` is a valid function use that
+ if (typeof value.valueOf === 'function') {
+ value = value.valueOf();
+ if (isPrimitive(value)) return value;
}
-
- function objectToString(value) {
- if (value === null) return 'null';
- if (typeof value.valueOf === 'function') {
- value = value.valueOf();
- if (isPrimitive(value)) return value;
- }
- if (typeof value.toString === 'function') {
- value = value.toString();
- if (isPrimitive(value)) return value;
- }
- return '';
+ // If `toString` is a valid function and not the one from `Object.prototype` use that
+ if (hasCustomToString(value)) {
+ value = value.toString();
+ if (isPrimitive(value)) return value;
}
+ // We have a basic object so we use the position of the object in the collection
+ return index;
+ }
- function compare(v1, v2) {
- var t1 = typeof v1;
- var t2 = typeof v2;
- if (t1 === t2 && t1 === "object") {
- v1 = objectToString(v1);
- v2 = objectToString(v2);
- }
- if (t1 === t2) {
- if (t1 === "string") {
- v1 = v1.toLowerCase();
- v2 = v2.toLowerCase();
- }
- if (v1 === v2) return 0;
- return v1 < v2 ? -1 : 1;
- } else {
- return t1 < t2 ? -1 : 1;
+ function getPredicateValue(value, index) {
+ var type = typeof value;
+ if (value === null) {
+ type = 'string';
+ value = 'null';
+ } else if (type === 'string') {
+ value = value.toLowerCase();
+ } else if (type === 'object') {
+ value = objectValue(value, index);
+ }
+ return { value: value, type: type };
+ }
+
+ function compare(v1, v2) {
+ var result = 0;
+ if (v1.type === v2.type) {
+ if (v1.value !== v2.value) {
+ result = v1.value < v2.value ? -1 : 1;
}
+ } else {
+ result = v1.type < v2.type ? -1 : 1;
}
- };
+ return result;
+ }
}
function ngDirective(directive) {
@@ -17919,7 +20044,7 @@ function ngDirective(directive) {
var htmlAnchorDirective = valueFn({
restrict: 'E',
compile: function(element, attr) {
- if (!attr.href && !attr.xlinkHref && !attr.name) {
+ if (!attr.href && !attr.xlinkHref) {
return function(scope, element) {
// If the linked element is not an anchor tag anymore, do nothing
if (element[0].nodeName.toLowerCase() !== 'a') return;
@@ -18006,7 +20131,7 @@ var htmlAnchorDirective = valueFn({
}, 5000, 'page should navigate to /123');
});
- xit('should execute ng-click but not reload when href empty string and name specified', function() {
+ it('should execute ng-click but not reload when href empty string and name specified', function() {
element(by.id('link-4')).click();
expect(element(by.model('value')).getAttribute('value')).toEqual('4');
expect(element(by.id('link-4')).getAttribute('href')).toBe('');
@@ -18051,12 +20176,12 @@ var htmlAnchorDirective = valueFn({
*
* The buggy way to write it:
* ```html
- * <img src="http://www.gravatar.com/avatar/{{hash}}"/>
+ * <img src="http://www.gravatar.com/avatar/{{hash}}" alt="Description"/>
* ```
*
* The correct way to write it:
* ```html
- * <img ng-src="http://www.gravatar.com/avatar/{{hash}}"/>
+ * <img ng-src="http://www.gravatar.com/avatar/{{hash}}" alt="Description" />
* ```
*
* @element IMG
@@ -18077,12 +20202,12 @@ var htmlAnchorDirective = valueFn({
*
* The buggy way to write it:
* ```html
- * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/>
+ * <img srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description"/>
* ```
*
* The correct way to write it:
* ```html
- * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x"/>
+ * <img ng-srcset="http://www.gravatar.com/avatar/{{hash}} 2x" alt="Description" />
* ```
*
* @element IMG
@@ -18101,25 +20226,12 @@ 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>
<file name="index.html">
- Click me to toggle: <input type="checkbox" ng-model="checked"><br/>
+ <label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/>
<button ng-model="button" ng-disabled="checked">Button</button>
</file>
<file name="protractor.js" type="protractor">
@@ -18144,18 +20256,19 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as checked. (Their presence means true and their absence means false.)
- * 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.
+ * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy.
+ *
+ * Note that this directive should not be used together with {@link ngModel `ngModel`},
+ * as this can lead to unexpected behavior.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
* @example
<example>
<file name="index.html">
- Check me to check both: <input type="checkbox" ng-model="master"><br/>
- <input id="checkSlave" type="checkbox" ng-checked="master">
+ <label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/>
+ <input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input">
</file>
<file name="protractor.js" type="protractor">
it('should check both checkBoxes', function() {
@@ -18168,7 +20281,7 @@ var htmlAnchorDirective = valueFn({
*
* @element INPUT
* @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
- * then special attribute "checked" will be set on the element
+ * then the `checked` attribute will be set on the element
*/
@@ -18179,18 +20292,17 @@ 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">
- Check me to make text readonly: <input type="checkbox" ng-model="checked"><br/>
- <input type="text" ng-readonly="checked" value="I'm Angular"/>
+ <label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/>
+ <input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" />
</file>
<file name="protractor.js" type="protractor">
it('should toggle readonly attr', function() {
@@ -18214,19 +20326,17 @@ 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>
<file name="index.html">
- Check me to select: <input type="checkbox" ng-model="selected"><br/>
- <select>
+ <label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/>
+ <select aria-label="ngSelected demo">
<option>Hello!</option>
<option id="greet" ng-selected="selected">Greetings!</option>
</select>
@@ -18252,17 +20362,16 @@ 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">
- Check me check multiple: <input type="checkbox" ng-model="open"><br/>
+ <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/>
<details id="details" ng-open="open">
<summary>Show/Hide me</summary>
</details>
@@ -18283,22 +20392,34 @@ var htmlAnchorDirective = valueFn({
var ngAttributeAliasDirectives = {};
-
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
// binding to multiple is not supported
if (propName == "multiple") return;
+ function defaultLinkFn(scope, element, attr) {
+ scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
+ attr.$set(attrName, !!value);
+ });
+ }
+
var normalized = directiveNormalize('ng-' + attrName);
+ var linkFn = defaultLinkFn;
+
+ if (propName === 'checked') {
+ linkFn = function(scope, element, attr) {
+ // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
+ if (attr.ngModel !== attr[normalized]) {
+ defaultLinkFn(scope, element, attr);
+ }
+ };
+ }
+
ngAttributeAliasDirectives[normalized] = function() {
return {
restrict: 'A',
priority: 100,
- link: function(scope, element, attr) {
- scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
- attr.$set(attrName, !!value);
- });
- }
+ link: linkFn
};
};
});
@@ -18390,6 +20511,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
@@ -18429,8 +20551,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 = {};
@@ -18441,8 +20561,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
@@ -18481,11 +20600,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 responsiblity to make sure the current state propagates to the parent form.
+ *
+ * For example, if an input control is added that is already `$dirty` and has `$error` properties,
+ * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
form.$addControl = function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
@@ -18496,6 +20627,8 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
if (control.$name) {
form[control.$name] = control;
}
+
+ control.$$parentForm = form;
};
// Private API: rename a form control
@@ -18512,11 +20645,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) {
@@ -18533,6 +20673,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
});
arrayRemove(controls, control);
+ control.$$parentForm = nullFormCtrl;
};
@@ -18569,7 +20710,6 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
delete object[property];
}
},
- parentForm: parentForm,
$animate: $animate
});
@@ -18588,7 +20728,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
$animate.addClass(element, DIRTY_CLASS);
form.$dirty = true;
form.$pristine = false;
- parentForm.$setDirty();
+ form.$$parentForm.$setDirty();
};
/**
@@ -18644,7 +20784,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
form.$setSubmitted = function() {
$animate.addClass(element, SUBMITTED_CLASS);
form.$submitted = true;
- parentForm.$setSubmitted();
+ form.$$parentForm.$setSubmitted();
};
}
@@ -18683,17 +20823,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.
@@ -18769,7 +20906,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;
}
@@ -18814,10 +20950,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
@@ -18826,7 +20963,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
@@ -18855,22 +20994,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, null, controller.$name, controller, controller.$name);
+ setter(scope, controller);
attr.$observe(nameAttr, function(newValue) {
if (controller.$name === newValue) return;
- setter(scope, null, controller.$name, undefined, controller.$name);
- parentFormCtrl.$$renameControl(controller, newValue);
- setter(scope, null, 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, null, attr[nameAttr], undefined, controller.$name);
- }
+ controller.$$parentForm.$removeControl(controller);
+ setter(scope, undefined);
extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
});
}
@@ -18879,6 +21020,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;
+ }
}];
};
@@ -18896,15 +21045,33 @@ var ngFormDirective = formDirectiveFactory(true);
// 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#!:.?+=&%@!\-\/]))?$/;
+// 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*)))\s*$/;
+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 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 = {
/**
@@ -18931,9 +21098,13 @@ var inputType = {
* 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.
- * If the expression evaluates to a RegExp object then this is used directly.
- * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$`
- * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * If the 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
@@ -18953,13 +21124,16 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- Single word: <input type="text" name="input" ng-model="example.text"
- ng-pattern="example.word" required ng-trim="false">
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.pattern">
- Single word only!</span>
-
+ <label>Single word:
+ <input type="text" name="input" ng-model="example.text"
+ ng-pattern="example.word" required ng-trim="false">
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.pattern">
+ Single word only!</span>
+ </div>
<tt>text = {{example.text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19016,9 +21190,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
@@ -19038,13 +21220,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- Pick a date in 2013:
+ <label for="exampleInput">Pick a date in 2013:</label>
<input type="date" id="exampleInput" name="input" ng-model="example.value"
placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.date">
- Not a valid date!</span>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.date">
+ Not a valid date!</span>
+ </div>
<tt>value = {{example.value | date: "yyyy-MM-dd"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19108,10 +21292,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
@@ -19131,13 +21323,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- Pick a date between in 2013:
+ <label for="exampleInput">Pick a date between in 2013:</label>
<input type="datetime-local" id="exampleInput" name="input" ng-model="example.value"
placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.datetimelocal">
- Not a valid date!</span>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.datetimelocal">
+ Not a valid date!</span>
+ </div>
<tt>value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19202,10 +21396,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
@@ -19225,13 +21427,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- Pick a between 8am and 5pm:
+ <label for="exampleInput">Pick a 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 />
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.time">
- Not a valid date!</span>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.time">
+ Not a valid date!</span>
+ </div>
<tt>value = {{example.value | date: "HH:mm:ss"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19295,10 +21499,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
@@ -19318,13 +21530,17 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- Pick a date between in 2013:
- <input id="exampleInput" type="week" name="input" ng-model="example.value"
- placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required />
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.week">
- Not a valid date!</span>
+ <label>Pick a date between in 2013:
+ <input id="exampleInput" type="week" name="input" ng-model="example.value"
+ placeholder="YYYY-W##" min="2012-W32"
+ max="2013-W52" required />
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.week">
+ Not a valid date!</span>
+ </div>
<tt>value = {{example.value | date: "yyyy-Www"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19388,10 +21604,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
@@ -19411,13 +21636,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- Pick a month in 2013:
+ <label for="exampleInput">Pick a month in 2013:</label>
<input id="exampleInput" type="month" name="input" ng-model="example.value"
placeholder="yyyy-MM" min="2013-01" max="2013-12" required />
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.month">
- Not a valid month!</span>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.month">
+ Not a valid month!</span>
+ </div>
<tt>value = {{example.value | date: "yyyy-MM"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19478,6 +21705,16 @@ var inputType = {
* error docs for more information and an example of how to convert your model if necessary.
* </div>
*
+ * ## Issues with HTML5 constraint validation
+ *
+ * In browsers that follow the
+ * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
+ * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}.
+ * If a non-number is entered in the input, the browser will report the value as an empty string,
+ * which means the view / model values in `ngModel` and subsequently the scope value
+ * will also be an empty string.
+ *
+ *
* @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`.
@@ -19496,9 +21733,13 @@ var inputType = {
* 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.
- * If the expression evaluates to a RegExp object then this is used directly.
- * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$`
- * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * If the 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
@@ -19514,12 +21755,16 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- Number: <input type="number" name="input" ng-model="example.value"
- min="0" max="99" required>
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.number">
- Not valid number!</span>
+ <label>Number:
+ <input type="number" name="input" ng-model="example.value"
+ min="0" max="99" required>
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.number">
+ Not valid number!</span>
+ </div>
<tt>value = {{example.value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19586,9 +21831,13 @@ var inputType = {
* 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.
- * If the expression evaluates to a RegExp object then this is used directly.
- * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$`
- * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * If the 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
@@ -19604,11 +21853,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- URL: <input type="url" name="input" ng-model="url.text" required>
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.url">
- Not valid url!</span>
+ <label>URL:
+ <input type="url" name="input" ng-model="url.text" required>
+ <label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.url">
+ Not valid url!</span>
+ </div>
<tt>text = {{url.text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19677,9 +21930,13 @@ var inputType = {
* 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.
- * If the expression evaluates to a RegExp object then this is used directly.
- * If the expression is a string then it will be converted to a RegExp after wrapping it in `^` and `$`
- * characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * If the 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
@@ -19695,11 +21952,15 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- Email: <input type="email" name="input" ng-model="email.text" required>
- <span class="error" ng-show="myForm.input.$error.required">
- Required!</span>
- <span class="error" ng-show="myForm.input.$error.email">
- Not valid email!</span>
+ <label>Email:
+ <input type="email" name="input" ng-model="email.text" required>
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.input.$error.required">
+ Required!</span>
+ <span class="error" ng-show="myForm.input.$error.email">
+ Not valid email!</span>
+ </div>
<tt>text = {{email.text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
@@ -19745,12 +22006,15 @@ var inputType = {
* HTML radio button.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
- * @param {string} value The value to which the expression should be set when selected.
+ * @param {string} value The value to which the `ngModel` expression should be set when selected.
+ * Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
+ * too. Use `ngValue` if you need complex models (`number`, `object`, ...).
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
- * @param {string} ngValue Angular expression which sets the value to which the expression should
- * be set when selected.
+ * @param {string} ngValue Angular expression to which `ngModel` will be be set when the radio
+ * is selected. Should be used instead of the `value` attribute if you need
+ * a non-string `ngModel` (`boolean`, `array`, ...).
*
* @example
<example name="radio-input-directive" module="radioExample">
@@ -19768,9 +22032,18 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- <input type="radio" ng-model="color.name" value="red"> Red <br/>
- <input type="radio" ng-model="color.name" ng-value="specialValue"> Green <br/>
- <input type="radio" ng-model="color.name" value="blue"> Blue <br/>
+ <label>
+ <input type="radio" ng-model="color.name" value="red">
+ Red
+ </label><br/>
+ <label>
+ <input type="radio" ng-model="color.name" ng-value="specialValue">
+ Green
+ </label><br/>
+ <label>
+ <input type="radio" ng-model="color.name" value="blue">
+ Blue
+ </label><br/>
<tt>color = {{color.name | json}}</tt><br/>
</form>
Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
@@ -19818,9 +22091,13 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
- Value1: <input type="checkbox" ng-model="checkboxModel.value1"> <br/>
- Value2: <input type="checkbox" ng-model="checkboxModel.value2"
- ng-true-value="'YES'" ng-false-value="'NO'"> <br/>
+ <label>Value1:
+ <input type="checkbox" ng-model="checkboxModel.value1">
+ </label><br/>
+ <label>Value2:
+ <input type="checkbox" ng-model="checkboxModel.value2"
+ ng-true-value="'YES'" ng-false-value="'NO'">
+ </label><br/>
<tt>value1 = {{checkboxModel.value1}}</tt><br/>
<tt>value2 = {{checkboxModel.value2}}</tt><br/>
</form>
@@ -19881,6 +22158,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
+ var timeout;
+
var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
@@ -19910,8 +22189,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() {
@@ -19943,8 +22220,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);
+ }
};
}
@@ -20045,8 +22346,8 @@ function createDateInputType(type, regexp, parseDate, format) {
// parser/formatter in the processing chain so that the model
// contains some different data format!
var parsedDate = parseDate(value, previousDate);
- if (timezone === 'UTC') {
- parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset());
+ if (timezone) {
+ parsedDate = convertTimezoneToLocal(parsedDate, timezone);
}
return parsedDate;
}
@@ -20059,9 +22360,8 @@ function createDateInputType(type, regexp, parseDate, format) {
}
if (isValidDate(value)) {
previousDate = value;
- if (previousDate && timezone === 'UTC') {
- var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
- previousDate = new Date(previousDate.getTime() + timezoneOffset);
+ if (previousDate && timezone) {
+ previousDate = convertTimezoneToLocal(previousDate, timezone, true);
}
return $filter('date')(value, format, timezone);
} else {
@@ -20098,7 +22398,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;
}
};
}
@@ -20285,9 +22585,15 @@ 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 value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
+ * @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.
+ * 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
@@ -20318,9 +22624,15 @@ 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 value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
+ * @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.
+ * 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$')`.<br />
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
@@ -20338,26 +22650,36 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
</script>
<div ng-controller="ExampleController">
<form name="myForm">
- User name: <input type="text" name="userName" ng-model="user.name" required>
- <span class="error" ng-show="myForm.userName.$error.required">
- Required!</span><br>
- Last name: <input type="text" name="lastName" ng-model="user.last"
- ng-minlength="3" ng-maxlength="10">
- <span class="error" ng-show="myForm.lastName.$error.minlength">
- Too short!</span>
- <span class="error" ng-show="myForm.lastName.$error.maxlength">
- Too long!</span><br>
+ <label>
+ User name:
+ <input type="text" name="userName" ng-model="user.name" required>
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.userName.$error.required">
+ Required!</span>
+ </div>
+ <label>
+ Last name:
+ <input type="text" name="lastName" ng-model="user.last"
+ ng-minlength="3" ng-maxlength="10">
+ </label>
+ <div role="alert">
+ <span class="error" ng-show="myForm.lastName.$error.minlength">
+ Too short!</span>
+ <span class="error" ng-show="myForm.lastName.$error.maxlength">
+ Too long!</span>
+ </div>
</form>
<hr>
<tt>user = {{user}}</tt><br/>
- <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br>
- <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br>
- <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br>
- <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br>
- <tt>myForm.$valid = {{myForm.$valid}}</tt><br>
- <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
- <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br>
- <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br>
+ <tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br/>
+ <tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br/>
+ <tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br/>
+ <tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br/>
+ <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
+ <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
+ <tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br/>
+ <tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br/>
</div>
</file>
<file name="protractor.js" type="protractor">
@@ -20546,7 +22868,7 @@ var ngValueDirective = function() {
}]);
</script>
<div ng-controller="ExampleController">
- Enter name: <input type="text" ng-model="name"><br>
+ <label>Enter name: <input type="text" ng-model="name"></label><br>
Hello <span ng-bind="name"></span>!
</div>
</file>
@@ -20571,7 +22893,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;
});
};
}
@@ -20607,8 +22929,8 @@ var ngBindDirective = ['$compile', function($compile) {
}]);
</script>
<div ng-controller="ExampleController">
- Salutation: <input type="text" ng-model="salutation"><br>
- Name: <input type="text" ng-model="name"><br>
+ <label>Salutation: <input type="text" ng-model="salutation"></label><br>
+ <label>Name: <input type="text" ng-model="name"></label><br>
<pre ng-bind-template="{{salutation}} {{name}}!"></pre>
</div>
</file>
@@ -20639,7 +22961,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;
});
};
}
@@ -20833,7 +23155,9 @@ function classDirective(name, selector) {
}
function digestClassCounts(classes, count) {
- var classCounts = element.data('$classCounts') || {};
+ // Use createMap() to prevent class assumptions involving property
+ // names in Object.prototype
+ var classCounts = element.data('$classCounts') || createMap();
var classesToUpdate = [];
forEach(classes, function(className) {
if (count > 0 || classCounts[className]) {
@@ -20890,12 +23214,15 @@ function classDirective(name, selector) {
}
function arrayClasses(classVal) {
+ var classes = [];
if (isArray(classVal)) {
- return classVal;
+ forEach(classVal, function(v) {
+ classes = classes.concat(arrayClasses(v));
+ });
+ return classes;
} else if (isString(classVal)) {
return classVal.split(' ');
} else if (isObject(classVal)) {
- var classes = [];
forEach(classVal, function(v, k) {
if (v) {
classes = classes.concat(k.split(' '));
@@ -20923,16 +23250,18 @@ function classDirective(name, selector) {
* 1. If the expression evaluates to a string, the string should be one or more space-delimited class
* names.
*
- * 2. If the expression evaluates to an array, each element of the array should be a string that is
- * one or more space-delimited class names.
- *
- * 3. If the expression evaluates to an object, then for each key-value pair of the
+ * 2. If the expression evaluates to an object, then for each key-value pair of the
* object with a truthy value the corresponding key is used as a class name.
*
+ * 3. If the expression evaluates to an array, each element of the array should either be a string as in
+ * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array
+ * to give you more control over what CSS classes appear. See the code below for an example of this.
+ *
+ *
* The directive won't add duplicate classes if a particular class was already set.
*
- * When the expression changes, the previously added classes are removed and only then the
- * new classes are added.
+ * When the expression changes, the previously added classes are removed and only then are the
+ * new classes added.
*
* @animations
* **add** - happens just before the class is applied to the elements
@@ -20949,22 +23278,39 @@ function classDirective(name, selector) {
* @example Example that demonstrates basic bindings via ngClass directive.
<example>
<file name="index.html">
- <p ng-class="{strike: deleted, bold: important, red: error}">Map Syntax Example</p>
- <input type="checkbox" ng-model="deleted"> deleted (apply "strike" class)<br>
- <input type="checkbox" ng-model="important"> important (apply "bold" class)<br>
- <input type="checkbox" ng-model="error"> error (apply "red" class)
+ <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
+ <label>
+ <input type="checkbox" ng-model="deleted">
+ deleted (apply "strike" class)
+ </label><br>
+ <label>
+ <input type="checkbox" ng-model="important">
+ important (apply "bold" class)
+ </label><br>
+ <label>
+ <input type="checkbox" ng-model="error">
+ error (apply "has-error" class)
+ </label>
<hr>
<p ng-class="style">Using String Syntax</p>
- <input type="text" ng-model="style" placeholder="Type: bold strike red">
+ <input type="text" ng-model="style"
+ placeholder="Type: bold strike red" aria-label="Type: bold strike red">
<hr>
<p ng-class="[style1, style2, style3]">Using Array Syntax</p>
- <input ng-model="style1" placeholder="Type: bold, strike or red"><br>
- <input ng-model="style2" placeholder="Type: bold, strike or red"><br>
- <input ng-model="style3" placeholder="Type: bold, strike or red"><br>
+ <input ng-model="style1"
+ placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red"><br>
+ <input ng-model="style2"
+ placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 2"><br>
+ <input ng-model="style3"
+ placeholder="Type: bold, strike or red" aria-label="Type: bold, strike or red 3"><br>
+ <hr>
+ <p ng-class="[style4, {orange: warning}]">Using Array and Map Syntax</p>
+ <input ng-model="style4" placeholder="Type: bold, strike" aria-label="Type: bold, strike"><br>
+ <label><input type="checkbox" ng-model="warning"> warning (apply "orange" class)</label>
</file>
<file name="style.css">
.strike {
- text-decoration: line-through;
+ text-decoration: line-through;
}
.bold {
font-weight: bold;
@@ -20972,6 +23318,13 @@ function classDirective(name, selector) {
.red {
color: red;
}
+ .has-error {
+ color: red;
+ background-color: yellow;
+ }
+ .orange {
+ color: orange;
+ }
</file>
<file name="protractor.js" type="protractor">
var ps = element.all(by.css('p'));
@@ -20979,13 +23332,13 @@ function classDirective(name, selector) {
it('should let you toggle the class', function() {
expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
- expect(ps.first().getAttribute('class')).not.toMatch(/red/);
+ expect(ps.first().getAttribute('class')).not.toMatch(/has-error/);
element(by.model('important')).click();
expect(ps.first().getAttribute('class')).toMatch(/bold/);
element(by.model('error')).click();
- expect(ps.first().getAttribute('class')).toMatch(/red/);
+ expect(ps.first().getAttribute('class')).toMatch(/has-error/);
});
it('should let you toggle string example', function() {
@@ -20996,11 +23349,18 @@ function classDirective(name, selector) {
});
it('array example should have 3 classes', function() {
- expect(ps.last().getAttribute('class')).toBe('');
+ expect(ps.get(2).getAttribute('class')).toBe('');
element(by.model('style1')).sendKeys('bold');
element(by.model('style2')).sendKeys('strike');
element(by.model('style3')).sendKeys('red');
- expect(ps.last().getAttribute('class')).toBe('bold strike red');
+ expect(ps.get(2).getAttribute('class')).toBe('bold strike red');
+ });
+
+ it('array with map example should have 2 classes', function() {
+ expect(ps.last().getAttribute('class')).toBe('');
+ element(by.model('style4')).sendKeys('bold');
+ element(by.model('warning')).click();
+ expect(ps.last().getAttribute('class')).toBe('bold orange');
});
</file>
</example>
@@ -21018,7 +23378,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;
}
@@ -21050,8 +23409,8 @@ function classDirective(name, selector) {
The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
- to view the step by step details of {@link ng.$animate#addClass $animate.addClass} and
- {@link ng.$animate#removeClass $animate.removeClass}.
+ to view the step by step details of {@link $animate#addClass $animate.addClass} and
+ {@link $animate#removeClass $animate.removeClass}.
*/
var ngClassDirective = classDirective('', true);
@@ -21274,20 +23633,20 @@ var ngCloakDirective = ngDirective({
* <example name="ngControllerAs" module="controllerAsExample">
* <file name="index.html">
* <div id="ctrl-as-exmpl" ng-controller="SettingsController1 as settings">
- * Name: <input type="text" ng-model="settings.name"/>
- * [ <a href="" ng-click="settings.greet()">greet</a> ]<br/>
+ * <label>Name: <input type="text" ng-model="settings.name"/></label>
+ * <button ng-click="settings.greet()">greet</button><br/>
* Contact:
* <ul>
* <li ng-repeat="contact in settings.contacts">
- * <select ng-model="contact.type">
+ * <select ng-model="contact.type" aria-label="Contact method" id="select_{{$index}}">
* <option>phone</option>
* <option>email</option>
* </select>
- * <input type="text" ng-model="contact.value"/>
- * [ <a href="" ng-click="settings.clearContact(contact)">clear</a>
- * | <a href="" ng-click="settings.removeContact(contact)">X</a> ]
+ * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
+ * <button ng-click="settings.clearContact(contact)">clear</button>
+ * <button ng-click="settings.removeContact(contact)" aria-label="Remove">X</button>
* </li>
- * <li>[ <a href="" ng-click="settings.addContact()">add</a> ]</li>
+ * <li><button ng-click="settings.addContact()">add</button></li>
* </ul>
* </div>
* </file>
@@ -21337,12 +23696,12 @@ var ngCloakDirective = ngDirective({
* expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
* .toBe('john.smith@example.org');
*
- * firstRepeat.element(by.linkText('clear')).click();
+ * firstRepeat.element(by.buttonText('clear')).click();
*
* expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
* .toBe('');
*
- * container.element(by.linkText('add')).click();
+ * container.element(by.buttonText('add')).click();
*
* expect(container.element(by.repeater('contact in settings.contacts').row(2))
* .element(by.model('contact.value'))
@@ -21357,20 +23716,20 @@ var ngCloakDirective = ngDirective({
* <example name="ngController" module="controllerExample">
* <file name="index.html">
* <div id="ctrl-exmpl" ng-controller="SettingsController2">
- * Name: <input type="text" ng-model="name"/>
- * [ <a href="" ng-click="greet()">greet</a> ]<br/>
+ * <label>Name: <input type="text" ng-model="name"/></label>
+ * <button ng-click="greet()">greet</button><br/>
* Contact:
* <ul>
* <li ng-repeat="contact in contacts">
- * <select ng-model="contact.type">
+ * <select ng-model="contact.type" id="select_{{$index}}">
* <option>phone</option>
* <option>email</option>
* </select>
- * <input type="text" ng-model="contact.value"/>
- * [ <a href="" ng-click="clearContact(contact)">clear</a>
- * | <a href="" ng-click="removeContact(contact)">X</a> ]
+ * <input type="text" ng-model="contact.value" aria-labelledby="select_{{$index}}" />
+ * <button ng-click="clearContact(contact)">clear</button>
+ * <button ng-click="removeContact(contact)">X</button>
* </li>
- * <li>[ <a href="" ng-click="addContact()">add</a> ]</li>
+ * <li>[ <button ng-click="addContact()">add</button> ]</li>
* </ul>
* </div>
* </file>
@@ -21420,12 +23779,12 @@ var ngCloakDirective = ngDirective({
* expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
* .toBe('john.smith@example.org');
*
- * firstRepeat.element(by.linkText('clear')).click();
+ * firstRepeat.element(by.buttonText('clear')).click();
*
* expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
* .toBe('');
*
- * container.element(by.linkText('add')).click();
+ * container.element(by.buttonText('add')).click();
*
* expect(container.element(by.repeater('contact in contacts').row(2))
* .element(by.model('contact.value'))
@@ -21451,27 +23810,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
@@ -21480,11 +23841,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 optimising $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 automcatic 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
@@ -21616,7 +24005,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
@@ -22106,6 +24495,7 @@ forEach(
* @ngdoc directive
* @name ngIf
* @restrict A
+ * @multiElement
*
* @description
* The `ngIf` directive removes or recreates a portion of the DOM tree based on an
@@ -22148,7 +24538,7 @@ forEach(
* @example
<example module="ngAnimate" deps="angular-animate.js" animations="true">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /><br/>
+ <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/>
Show when checked:
<span ng-if="checked" class="animate-if">
This is removed when the checkbox is unchecked.
@@ -22162,7 +24552,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;
}
@@ -22260,7 +24649,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.
*
@@ -22311,7 +24706,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;
@@ -22453,6 +24847,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;
@@ -22474,6 +24870,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);
@@ -22529,17 +24927,19 @@ var ngIncludeFillContentDirective = ['$compile',
* The `ngInit` directive allows you to evaluate an expression in the
* current scope.
*
- * <div class="alert alert-error">
- * 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.
+ * <div class="alert alert-danger">
+ * 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>
*
@@ -22616,9 +25016,11 @@ var ngInitDirective = ngDirective({
* </file>
* <file name="index.html">
* <form name="myForm" ng-controller="ExampleController">
- * List: <input name="namesInput" ng-model="names" ng-list required>
- * <span class="error" ng-show="myForm.namesInput.$error.required">
+ * <label>List: <input name="namesInput" ng-model="names" ng-list required></label>
+ * <span role="alert">
+ * <span class="error" ng-show="myForm.namesInput.$error.required">
* Required!</span>
+ * </span>
* <br>
* <tt>names = {{names}}</tt><br/>
* <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
@@ -22650,7 +25052,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>
@@ -22735,7 +25137,9 @@ 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
@@ -22904,7 +25308,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
- <textarea ng-model="userContent"></textarea>
+ <textarea ng-model="userContent" aria-label="Dynamic textarea"></textarea>
</form>
</file>
<file name="protractor.js" type="protractor">
@@ -22949,7 +25353,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,
@@ -22998,10 +25402,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
* committed value then `$render()` is called to update the input control.
* * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
- * the `$viewValue` are different to last time.
+ * 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 to their previous value. If `$modelValue`
+ * `$modelValue` and `$viewValue` are actually different from their previous value. 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.
*/
@@ -23018,7 +25422,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*
* The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
*
- * You can override this for input directives whose concept of being empty is different to the
+ * You can override this for input directives whose concept of being empty is different from the
* default. The `checkboxInputType` directive does this because in its case a value of `false`
* implies empty.
*
@@ -23029,8 +25433,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
return isUndefined(value) || value === '' || value === null || value !== value;
};
- var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
- currentValidationRunId = 0;
+ var currentValidationRunId = 0;
/**
* @ngdoc method
@@ -23063,7 +25466,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
unset: function(object, property) {
delete object[property];
},
- parentForm: parentForm,
$animate: $animate
});
@@ -23101,7 +25503,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();
};
/**
@@ -23148,11 +25550,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.
*
@@ -23165,37 +25570,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>With $rollbackViewValue()</p>
- * <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/>
- * myValue: "{{ myValue }}"
- *
- * <p>Without $rollbackViewValue()</p>
- * <input name="myInput2" 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() {
@@ -23269,7 +25700,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) {
@@ -23309,7 +25740,7 @@ 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);
@@ -23439,37 +25870,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.
+ * 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.
*
- * 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.
- *
- * 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 realise 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) {
@@ -23595,6 +26036,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.
@@ -23646,7 +26103,6 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
</script>
<style>
.my-input {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: transparent;
}
@@ -23655,10 +26111,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
background: red;
}
</style>
- Update input to see transitions when valid/invalid.
- Integer is a valid value.
+ <p id="inputDescription">
+ Update input to see transitions when valid/invalid.
+ Integer is a valid value.
+ </p>
<form name="testForm" ng-controller="ExampleController">
- <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
+ <input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input"
+ aria-describedby="inputDescription" />
</form>
</file>
* </example>
@@ -23668,7 +26127,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
* function that returns a representation of the model when called with zero arguments, and sets
* the internal state of a model when called with an argument. It's sometimes useful to use this
- * for models that have an internal representation that's different than what the model exposes
+ * for models that have an internal representation that's different from what the model exposes
* to the view.
*
* <div class="alert alert-success">
@@ -23688,10 +26147,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
- Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
+ <label>Name:
+ <input type="text" name="userName"
+ ng-model="user.name"
+ ng-model-options="{ getterSetter: true }" />
+ </label>
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
@@ -23729,7 +26189,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);
@@ -23738,12 +26198,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) {
@@ -23781,7 +26241,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
* takes place when a timer expires; this timer will be reset after another change takes place.
*
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
- * be different than the value in the actual model. This means that if you update the model you
+ * be different from the value in the actual model. This means that if you update the model you
* should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
* order to make sure it is synchronized with the model and that any debounced action is canceled.
*
@@ -23803,14 +26263,16 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
* custom value for each event. For example:
- * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
+ * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
`ngModel` as getters/setters.
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
- * `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`,
- * otherwise the default timezone of the browser will be used.
+ * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
*
* @example
@@ -23822,22 +26284,24 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
- Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ updateOn: 'blur' }"
- ng-keyup="cancel($event)" /><br />
-
- Other data:
- <input type="text" ng-model="user.data" /><br />
+ <label>Name:
+ <input type="text" name="userName"
+ ng-model="user.name"
+ ng-model-options="{ updateOn: 'blur' }"
+ ng-keyup="cancel($event)" />
+ </label><br />
+ <label>Other data:
+ <input type="text" ng-model="user.data" />
+ </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) {
@@ -23852,20 +26316,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>
@@ -23877,11 +26341,13 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
- Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ debounce: 1000 }" />
- <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
+ <label>Name:
+ <input type="text" name="userName"
+ ng-model="user.name"
+ ng-model-options="{ debounce: 1000 }" />
+ </label>
+ <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
+ <br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
</div>
@@ -23889,7 +26355,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>
@@ -23900,10 +26366,11 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
- Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
+ <label>Name:
+ <input type="text" name="userName"
+ ng-model="user.name"
+ ng-model-options="{ getterSetter: true }" />
+ </label>
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
@@ -23930,9 +26397,9 @@ var ngModelOptionsDirective = function() {
restrict: 'A',
controller: ['$scope', '$attrs', function($scope, $attrs) {
var that = this;
- this.$options = $scope.$eval($attrs.ngModelOptions);
+ 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() {
@@ -23955,7 +26422,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));
@@ -23963,7 +26429,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);
@@ -24007,7 +26473,7 @@ function addSetValidityMethod(context) {
}
toggleValidationCss(validationErrorKey, combinedState);
- parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
+ ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
}
function createAndSet(name, value, controller) {
@@ -24047,7 +26513,9 @@ function addSetValidityMethod(context) {
function isObjectEmpty(obj) {
if (obj) {
for (var prop in obj) {
- return false;
+ if (obj.hasOwnProperty(prop)) {
+ return false;
+ }
}
}
return true;
@@ -24087,6 +26555,775 @@ function isObjectEmpty(obj) {
*/
var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+/* global jqLiteRemove */
+
+var ngOptionsMinErr = minErr('ngOptions');
+
+/**
+ * @ngdoc directive
+ * @name ngOptions
+ * @restrict A
+ *
+ * @description
+ *
+ * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
+ * elements for the `<select>` element using the array or object obtained by evaluating the
+ * `ngOptions` comprehension expression.
+ *
+ * In many cases, `ngRepeat` can be used on `<option>` elements instead of `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. `ngOptions` should be used when the `<select>` model needs to be bound
+ * to a non-string value. This is because an option element can only be bound to string values at
+ * present.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * ## Complex Models (objects or collections)
+ *
+ * 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.
+ *
+ * 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]`.
+ *
+ * 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`**
+ *
+ * Using `select` **`as`** will bind the result of the `select` expression to the model, but
+ * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
+ * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
+ * is used, the result of that expression will be set as the value of the `option` and `select` elements.
+ *
+ *
+ * ### `select` **`as`** and **`track by`**
+ *
+ * <div class="alert alert-warning">
+ * Be careful when using `select` **`as`** and **`track by`** in the same expression.
+ * </div>
+ *
+ * Given this array of items on the $scope:
+ *
+ * ```js
+ * $scope.items = [{
+ * id: 1,
+ * label: 'aLabel',
+ * subItem: { name: 'aSubItem' }
+ * }, {
+ * id: 2,
+ * label: 'bLabel',
+ * subItem: { name: 'bSubItem' }
+ * }];
+ * ```
+ *
+ * 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:
+ *
+ * ```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;
+ * ```
+ *
+ * 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.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} required The control is considered valid only if value is 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 {comprehension_expression=} ngOptions in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
+ * (for including a filter with `track by`)
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`disable when`** `disable`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `<option>` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent `<select>`
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the `<optgroup>`
+ * DOM element.
+ * * `disable`: The result of this expression will be used to disable the rendered `<option>`
+ * element. Return `true` to disable.
+ * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
+ * used to identify the objects in the array. The `trackexpr` will most likely refer to the
+ * `value` variable (e.g. `value.propertyName`). With this the selection is preserved
+ * even when the options are recreated (e.g. reloaded from the server).
+ *
+ * @example
+ <example module="selectExample">
+ <file name="index.html">
+ <script>
+ angular.module('selectExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.colors = [
+ {name:'black', shade:'dark'},
+ {name:'white', shade:'light', notAnOption: true},
+ {name:'red', shade:'dark'},
+ {name:'blue', shade:'dark', notAnOption: true},
+ {name:'yellow', shade:'light', notAnOption: false}
+ ];
+ $scope.myColor = $scope.colors[2]; // red
+ }]);
+ </script>
+ <div ng-controller="ExampleController">
+ <ul>
+ <li ng-repeat="color in colors">
+ <label>Name: <input ng-model="color.name"></label>
+ <label><input type="checkbox" ng-model="color.notAnOption"> Disabled?</label>
+ <button ng-click="colors.splice($index, 1)" aria-label="Remove">X</button>
+ </li>
+ <li>
+ <button ng-click="colors.push({})">add</button>
+ </li>
+ </ul>
+ <hr/>
+ <label>Color (null not allowed):
+ <select ng-model="myColor" ng-options="color.name for color in colors"></select>
+ </label><br/>
+ <label>Color (null allowed):
+ <span class="nullable">
+ <select ng-model="myColor" ng-options="color.name for color in colors">
+ <option value="">-- choose color --</option>
+ </select>
+ </span></label><br/>
+
+ <label>Color grouped by shade:
+ <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
+ </select>
+ </label><br/>
+
+ <label>Color grouped by shade, with some disabled:
+ <select ng-model="myColor"
+ ng-options="color.name group by color.shade disable when color.notAnOption for color in colors">
+ </select>
+ </label><br/>
+
+
+
+ Select <button ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</button>.
+ <br/>
+ <hr/>
+ Currently selected: {{ {selected_color:myColor} }}
+ <div style="border:solid 1px black; height:20px"
+ ng-style="{'background-color':myColor.name}">
+ </div>
+ </div>
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ng-options', function() {
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
+ element.all(by.model('myColor')).first().click();
+ element.all(by.css('select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
+ element(by.css('.nullable select[ng-model="myColor"]')).click();
+ element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
+ });
+ </file>
+ </example>
+ */
+
+// jshint maxlen: false
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+ // 1: value expression (valueFn)
+ // 2: label expression (displayFn)
+ // 3: group by expression (groupByFn)
+ // 4: disable when expression (disableWhenFn)
+ // 5: array item variable name
+ // 6: object item key variable name
+ // 7: object item value variable name
+ // 8: collection expression
+ // 9: track by expression
+// jshint maxlen: 100
+
+
+var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
+
+ function parseOptionsExpression(optionsExp, selectElement, scope) {
+
+ var match = optionsExp.match(NG_OPTIONS_REGEXP);
+ if (!(match)) {
+ throw ngOptionsMinErr('iexp',
+ "Expected expression in form of " +
+ "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
+ " but got '{0}'. Element: {1}",
+ optionsExp, startingTag(selectElement));
+ }
+
+ // Extract the parts from the ngOptions expression
+
+ // The variable name for the value of the item in the collection
+ var valueName = match[5] || match[7];
+ // The variable name for the key of the item in the collection
+ var keyName = match[6];
+
+ // An expression that generates the viewValue for an option if there is a label expression
+ var selectAs = / as /.test(match[0]) && match[1];
+ // An expression that is used to track the id of each object in the options collection
+ var trackBy = match[9];
+ // An expression that generates the viewValue for an option if there is no label expression
+ var valueFn = $parse(match[2] ? match[1] : valueName);
+ var selectAsFn = selectAs && $parse(selectAs);
+ var viewValueFn = selectAsFn || valueFn;
+ var trackByFn = trackBy && $parse(trackBy);
+
+ // Get the value by which we are going to track the option
+ // if we have a trackFn then use that (passing scope and locals)
+ // otherwise just hash the given viewValue
+ var getTrackByValueFn = trackBy ?
+ function(value, locals) { return trackByFn(scope, locals); } :
+ function getHashOfValue(value) { return hashKey(value); };
+ var getTrackByValue = function(value, key) {
+ return getTrackByValueFn(value, getLocals(value, key));
+ };
+
+ var displayFn = $parse(match[2] || match[1]);
+ var groupByFn = $parse(match[3] || '');
+ var disableWhenFn = $parse(match[4] || '');
+ var valuesFn = $parse(match[8]);
+
+ var locals = {};
+ var getLocals = keyName ? function(value, key) {
+ locals[keyName] = key;
+ locals[valueName] = value;
+ return locals;
+ } : function(value) {
+ locals[valueName] = value;
+ return locals;
+ };
+
+
+ function Option(selectValue, viewValue, label, group, disabled) {
+ this.selectValue = selectValue;
+ this.viewValue = viewValue;
+ this.label = label;
+ this.group = group;
+ this.disabled = disabled;
+ }
+
+ function getOptionValuesKeys(optionValues) {
+ var optionValuesKeys;
+
+ if (!keyName && isArrayLike(optionValues)) {
+ optionValuesKeys = optionValues;
+ } else {
+ // if object, extract keys, in enumeration order, unsorted
+ optionValuesKeys = [];
+ for (var itemKey in optionValues) {
+ if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
+ optionValuesKeys.push(itemKey);
+ }
+ }
+ }
+ return optionValuesKeys;
+ }
+
+ return {
+ trackBy: trackBy,
+ getTrackByValue: getTrackByValue,
+ getWatchables: $parse(valuesFn, function(optionValues) {
+ // Create a collection of things that we would like to watch (watchedArray)
+ // so that they can all be watched using a single $watchCollection
+ // that only runs the handler once if anything changes
+ var watchedArray = [];
+ optionValues = optionValues || [];
+
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+
+ var locals = getLocals(optionValues[key], key);
+ var selectValue = getTrackByValueFn(optionValues[key], locals);
+ watchedArray.push(selectValue);
+
+ // Only need to watch the displayFn if there is a specific label expression
+ if (match[2] || match[1]) {
+ var label = displayFn(scope, locals);
+ watchedArray.push(label);
+ }
+
+ // Only need to watch the disableWhenFn if there is a specific disable expression
+ if (match[4]) {
+ var disableWhen = disableWhenFn(scope, locals);
+ watchedArray.push(disableWhen);
+ }
+ }
+ return watchedArray;
+ }),
+
+ getOptions: function() {
+
+ var optionItems = [];
+ var selectValueMap = {};
+
+ // The option values were already computed in the `getWatchables` fn,
+ // which must have been called to trigger `getOptions`
+ var optionValues = valuesFn(scope) || [];
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+ var locals = getLocals(value, key);
+ var viewValue = viewValueFn(scope, locals);
+ var selectValue = getTrackByValueFn(viewValue, locals);
+ var label = displayFn(scope, locals);
+ var group = groupByFn(scope, locals);
+ var disabled = disableWhenFn(scope, locals);
+ var optionItem = new Option(selectValue, viewValue, label, group, disabled);
+
+ optionItems.push(optionItem);
+ selectValueMap[selectValue] = optionItem;
+ }
+
+ return {
+ items: optionItems,
+ selectValueMap: selectValueMap,
+ getOptionFromViewValue: function(value) {
+ return selectValueMap[getTrackByValue(value)];
+ },
+ getViewValueFromOption: function(option) {
+ // If the viewValue could be an object that may be mutated by the application,
+ // we need to make a copy and not return the reference to the value on the option.
+ return trackBy ? angular.copy(option.viewValue) : option.viewValue;
+ }
+ };
+ }
+ };
+ }
+
+
+ // we can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ var optionTemplate = document.createElement('option'),
+ optGroupTemplate = document.createElement('optgroup');
+
+
+ function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
+
+ // if ngModel is not defined, we don't need to do anything
+ var ngModelCtrl = ctrls[1];
+ if (!ngModelCtrl) return;
+
+ var selectCtrl = ctrls[0];
+ var multiple = attr.multiple;
+
+ // The emptyOption allows the application developer to provide their own custom "empty"
+ // option when the viewValue does not match any of the option values.
+ var emptyOption;
+ for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
+ if (children[i].value === '') {
+ emptyOption = children.eq(i);
+ break;
+ }
+ }
+
+ var providedEmptyOption = !!emptyOption;
+
+ var unknownOption = jqLite(optionTemplate.cloneNode(false));
+ unknownOption.val('?');
+
+ var options;
+ var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
+
+
+ var renderEmptyOption = function() {
+ if (!providedEmptyOption) {
+ selectElement.prepend(emptyOption);
+ }
+ selectElement.val('');
+ emptyOption.prop('selected', true); // needed for IE
+ emptyOption.attr('selected', true);
+ };
+
+ var removeEmptyOption = function() {
+ if (!providedEmptyOption) {
+ emptyOption.remove();
+ }
+ };
+
+
+ var renderUnknownOption = function() {
+ selectElement.prepend(unknownOption);
+ selectElement.val('?');
+ unknownOption.prop('selected', true); // needed for IE
+ unknownOption.attr('selected', true);
+ };
+
+ var removeUnknownOption = function() {
+ unknownOption.remove();
+ };
+
+ // Update the controller methods for multiple selectable options
+ if (!multiple) {
+
+ selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ 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');
+ } else {
+ if (value === null || providedEmptyOption) {
+ removeUnknownOption();
+ renderEmptyOption();
+ } else {
+ removeEmptyOption();
+ renderUnknownOption();
+ }
+ }
+ };
+
+ selectCtrl.readValue = function readNgOptionsValue() {
+
+ var selectedOption = options.selectValueMap[selectElement.val()];
+
+ if (selectedOption && !selectedOption.disabled) {
+ removeEmptyOption();
+ removeUnknownOption();
+ return options.getViewValueFromOption(selectedOption);
+ }
+ return null;
+ };
+
+ // If we are using `track by` then we must watch the tracked value on the model
+ // since ngModel only watches for object identity change
+ if (ngOptions.trackBy) {
+ scope.$watch(
+ function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
+ function() { ngModelCtrl.$render(); }
+ );
+ }
+
+ } else {
+
+ ngModelCtrl.$isEmpty = function(value) {
+ return !value || value.length === 0;
+ };
+
+
+ selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
+ options.items.forEach(function(option) {
+ option.element.selected = false;
+ });
+
+ if (value) {
+ value.forEach(function(item) {
+ var option = options.getOptionFromViewValue(item);
+ if (option && !option.disabled) option.element.selected = true;
+ });
+ }
+ };
+
+
+ selectCtrl.readValue = function readNgOptionsMultiple() {
+ var selectedValues = selectElement.val() || [],
+ selections = [];
+
+ forEach(selectedValues, function(value) {
+ var option = options.selectValueMap[value];
+ if (option && !option.disabled) selections.push(options.getViewValueFromOption(option));
+ });
+
+ return selections;
+ };
+
+ // If we are using `track by` then we must watch these tracked values on the model
+ // since ngModel only watches for object identity change
+ if (ngOptions.trackBy) {
+
+ scope.$watchCollection(function() {
+ if (isArray(ngModelCtrl.$viewValue)) {
+ return ngModelCtrl.$viewValue.map(function(value) {
+ return ngOptions.getTrackByValue(value);
+ });
+ }
+ }, function() {
+ ngModelCtrl.$render();
+ });
+
+ }
+ }
+
+
+ if (providedEmptyOption) {
+
+ // we need to remove it before calling selectElement.empty() because otherwise IE will
+ // remove the label from the element. wtf?
+ emptyOption.remove();
+
+ // compile the element since there might be bindings in it
+ $compile(emptyOption)(scope);
+
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ emptyOption.removeClass('ng-scope');
+ } else {
+ emptyOption = jqLite(optionTemplate.cloneNode(false));
+ }
+
+ // We need to do this here to ensure that the options object is defined
+ // when we first hit it in writeNgOptionsValue
+ updateOptions();
+
+ // We will re-render the option elements if the option values or labels change
+ scope.$watchCollection(ngOptions.getWatchables, updateOptions);
+
+ // ------------------------------------------------------------------ //
+
+
+ function updateOptionElement(option, element) {
+ option.element = element;
+ element.disabled = option.disabled;
+ // 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) {
+ var element;
+ // Check whether we can reuse the next element
+ if (current && lowercase(current.nodeName) === type) {
+ // The next element is the right type so reuse it
+ element = current;
+ } else {
+ // The next element is not the right type so create a new one
+ element = templateElement.cloneNode(false);
+ if (!current) {
+ // There are no more elements so just append it to the select
+ parent.appendChild(element);
+ } else {
+ // The next element is not a group so insert the new one
+ parent.insertBefore(element, current);
+ }
+ }
+ return element;
+ }
+
+
+ function removeExcessElements(current) {
+ var next;
+ while (current) {
+ next = current.nextSibling;
+ jqLiteRemove(current);
+ current = next;
+ }
+ }
+
+
+ function skipEmptyAndUnknownOptions(current) {
+ var emptyOption_ = emptyOption && emptyOption[0];
+ var unknownOption_ = unknownOption && unknownOption[0];
+
+ // We cannot rely on the extracted empty option being the same as the compiled empty option,
+ // because the compiled empty option might have been replaced by a comment because
+ // it had an "element" transclusion directive on it (such as ngIf)
+ if (emptyOption_ || unknownOption_) {
+ while (current &&
+ (current === emptyOption_ ||
+ current === unknownOption_ ||
+ current.nodeType === NODE_TYPE_COMMENT ||
+ (nodeName_(current) === 'option' && current.value === ''))) {
+ current = current.nextSibling;
+ }
+ }
+ return current;
+ }
+
+
+ function updateOptions() {
+
+ var previousValue = options && selectCtrl.readValue();
+
+ options = ngOptions.getOptions();
+
+ var groupMap = {};
+ var currentElement = selectElement[0].firstChild;
+
+ // Ensure that the empty option is always there if it was explicitly provided
+ if (providedEmptyOption) {
+ selectElement.prepend(emptyOption);
+ }
+
+ currentElement = skipEmptyAndUnknownOptions(currentElement);
+
+ options.items.forEach(function updateOption(option) {
+ var group;
+ var groupElement;
+ var optionElement;
+
+ if (option.group) {
+
+ // This option is to live in a group
+ // See if we have already created this group
+ group = groupMap[option.group];
+
+ if (!group) {
+
+ // We have not already created this group
+ groupElement = addOrReuseElement(selectElement[0],
+ currentElement,
+ 'optgroup',
+ optGroupTemplate);
+ // Move to the next element
+ currentElement = groupElement.nextSibling;
+
+ // Update the label on the group element
+ groupElement.label = option.group;
+
+ // Store it for use later
+ group = groupMap[option.group] = {
+ groupElement: groupElement,
+ currentOptionElement: groupElement.firstChild
+ };
+
+ }
+
+ // So now we have a group for this option we add the option to the group
+ optionElement = addOrReuseElement(group.groupElement,
+ group.currentOptionElement,
+ 'option',
+ optionTemplate);
+ updateOptionElement(option, optionElement);
+ // Move to the next element
+ group.currentOptionElement = optionElement.nextSibling;
+
+ } else {
+
+ // This option is not in a group
+ optionElement = addOrReuseElement(selectElement[0],
+ currentElement,
+ 'option',
+ optionTemplate);
+ updateOptionElement(option, optionElement);
+ // Move to the next element
+ currentElement = optionElement.nextSibling;
+ }
+ });
+
+
+ // Now remove all excess options and group
+ Object.keys(groupMap).forEach(function(key) {
+ removeExcessElements(groupMap[key].currentOptionElement);
+ });
+ removeExcessElements(currentElement);
+
+ ngModelCtrl.$render();
+
+ // Check to see if the value has changed due to the update to the options
+ if (!ngModelCtrl.$isEmpty(previousValue)) {
+ var nextValue = selectCtrl.readValue();
+ 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
+ }
+ };
+}];
+
/**
* @ngdoc directive
* @name ngPluralize
@@ -24141,6 +27378,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
* <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
* for <span ng-non-bindable>{{numberExpression}}</span>.
*
+ * If no rule is defined for a category, then an empty string is displayed and a warning is generated.
+ * Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
+ *
* # Configuring ngPluralize with offset
* The `offset` attribute allows further customization of pluralized text, which can result in
* a better user experience. For example, instead of the message "4 people are viewing this document",
@@ -24187,9 +27427,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
}]);
</script>
<div ng-controller="ExampleController">
- Person 1:<input type="text" ng-model="person1" value="Igor" /><br/>
- Person 2:<input type="text" ng-model="person2" value="Misko" /><br/>
- Number of People:<input type="text" ng-model="personCount" value="1" /><br/>
+ <label>Person 1:<input type="text" ng-model="person1" value="Igor" /></label><br/>
+ <label>Person 2:<input type="text" ng-model="person2" value="Misko" /></label><br/>
+ <label>Number of People:<input type="text" ng-model="personCount" value="1" /></label><br/>
<!--- Example with simple pluralization rules for en locale --->
Without Offset:
@@ -24259,12 +27499,11 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
</file>
</example>
*/
-var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
+var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) {
var BRACE = /{}/g,
IS_WHEN = /^when(Minus)?(.+)$/;
return {
- restrict: 'EA',
link: function(scope, element, attr) {
var numberExp = attr.count,
whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs
@@ -24301,9 +27540,18 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
// 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.
- if ((count !== lastCount) && !(countIsNaN && isNaN(lastCount))) {
+ if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
watchRemover();
- watchRemover = scope.$watch(whensExpFns[count], updateElementText);
+ var whenExpFn = whensExpFns[count];
+ if (isUndefined(whenExpFn)) {
+ if (newVal != null) {
+ $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
+ }
+ watchRemover = noop;
+ updateElementText();
+ } else {
+ watchRemover = scope.$watch(whenExpFn, updateElementText);
+ }
lastCount = count;
}
});
@@ -24318,6 +27566,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
/**
* @ngdoc directive
* @name ngRepeat
+ * @multiElement
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -24335,8 +27584,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* | `$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
*
@@ -24347,31 +27599,38 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * You need to be aware that the JavaScript specification does not define what order
- * it will return the keys for an object. In order to have a guaranteed deterministic order
- * for the keys, Angular versions up to and including 1.3 **sort the keys alphabetically**.
+ * 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.)
+ *
+ * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
+ * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
+ * keys in the order in which they were defined, although there are exceptions when keys are deleted
+ * and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
* 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
* do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
* or implement a `$watch` on the object yourself.
*
- * In version 1.4 we will remove the sorting, since 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.
- *
*
* # 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.
@@ -24384,7 +27643,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* </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)">
@@ -24392,10 +27651,14 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* </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 signifincantly 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}}
@@ -24410,6 +27673,15 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* </div>
* ```
*
+ * <div class="alert alert-warning">
+ * **Note:** `track by` must always be the last expression:
+ * </div>
+ * ```
+ * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
+ * {{model.name}}
+ * </div>
+ * ```
+ *
* # Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
@@ -24481,8 +27753,9 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* which can be used to associate the objects in the collection with the DOM elements. If no tracking expression
* is specified, ng-repeat associates elements by identity. It is an error to have
* more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
- * mapped to the same DOM element, which is not possible.) If filters are used in the expression, they should be
- * applied before the tracking expression.
+ * mapped to the same DOM element, which is not possible.)
+ *
+ * Note that the tracking expression must come last, after any filters, and the alias expression.
*
* For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
* will be associated by item identity in the array.
@@ -24506,6 +27779,11 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
* the items have been processed through the filter.
*
+ * Please note that `as [variable name] is not an operator but rather a part of ngRepeat micro-syntax so it can be used only at the end
+ * (and not as operator, inside an expression).
+ *
+ * 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:
@@ -24524,7 +27802,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
{name:'Samantha', age:60, gender:'girl'}
]">
I have {{friends.length}} friends. They are:
- <input type="search" ng-model="q" placeholder="filter friends..." />
+ <input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
@@ -24553,7 +27831,6 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
@@ -24722,14 +27999,13 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
} else {
trackByIdFn = trackByIdExpFn || trackByIdObjFn;
- // if object, extract keys, sort them and use to determine order of iteration over obj props
+ // 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);
}
}
- collectionKeys.sort();
}
collectionLength = collectionKeys.length;
@@ -24795,7 +28071,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);
@@ -24807,8 +28083,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
@@ -24831,6 +28106,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
/**
* @ngdoc directive
* @name ngShow
+ * @multiElement
*
* @description
* The `ngShow` directive shows or hides the given HTML element based on the expression
@@ -24924,7 +28200,7 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* @example
<example module="ngAnimate" deps="angular-animate.js" animations="true">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked"><br/>
+ Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/>
<div>
Show:
<div class="check-element animate-show" ng-show="checked">
@@ -24950,9 +28226,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;
}
@@ -25006,6 +28280,7 @@ var ngShowDirective = ['$animate', function($animate) {
/**
* @ngdoc directive
* @name ngHide
+ * @multiElement
*
* @description
* The `ngHide` directive shows or hides the given HTML element based on the expression
@@ -25089,7 +28364,7 @@ var ngShowDirective = ['$animate', function($animate) {
* @example
<example module="ngAnimate" deps="angular-animate.js" animations="true">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked"><br/>
+ Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/>
<div>
Show:
<div class="check-element animate-hide" ng-show="checked">
@@ -25108,7 +28383,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;
@@ -25307,7 +28581,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;
@@ -25346,7 +28619,6 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
*/
var ngSwitchDirective = ['$animate', function($animate) {
return {
- restrict: 'EA',
require: 'ngSwitch',
// asks for $scope to fool the BC controller module
@@ -25455,8 +28727,8 @@ var ngSwitchDefaultDirective = ngDirective({
}]);
</script>
<div ng-controller="ExampleController">
- <input ng-model="title"> <br/>
- <textarea ng-model="text"></textarea> <br/>
+ <input ng-model="title" aria-label="title"> <br/>
+ <textarea ng-model="text" aria-label="text"></textarea> <br/>
<pane title="{{title}}">{{text}}</pane>
</div>
</file>
@@ -25541,7 +28813,153 @@ var scriptDirective = ['$templateCache', function($templateCache) {
};
}];
-var ngOptionsMinErr = minErr('ngOptions');
+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
+ * @description
+ * The controller for the `<select>` directive. This provides support for reading
+ * and writing the selected value(s) of the control and also coordinates dynamically
+ * added `<option>` elements, perhaps by an `ngRepeat` directive.
+ */
+var SelectController =
+ ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+
+ var self = this,
+ optionsMap = new HashMap();
+
+ // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
+ self.ngModelCtrl = noopNgModelController;
+
+ // The "unknown" option is one that is prepended to the list if the viewValue
+ // does not match any of the options. When it is rendered the value of the unknown
+ // option is '? XXX ?' where XXX is the hashKey of the value that is not known.
+ //
+ // We can't just jqLite('<option>') since jqLite is not smart enough
+ // to create it in <select> and IE barfs otherwise.
+ self.unknownOption = jqLite(document.createElement('option'));
+ self.renderUnknownOption = function(val) {
+ var unknownVal = '? ' + hashKey(val) + ' ?';
+ self.unknownOption.val(unknownVal);
+ $element.prepend(self.unknownOption);
+ $element.val(unknownVal);
+ };
+
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
+
+ self.removeUnknownOption = function() {
+ if (self.unknownOption.parent()) self.unknownOption.remove();
+ };
+
+
+ // Read the value of the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.readValue = function readSingleValue() {
+ self.removeUnknownOption();
+ return $element.val();
+ };
+
+
+ // Write the value to the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.writeValue = function writeSingleValue(value) {
+ if (self.hasOption(value)) {
+ self.removeUnknownOption();
+ $element.val(value);
+ if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
+ } else {
+ if (value == null && self.emptyOption) {
+ self.removeUnknownOption();
+ $element.val('');
+ } else {
+ self.renderUnknownOption(value);
+ }
+ }
+ };
+
+
+ // 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
+ self.removeOption = function(value) {
+ var count = optionsMap.get(value);
+ if (count) {
+ if (count === 1) {
+ optionsMap.remove(value);
+ if (value === '') {
+ self.emptyOption = undefined;
+ }
+ } else {
+ optionsMap.put(value, count - 1);
+ }
+ }
+ };
+
+ // Check whether the select control has an option matching the given value
+ 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();
+ });
+ };
+}];
+
/**
* @ngdoc directive
* @name select
@@ -25550,767 +28968,329 @@ var ngOptionsMinErr = minErr('ngOptions');
* @description
* HTML `SELECT` element with angular data-binding.
*
- * # `ngOptions`
+ * 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.
*
- * The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
- * elements for the `<select>` element using the array or object obtained by evaluating the
- * `ngOptions` comprehension expression.
+ * 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.
*
- * In many cases, `ngRepeat` can be used on `<option>` elements instead of `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. `ngOptions` should be used when the `<select>` model needs to be bound
- * to a non-string value. This is because an option element can only be bound to string values at
- * present.
+ * <div class="alert alert-warning">
+ * Note that the value of a `select` directive used without `ngOptions` is always a string.
+ * When the model needs to be bound to a non-string value, you must either explictly convert it
+ * 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>
*
- * 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.
+ * 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-warning">
- * **Note:** `ngModel` compares by reference, not value. This is important when binding to an
- * array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
+ * <div class="alert alert-info">
+ * 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>
*
- * ## `select` **`as`**
- *
- * Using `select` **`as`** will bind the result of the `select` expression to the model, but
- * the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
- * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
- * is used, the result of that expression will be set as the value of the `option` and `select` elements.
*
+ * @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`}.
*
- * ### `select` **`as`** and **`track by`**
+ * @example
+ * ### Simple `select` elements with static options
*
- * <div class="alert alert-warning">
- * Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
+ * <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',
+ * };
*
- * Consider the following example:
- *
- * ```html
- * <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
- * ```
- *
- * ```js
- * $scope.values = [{
- * id: 1,
- * label: 'aLabel',
- * subItem: { name: 'aSubItem' }
- * }, {
- * id: 2,
- * label: 'bLabel',
- * subItem: { name: 'bSubItem' }
- * }];
- *
- * $scope.selected = { name: 'aSubItem' };
- * ```
+ * $scope.forceUnknownOption = function() {
+ * $scope.data.singleSelect = 'nonsense';
+ * };
+ * }]);
+ * </file>
+ *</example>
*
- * 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:
+ * ### 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>
*
- * 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.
*
+ * ### Using `select` with `ngOptions` and setting a default value
+ * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
*
- * @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=} required The control is considered valid only if value is 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 {comprehension_expression=} ngOptions in one of the following forms:
+ * <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>
*
- * * for array data sources:
- * * `label` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
- * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
- * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
- * (for including a filter with `track by`)
- * * for object data sources:
- * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`group by`** `group`
- * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
*
- * Where:
+ * ### Binding `select` to a non-string value via `ngModel` parsing / formatting
*
- * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
- * * `value`: local variable which will refer to each item in the `array` or each property value
- * of `object` during iteration.
- * * `key`: local variable which will refer to a property name in `object` during iteration.
- * * `label`: The result of this expression will be the label for `<option>` element. The
- * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
- * * `select`: The result of this expression will be bound to the model of the parent `<select>`
- * element. If not specified, `select` expression will default to `value`.
- * * `group`: The result of this expression will be used to group options using the `<optgroup>`
- * DOM element.
- * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
- * used to identify the objects in the array. The `trackexpr` will most likely refer to the
- * `value` variable (e.g. `value.propertyName`). With this the selection is preserved
- * even when the options are recreated (e.g. reloaded from the server).
+ * <example name="select-with-non-string-options" module="nonStringSelect">
+ * <file name="index.html">
+ * <select ng-model="model.id" convert-to-number>
+ * <option value="0">Zero</option>
+ * <option value="1">One</option>
+ * <option value="2">Two</option>
+ * </select>
+ * {{ model }}
+ * </file>
+ * <file name="app.js">
+ * angular.module('nonStringSelect', [])
+ * .run(function($rootScope) {
+ * $rootScope.model = { id: 2 };
+ * })
+ * .directive('convertToNumber', function() {
+ * return {
+ * require: 'ngModel',
+ * link: function(scope, element, attrs, ngModel) {
+ * ngModel.$parsers.push(function(val) {
+ * return parseInt(val, 10);
+ * });
+ * ngModel.$formatters.push(function(val) {
+ * return '' + val;
+ * });
+ * }
+ * };
+ * });
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should initialize to model', function() {
+ * var select = element(by.css('select'));
+ * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
+ * });
+ * </file>
+ * </example>
*
- * @example
- <example module="selectExample">
- <file name="index.html">
- <script>
- angular.module('selectExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.colors = [
- {name:'black', shade:'dark'},
- {name:'white', shade:'light'},
- {name:'red', shade:'dark'},
- {name:'blue', shade:'dark'},
- {name:'yellow', shade:'light'}
- ];
- $scope.myColor = $scope.colors[2]; // red
- }]);
- </script>
- <div ng-controller="ExampleController">
- <ul>
- <li ng-repeat="color in colors">
- Name: <input ng-model="color.name">
- [<a href ng-click="colors.splice($index, 1)">X</a>]
- </li>
- <li>
- [<a href ng-click="colors.push({})">add</a>]
- </li>
- </ul>
- <hr/>
- Color (null not allowed):
- <select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
-
- Color (null allowed):
- <span class="nullable">
- <select ng-model="myColor" ng-options="color.name for color in colors">
- <option value="">-- choose color --</option>
- </select>
- </span><br/>
-
- Color grouped by shade:
- <select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
- </select><br/>
-
-
- Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
- <hr/>
- Currently selected: {{ {selected_color:myColor} }}
- <div style="border:solid 1px black; height:20px"
- ng-style="{'background-color':myColor.name}">
- </div>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should check ng-options', function() {
- expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
- element.all(by.model('myColor')).first().click();
- element.all(by.css('select[ng-model="myColor"] option')).first().click();
- expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
- element(by.css('.nullable select[ng-model="myColor"]')).click();
- element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
- expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
- });
- </file>
- </example>
*/
-
-var ngOptionsDirective = valueFn({
- restrict: 'A',
- terminal: true
-});
-
-// jshint maxlen: false
-var selectDirective = ['$compile', '$parse', function($compile, $parse) {
- //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
- var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
- nullModelCtrl = {$setViewValue: noop};
-// jshint maxlen: 100
+var selectDirective = function() {
return {
restrict: 'E',
require: ['select', '?ngModel'],
- controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
- var self = this,
- optionsMap = {},
- ngModelCtrl = nullModelCtrl,
- nullOption,
- unknownOption;
-
-
- self.databound = $attrs.ngModel;
-
-
- self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
- ngModelCtrl = ngModelCtrl_;
- nullOption = nullOption_;
- unknownOption = unknownOption_;
- };
-
-
- self.addOption = function(value, element) {
- assertNotHasOwnProperty(value, '"option value"');
- optionsMap[value] = true;
-
- if (ngModelCtrl.$viewValue == value) {
- $element.val(value);
- if (unknownOption.parent()) unknownOption.remove();
- }
- // 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 (element && element[0].hasAttribute('selected')) {
- element[0].selected = true;
- }
- };
-
-
- self.removeOption = function(value) {
- if (this.hasOption(value)) {
- delete optionsMap[value];
- if (ngModelCtrl.$viewValue === value) {
- this.renderUnknownOption(value);
- }
- }
- };
-
-
- self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
- unknownOption.val(unknownVal);
- $element.prepend(unknownOption);
- $element.val(unknownVal);
- unknownOption.prop('selected', true); // needed for IE
- };
-
-
- self.hasOption = function(value) {
- return optionsMap.hasOwnProperty(value);
- };
+ controller: SelectController,
+ priority: 1,
+ link: {
+ pre: selectPreLink,
+ post: selectPostLink
+ }
+ };
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
- });
- }],
+ function selectPreLink(scope, element, attr, ctrls) {
- link: function(scope, element, attr, ctrls) {
// if ngModel is not defined, we don't need to do anything
- if (!ctrls[1]) return;
-
- var selectCtrl = ctrls[0],
- ngModelCtrl = ctrls[1],
- multiple = attr.multiple,
- optionsExp = attr.ngOptions,
- nullOption = false, // if false, user will not be able to select it (used by ngOptions)
- emptyOption,
- renderScheduled = false,
- // we can't just jqLite('<option>') since jqLite is not smart enough
- // to create it in <select> and IE barfs otherwise.
- optionTemplate = jqLite(document.createElement('option')),
- optGroupTemplate =jqLite(document.createElement('optgroup')),
- unknownOption = optionTemplate.clone();
-
- // find "null" option
- for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
- if (children[i].value === '') {
- emptyOption = nullOption = children.eq(i);
- break;
- }
- }
+ var ngModelCtrl = ctrls[1];
+ if (!ngModelCtrl) return;
- selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
-
- // required validator
- if (multiple) {
- ngModelCtrl.$isEmpty = function(value) {
- return !value || value.length === 0;
- };
- }
-
- if (optionsExp) setupAsOptions(scope, element, ngModelCtrl);
- else if (multiple) setupAsMultiple(scope, element, ngModelCtrl);
- else setupAsSingle(scope, element, ngModelCtrl, selectCtrl);
+ var selectCtrl = ctrls[0];
+ selectCtrl.ngModelCtrl = ngModelCtrl;
- ////////////////////////////
-
-
-
- function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) {
- ngModelCtrl.$render = function() {
- var viewValue = 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`
+ element.on('change', function() {
+ scope.$apply(function() {
+ ngModelCtrl.$setViewValue(selectCtrl.readValue());
+ });
+ });
- if (selectCtrl.hasOption(viewValue)) {
- if (unknownOption.parent()) unknownOption.remove();
- selectElement.val(viewValue);
- if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
- } else {
- if (viewValue == null && emptyOption) {
- selectElement.val('');
- } else {
- selectCtrl.renderUnknownOption(viewValue);
+ // If the select allows multiple values then we need to modify how we read and write
+ // values from and to the control; also what it means for the value to be empty and
+ // we have to add an extra watch since ngModel doesn't work well with arrays - it
+ // doesn't trigger rendering if only an item in the array changes.
+ if (attr.multiple) {
+
+ // Read value now needs to check each option to see if it is selected
+ selectCtrl.readValue = function readMultipleValue() {
+ var array = [];
+ forEach(element.find('option'), function(option) {
+ if (option.selected) {
+ array.push(option.value);
}
- }
- };
-
- selectElement.on('change', function() {
- scope.$apply(function() {
- if (unknownOption.parent()) unknownOption.remove();
- ngModelCtrl.$setViewValue(selectElement.val());
});
- });
- }
+ return array;
+ };
- function setupAsMultiple(scope, selectElement, ctrl) {
- var lastView;
- ctrl.$render = function() {
- var items = new HashMap(ctrl.$viewValue);
- forEach(selectElement.find('option'), function(option) {
+ // Write value now needs to set the selected property of each matching option
+ selectCtrl.writeValue = function writeMultipleValue(value) {
+ var items = new HashMap(value);
+ forEach(element.find('option'), function(option) {
option.selected = isDefined(items.get(option.value));
});
};
// we have to do it on each watch since ngModel watches reference, but
// we need to work of an array, so we need to see if anything was inserted/removed
+ var lastView, lastViewRef = NaN;
scope.$watch(function selectMultipleWatch() {
- if (!equals(lastView, ctrl.$viewValue)) {
- lastView = shallowCopy(ctrl.$viewValue);
- ctrl.$render();
+ if (lastViewRef === ngModelCtrl.$viewValue && !equals(lastView, ngModelCtrl.$viewValue)) {
+ lastView = shallowCopy(ngModelCtrl.$viewValue);
+ ngModelCtrl.$render();
}
+ lastViewRef = ngModelCtrl.$viewValue;
});
- selectElement.on('change', function() {
- scope.$apply(function() {
- var array = [];
- forEach(selectElement.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
- }
- });
- ctrl.$setViewValue(array);
- });
- });
- }
-
- function setupAsOptions(scope, selectElement, ctrl) {
- var match;
-
- if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) {
- throw ngOptionsMinErr('iexp',
- "Expected expression in form of " +
- "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '{0}'. Element: {1}",
- optionsExp, startingTag(selectElement));
- }
-
- var displayFn = $parse(match[2] || match[1]),
- valueName = match[4] || match[6],
- selectAs = / as /.test(match[0]) && match[1],
- selectAsFn = selectAs ? $parse(selectAs) : null,
- keyName = match[5],
- groupByFn = $parse(match[3] || ''),
- valueFn = $parse(match[2] ? match[1] : valueName),
- valuesFn = $parse(match[7]),
- track = match[8],
- trackFn = track ? $parse(match[8]) : null,
- trackKeysCache = {},
- // This is an array of array of existing option groups in DOM.
- // We try to reuse these if possible
- // - optionGroupsCache[0] is the options with no option group
- // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
- optionGroupsCache = [[{element: selectElement, label:''}]],
- //re-usable object to represent option's locals
- locals = {};
-
- if (nullOption) {
- // compile the element since there might be bindings in it
- $compile(nullOption)(scope);
-
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- nullOption.removeClass('ng-scope');
-
- // we need to remove it before calling selectElement.empty() because otherwise IE will
- // remove the label from the element. wtf?
- nullOption.remove();
- }
-
- // clear contents, we'll add what's needed based on the model
- selectElement.empty();
-
- selectElement.on('change', selectionChanged);
-
- ctrl.$render = render;
-
- scope.$watchCollection(valuesFn, scheduleRendering);
- scope.$watchCollection(getLabels, scheduleRendering);
-
- if (multiple) {
- scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering);
- }
-
- // ------------------------------------------------------------------ //
-
- function callExpression(exprFn, key, value) {
- locals[valueName] = value;
- if (keyName) locals[keyName] = key;
- return exprFn(scope, locals);
- }
-
- function selectionChanged() {
- scope.$apply(function() {
- var collection = valuesFn(scope) || [];
- var viewValue;
- if (multiple) {
- viewValue = [];
- forEach(selectElement.val(), function(selectedKey) {
- selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey;
- viewValue.push(getViewValue(selectedKey, collection[selectedKey]));
- });
- } else {
- var selectedKey = trackFn ? trackKeysCache[selectElement.val()] : selectElement.val();
- viewValue = getViewValue(selectedKey, collection[selectedKey]);
- }
- ctrl.$setViewValue(viewValue);
- render();
- });
- }
-
- function getViewValue(key, value) {
- if (key === '?') {
- return undefined;
- } else if (key === '') {
- return null;
- } else {
- var viewValueFn = selectAsFn ? selectAsFn : valueFn;
- return callExpression(viewValueFn, key, value);
- }
- }
-
- function getLabels() {
- var values = valuesFn(scope);
- var toDisplay;
- if (values && isArray(values)) {
- toDisplay = new Array(values.length);
- for (var i = 0, ii = values.length; i < ii; i++) {
- toDisplay[i] = callExpression(displayFn, i, values[i]);
- }
- return toDisplay;
- } else if (values) {
- // TODO: Add a test for this case
- toDisplay = {};
- for (var prop in values) {
- if (values.hasOwnProperty(prop)) {
- toDisplay[prop] = callExpression(displayFn, prop, values[prop]);
- }
- }
- }
- return toDisplay;
- }
-
- function createIsSelectedFn(viewValue) {
- var selectedSet;
- if (multiple) {
- if (trackFn && isArray(viewValue)) {
-
- selectedSet = new HashMap([]);
- for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) {
- // tracking by key
- selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true);
- }
- } else {
- selectedSet = new HashMap(viewValue);
- }
- } else if (trackFn) {
- viewValue = callExpression(trackFn, null, viewValue);
- }
-
- return function isSelected(key, value) {
- var compareValueFn;
- if (trackFn) {
- compareValueFn = trackFn;
- } else if (selectAsFn) {
- compareValueFn = selectAsFn;
- } else {
- compareValueFn = valueFn;
- }
-
- if (multiple) {
- return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value)));
- } else {
- return viewValue === callExpression(compareValueFn, key, value);
- }
- };
- }
-
- function scheduleRendering() {
- if (!renderScheduled) {
- scope.$$postDigest(render);
- renderScheduled = true;
- }
- }
-
- /**
- * A new labelMap is created with each render.
- * This function is called for each existing option with added=false,
- * and each new option with added=true.
- * - Labels that are passed to this method twice,
- * (once with added=true and once with added=false) will end up with a value of 0, and
- * will cause no change to happen to the corresponding option.
- * - Labels that are passed to this method only once with added=false will end up with a
- * value of -1 and will eventually be passed to selectCtrl.removeOption()
- * - Labels that are passed to this method only once with added=true will end up with a
- * value of 1 and will eventually be passed to selectCtrl.addOption()
- */
- function updateLabelMap(labelMap, label, added) {
- labelMap[label] = labelMap[label] || 0;
- labelMap[label] += (added ? 1 : -1);
- }
-
- function render() {
- renderScheduled = false;
-
- // Temporary location for the option groups before we render them
- var optionGroups = {'':[]},
- optionGroupNames = [''],
- optionGroupName,
- optionGroup,
- option,
- existingParent, existingOptions, existingOption,
- viewValue = ctrl.$viewValue,
- values = valuesFn(scope) || [],
- keys = keyName ? sortedKeys(values) : values,
- key,
- value,
- groupLength, length,
- groupIndex, index,
- labelMap = {},
- selected,
- isSelected = createIsSelectedFn(viewValue),
- anySelected = false,
- lastElement,
- element,
- label,
- optionId;
-
- trackKeysCache = {};
-
- // We now build up the list of options we need (we merge later)
- for (index = 0; length = keys.length, index < length; index++) {
- key = index;
- if (keyName) {
- key = keys[index];
- if (key.charAt(0) === '$') continue;
- }
- value = values[key];
-
- optionGroupName = callExpression(groupByFn, key, value) || '';
- if (!(optionGroup = optionGroups[optionGroupName])) {
- optionGroup = optionGroups[optionGroupName] = [];
- optionGroupNames.push(optionGroupName);
- }
-
- selected = isSelected(key, value);
- anySelected = anySelected || selected;
-
- label = callExpression(displayFn, key, value); // what will be seen by the user
-
- // doing displayFn(scope, locals) || '' overwrites zero values
- label = isDefined(label) ? label : '';
- optionId = trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index);
- if (trackFn) {
- trackKeysCache[optionId] = key;
- }
-
- optionGroup.push({
- // either the index into array or key from object
- id: optionId,
- label: label,
- selected: selected // determine if we should be selected
- });
- }
- if (!multiple) {
- if (nullOption || viewValue === null) {
- // insert null option if we have a placeholder, or the model is null
- optionGroups[''].unshift({id:'', label:'', selected:!anySelected});
- } else if (!anySelected) {
- // option could not be found, we have to insert the undefined item
- optionGroups[''].unshift({id:'?', label:'', selected:true});
- }
- }
-
- // Now we need to update the list of DOM nodes to match the optionGroups we computed above
- for (groupIndex = 0, groupLength = optionGroupNames.length;
- groupIndex < groupLength;
- groupIndex++) {
- // current option group name or '' if no group
- optionGroupName = optionGroupNames[groupIndex];
-
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroups[optionGroupName];
-
- if (optionGroupsCache.length <= groupIndex) {
- // we need to grow the optionGroups
- existingParent = {
- element: optGroupTemplate.clone().attr('label', optionGroupName),
- label: optionGroup.label
- };
- existingOptions = [existingParent];
- optionGroupsCache.push(existingOptions);
- selectElement.append(existingParent.element);
- } else {
- existingOptions = optionGroupsCache[groupIndex];
- existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
-
- // update the OPTGROUP label if not the same.
- if (existingParent.label != optionGroupName) {
- existingParent.element.attr('label', existingParent.label = optionGroupName);
- }
- }
-
- lastElement = null; // start at the beginning
- for (index = 0, length = optionGroup.length; index < length; index++) {
- option = optionGroup[index];
- if ((existingOption = existingOptions[index + 1])) {
- // reuse elements
- lastElement = existingOption.element;
- if (existingOption.label !== option.label) {
- updateLabelMap(labelMap, existingOption.label, false);
- updateLabelMap(labelMap, option.label, true);
- lastElement.text(existingOption.label = option.label);
- lastElement.prop('label', existingOption.label);
- }
- if (existingOption.id !== option.id) {
- lastElement.val(existingOption.id = option.id);
- }
- // lastElement.prop('selected') provided by jQuery has side-effects
- if (lastElement[0].selected !== option.selected) {
- lastElement.prop('selected', (existingOption.selected = option.selected));
- if (msie) {
- // See #7692
- // The selected item wouldn't visually update on IE without this.
- // Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well
- lastElement.prop('selected', existingOption.selected);
- }
- }
- } else {
- // grow elements
-
- // if it's a null option
- if (option.id === '' && nullOption) {
- // put back the pre-compiled element
- element = nullOption;
- } else {
- // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
- // in this version of jQuery on some browser the .text() returns a string
- // rather then the element.
- (element = optionTemplate.clone())
- .val(option.id)
- .prop('selected', option.selected)
- .attr('selected', option.selected)
- .prop('label', option.label)
- .text(option.label);
- }
+ // If we are a multiple select then value is now a collection
+ // so the meaning of $isEmpty changes
+ ngModelCtrl.$isEmpty = function(value) {
+ return !value || value.length === 0;
+ };
- existingOptions.push(existingOption = {
- element: element,
- label: option.label,
- id: option.id,
- selected: option.selected
- });
- updateLabelMap(labelMap, option.label, true);
- if (lastElement) {
- lastElement.after(element);
- } else {
- existingParent.element.append(element);
- }
- lastElement = element;
- }
- }
- // remove any excessive OPTIONs in a group
- index++; // increment since the existingOptions[0] is parent element not OPTION
- while (existingOptions.length > index) {
- option = existingOptions.pop();
- updateLabelMap(labelMap, option.label, false);
- option.element.remove();
- }
- }
- // remove any excessive OPTGROUPs from select
- while (optionGroupsCache.length > groupIndex) {
- // remove all the labels in the option group
- optionGroup = optionGroupsCache.pop();
- for (index = 1; index < optionGroup.length; ++index) {
- updateLabelMap(labelMap, optionGroup[index].label, false);
- }
- optionGroup[0].element.remove();
- }
- forEach(labelMap, function(count, label) {
- if (count > 0) {
- selectCtrl.addOption(label);
- } else if (count < 0) {
- selectCtrl.removeOption(label);
- }
- });
- }
}
}
- };
-}];
-var optionDirective = ['$interpolate', function($interpolate) {
- var nullSelectCtrl = {
- addOption: noop,
- removeOption: noop
- };
+ 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);
+ };
+ }
+};
+
+// The option directive is purely designed to communicate the existence (or lack of)
+// of dynamically created (and destroyed) option elements to their containing select
+// directive via its controller.
+var optionDirective = ['$interpolate', function($interpolate) {
return {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
- 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',
parent = element.parent(),
selectCtrl = parent.data(selectCtrlName) ||
parent.parent().data(selectCtrlName); // in case we are in optgroup
- if (!selectCtrl || !selectCtrl.databound) {
- selectCtrl = nullSelectCtrl;
+ if (selectCtrl) {
+ selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn);
}
-
- if (interpolateFn) {
- scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
- attr.$set('value', newVal);
- if (oldVal !== newVal) {
- selectCtrl.removeOption(oldVal);
- }
- selectCtrl.addOption(newVal, element);
- });
- } else {
- selectCtrl.addOption(attr.value, element);
- }
-
- element.on('$destroy', function() {
- selectCtrl.removeOption(attr.value);
- });
};
}
};
@@ -26321,6 +29301,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',
@@ -26340,7 +29378,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',
@@ -26364,14 +29476,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',
@@ -26381,7 +29559,7 @@ var maxlengthDirective = function() {
var maxlength = -1;
attr.$observe('maxlength', function(value) {
- var intVal = int(value);
+ var intVal = toInt(value);
maxlength = isNaN(intVal) ? -1 : intVal;
ctrl.$validate();
});
@@ -26392,6 +29570,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',
@@ -26401,7 +29643,7 @@ var minlengthDirective = function() {
var minlength = 0;
attr.$observe('minlength', function(value) {
- minlength = int(value) || 0;
+ minlength = toInt(value) || 0;
ctrl.$validate();
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
@@ -26411,17 +29653,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();
+//try to bind to jquery now so that one can write jqLite(document).ready()
+//but we will rebind on bootstrap again.
+bindJQuery();
- publishExternalAPI(angular);
+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);
+ }
+
+ var base = Math.pow(10, v);
+ var f = ((n * base) | 0) % base;
+ return {v: v, f: f};
+}
+
+$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);
@@ -26429,4 +29816,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;}</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