diff options
author | st398c <st398c@research.att.com> | 2019-07-10 14:14:39 -0400 |
---|---|---|
committer | st398c <st398c@research.att.com> | 2019-07-10 14:29:42 -0400 |
commit | 7d7b74d4d92e7fa45471fb0e8678837f71da3d02 (patch) | |
tree | be75fe5dc393aebb2a0da7591d684e746212c98b /ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration | |
parent | 5837a6d32c065cc80a95a4bee6b71008304ef312 (diff) |
Blackduck scan fixes
Issue-ID: PORTAL-631
Change-Id: I240f3656de2de2d3540f0fc1d615c461beab14c5
Signed-off-by: st398c <st398c@research.att.com>
Diffstat (limited to 'ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration')
-rw-r--r-- | ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js | 11827 |
1 files changed, 7886 insertions, 3941 deletions
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js index 9e6a4f4f..db30da8f 100644 --- a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js +++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js @@ -1,9 +1,9 @@ /** - * @license AngularJS v1.5.0-beta.2 - * (c) 2010-2015 Google, Inc. http://angularjs.org + * @license AngularJS v1.6.3 + * (c) 2010-2017 Google, Inc. http://angularjs.org * License: MIT */ -(function(window, document, undefined) {'use strict'; +(function(window) {'use strict'; /** * @description @@ -38,131 +38,134 @@ function minErr(module, ErrorConstructor) { ErrorConstructor = ErrorConstructor || Error; return function() { - var SKIP_INDEXES = 2; - - var templateArgs = arguments, - code = templateArgs[0], + var code = arguments[0], + template = arguments[1], message = '[' + (module ? module + ':' : '') + code + '] ', - template = templateArgs[1], + templateArgs = sliceArgs(arguments, 2).map(function(arg) { + return toDebugString(arg, minErrConfig.objectMaxDepth); + }), paramPrefix, i; message += template.replace(/\{\d+\}/g, function(match) { - var index = +match.slice(1, -1), - shiftedIndex = index + SKIP_INDEXES; + var index = +match.slice(1, -1); - if (shiftedIndex < templateArgs.length) { - return toDebugString(templateArgs[shiftedIndex]); + if (index < templateArgs.length) { + return templateArgs[index]; } return match; }); - message += '\nhttp://errors.angularjs.org/1.5.0-beta.2/' + + message += '\nhttp://errors.angularjs.org/1.6.3/' + (module ? module + '/' : '') + code; - for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { - message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' + - encodeURIComponent(toDebugString(templateArgs[i])); + for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') { + message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]); } return new ErrorConstructor(message); }; } -/* We need to tell jshint what variables are being exported */ -/* global angular: true, - msie: true, - jqLite: true, - jQuery: true, - slice: true, - splice: true, - push: true, - toString: true, - ngMinErr: true, - angularModule: true, - uid: true, - REGEX_STRING_REGEXP: true, - VALIDITY_STATE_PROPERTY: true, - - lowercase: true, - uppercase: true, - manualLowercase: true, - manualUppercase: true, - nodeName_: true, - isArrayLike: true, - forEach: true, - forEachSorted: true, - reverseParams: true, - nextUid: true, - setHashKey: true, - extend: 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, - isArray: true, - isFunction: true, - isRegExp: true, - isWindow: true, - isScope: true, - isFile: true, - isFormData: true, - isBlob: true, - isBoolean: true, - isPromiseLike: true, - trim: true, - escapeForRegexp: true, - isElement: true, - makeMap: true, - includes: true, - arrayRemove: true, - copy: true, - 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, - toKeyValue: true, - encodeUriSegment: true, - encodeUriQuery: true, - angularInit: true, - bootstrap: true, - getTestability: true, - snake_case: true, - bindJQuery: true, - assertArg: true, - assertArgFn: true, - assertNotHasOwnProperty: true, - getter: true, - getBlockNodes: true, - hasOwnProperty: true, - createMap: true, - - NODE_TYPE_ELEMENT: true, - NODE_TYPE_ATTRIBUTE: true, - NODE_TYPE_TEXT: true, - NODE_TYPE_COMMENT: true, - NODE_TYPE_DOCUMENT: true, - NODE_TYPE_DOCUMENT_FRAGMENT: true, +/* We need to tell ESLint what variables are being exported */ +/* exported + angular, + msie, + jqLite, + jQuery, + slice, + splice, + push, + toString, + minErrConfig, + errorHandlingConfig, + isValidObjectMaxDepth, + ngMinErr, + angularModule, + uid, + REGEX_STRING_REGEXP, + VALIDITY_STATE_PROPERTY, + + lowercase, + uppercase, + manualLowercase, + manualUppercase, + nodeName_, + isArrayLike, + forEach, + forEachSorted, + reverseParams, + nextUid, + setHashKey, + extend, + toInt, + inherit, + merge, + noop, + identity, + valueFn, + isUndefined, + isDefined, + isObject, + isBlankObject, + isString, + isNumber, + isNumberNaN, + isDate, + isArray, + isFunction, + isRegExp, + isWindow, + isScope, + isFile, + isFormData, + isBlob, + isBoolean, + isPromiseLike, + trim, + escapeForRegexp, + isElement, + makeMap, + includes, + arrayRemove, + copy, + equals, + csp, + jq, + concat, + sliceArgs, + bind, + toJsonReplacer, + toJson, + fromJson, + convertTimezoneToLocal, + timezoneToOffset, + startingTag, + tryDecodeURIComponent, + parseKeyValue, + toKeyValue, + encodeUriSegment, + encodeUriQuery, + angularInit, + bootstrap, + getTestability, + snake_case, + bindJQuery, + assertArg, + assertArgFn, + assertNotHasOwnProperty, + getter, + getBlockNodes, + hasOwnProperty, + createMap, + stringify, + + NODE_TYPE_ELEMENT, + NODE_TYPE_ATTRIBUTE, + NODE_TYPE_TEXT, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT */ //////////////////////////////////// @@ -171,6 +174,7 @@ function minErr(module, ErrorConstructor) { * @ngdoc module * @name ng * @module ng + * @installation * @description * * # ng (core module) @@ -188,18 +192,69 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/; // This is used so that it's possible for internal tests to create mock ValidityStates. var VALIDITY_STATE_PROPERTY = 'validity'; + +var hasOwnProperty = Object.prototype.hasOwnProperty; + +var minErrConfig = { + objectMaxDepth: 5 +}; + + /** + * @ngdoc function + * @name angular.errorHandlingConfig + * @module ng + * @kind function + * + * @description + * Configure several aspects of error handling in AngularJS if used as a setter or return the + * current configuration if used as a getter. The following options are supported: + * + * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages. + * + * Omitted or undefined options will leave the corresponding configuration values unchanged. + * + * @param {Object=} config - The configuration object. May only contain the options that need to be + * updated. Supported keys: + * + * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a + * non-positive or non-numeric value, removes the max depth limit. + * Default: 5 + */ +function errorHandlingConfig(config) { + if (isObject(config)) { + if (isDefined(config.objectMaxDepth)) { + minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN; + } + } else { + return minErrConfig; + } +} + +/** + * @private + * @param {Number} maxDepth + * @return {boolean} + */ +function isValidObjectMaxDepth(maxDepth) { + return isNumber(maxDepth) && maxDepth > 0; +} + /** * @ngdoc function * @name angular.lowercase * @module ng * @kind function * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead. + * * @description Converts the specified string to lowercase. * @param {string} string String to be converted to lowercase. * @returns {string} Lowercased string. */ var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;}; -var hasOwnProperty = Object.prototype.hasOwnProperty; /** * @ngdoc function @@ -207,6 +262,11 @@ var hasOwnProperty = Object.prototype.hasOwnProperty; * @module ng * @kind function * + * @deprecated + * sinceVersion="1.5.0" + * removeVersion="1.7.0" + * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead. + * * @description Converts the specified string to uppercase. * @param {string} string String to be converted to uppercase. * @returns {string} Uppercased string. @@ -215,22 +275,24 @@ var uppercase = function(string) {return isString(string) ? string.toUpperCase() var manualLowercase = function(s) { - /* jshint bitwise: false */ + /* eslint-disable no-bitwise */ return isString(s) ? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) : s; + /* eslint-enable */ }; var manualUppercase = function(s) { - /* jshint bitwise: false */ + /* eslint-disable no-bitwise */ return isString(s) ? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) : s; + /* eslint-enable */ }; // String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish // locale, for this reason we need to detect this case and redefine lowercase/uppercase methods -// with correct but slower alternatives. +// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387 if ('i' !== 'I'.toLowerCase()) { lowercase = manualLowercase; uppercase = manualUppercase; @@ -253,11 +315,12 @@ var angularModule, uid = 0; +// Support: IE 9-11 only /** * documentMode is an IE-only property * http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx */ -msie = document.documentMode; +msie = window.document.documentMode; /** @@ -273,18 +336,19 @@ function isArrayLike(obj) { // arrays, strings and jQuery/jqLite objects are array like // * jqLite is either the jQuery or jqLite constructor function - // * we have to check the existance of jqLite first as this method is called + // * we have to check the existence of jqLite first as this method is called // via the forEach method when constructing the jqLite object in the first place if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true; // 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; + var length = 'length' in Object(obj) && obj.length; // NodeList objects (with `item` method) and // other objects with suitable length characteristics are array-like return isNumber(length) && - (length >= 0 && (length - 1) in obj || typeof obj.item == 'function'); + (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function'); + } /** @@ -304,7 +368,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 @@ -327,9 +391,7 @@ function forEach(obj, iterator, context) { if (obj) { if (isFunction(obj)) { for (key in obj) { - // Need to check if hasOwnProperty exists, - // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function - if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) { + if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) { iterator.call(context, obj[key], key, obj); } } @@ -381,7 +443,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);}; } /** @@ -498,6 +560,11 @@ function toInt(str) { return parseInt(str, 10); } +var isNumberNaN = Number.isNaN || function isNumberNaN(num) { + // eslint-disable-next-line no-self-compare + return num !== num; +}; + function inherit(parent, extra) { return extend(Object.create(parent), extra); @@ -534,18 +601,28 @@ noop.$inject = []; * functional style. * ```js - function transformer(transformationFn, value) { - return (transformationFn || angular.identity)(value); - }; + function transformer(transformationFn, value) { + return (transformationFn || angular.identity)(value); + }; + + // E.g. + function getResult(fn, input) { + return (fn || angular.identity)(input); + }; + + getResult(function(n) { return n * 2; }, 21); // returns 42 + getResult(null, 21); // returns 21 + getResult(undefined, 21); // returns 21 ``` - * @param {*} value to be returned. - * @returns {*} the value passed in. + * + * @param {*} value to be returned. + * @returns {*} the value passed in. */ function identity($) {return $;} identity.$inject = []; -function valueFn(value) {return function() {return value;};} +function valueFn(value) {return function valueRef() {return value;};} function hasCustomToString(obj) { return isFunction(obj.toString) && obj.toString !== toString; @@ -671,7 +748,7 @@ function isDate(value) { * @kind function * * @description - * Determines if a reference is an `Array`. + * Determines if a reference is an `Array`. Alias of Array.isArray. * * @param {*} value Reference to check. * @returns {boolean} True if `value` is an `Array`. @@ -747,11 +824,15 @@ function isPromiseLike(obj) { } -var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/; +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)); } +function isArrayBuffer(obj) { + return toString.call(obj) === '[object ArrayBuffer]'; +} + var trim = function(value) { return isString(value) ? value.trim() : value; @@ -761,8 +842,10 @@ var trim = function(value) { // http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021 // Prereq: s is a string. var escapeForRegexp = function(s) { - return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1'). - replace(/\x08/g, '\\x08'); + return s + .replace(/([-()[\]{}+?*.$^|,:#<!\\])/g, '\\$1') + // eslint-disable-next-line no-control-regex + .replace(/\x08/g, '\\x08'); }; @@ -780,8 +863,8 @@ var escapeForRegexp = function(s) { */ function isElement(node) { return !!(node && - (node.nodeName // we are a direct element - || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API + (node.nodeName // We are a direct element. + || (node.prop && node.attr && node.find))); // We have an on and find method part of jQuery API. } /** @@ -789,7 +872,7 @@ function isElement(node) { * @returns {object} in the form of {key1:true, key2:true, ...} */ function makeMap(str) { - var obj = {}, items = str.split(","), i; + var obj = {}, items = str.split(','), i; for (i = 0; i < items.length; i++) { obj[items[i]] = true; } @@ -802,7 +885,7 @@ function nodeName_(element) { } function includes(array, obj) { - return Array.prototype.indexOf.call(array, obj) != -1; + return Array.prototype.indexOf.call(array, obj) !== -1; } function arrayRemove(array, value) { @@ -826,7 +909,13 @@ function arrayRemove(array, value) { * * If a destination is provided, all of its elements (for arrays) or properties (for objects) * are deleted and then all elements/properties from the source are copied to it. * * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned. - * * If `source` is identical to 'destination' an exception will be thrown. + * * If `source` is identical to `destination` an exception will be thrown. + * + * <br /> + * <div class="alert alert-warning"> + * Only enumerable properties are taken into account. Non-enumerable properties (both on `source` + * and on `destination`) will be ignored. + * </div> * * @param {*} source The source that will be used to make a copy. * Can be any type, including primitives, `null`, and `undefined`. @@ -835,52 +924,54 @@ function arrayRemove(array, value) { * @returns {*} The copy or updated `destination`, if `destination` was specified. * * @example - <example module="copyExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form novalidate class="simple-form"> - Name: <input type="text" ng-model="user.name" /><br /> - E-mail: <input type="email" ng-model="user.email" /><br /> - Gender: <input type="radio" ng-model="user.gender" value="male" />male - <input type="radio" ng-model="user.gender" value="female" />female<br /> - <button ng-click="reset()">RESET</button> - <button ng-click="update(user)">SAVE</button> - </form> - <pre>form = {{user | json}}</pre> - <pre>master = {{master | json}}</pre> - </div> - - <script> - angular.module('copyExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.master= {}; - - $scope.update = function(user) { - // Example with 1 argument - $scope.master= angular.copy(user); - }; + <example module="copyExample" name="angular-copy"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <form novalidate class="simple-form"> + <label>Name: <input type="text" ng-model="user.name" /></label><br /> + <label>Age: <input type="number" ng-model="user.age" /></label><br /> + Gender: <label><input type="radio" ng-model="user.gender" value="male" />male</label> + <label><input type="radio" ng-model="user.gender" value="female" />female</label><br /> + <button ng-click="reset()">RESET</button> + <button ng-click="update(user)">SAVE</button> + </form> + <pre>form = {{user | json}}</pre> + <pre>master = {{master | json}}</pre> + </div> + </file> + <file name="script.js"> + // Module: copyExample + angular. + module('copyExample', []). + controller('ExampleController', ['$scope', function($scope) { + $scope.master = {}; + + $scope.reset = function() { + // Example with 1 argument + $scope.user = angular.copy($scope.master); + }; - $scope.reset = function() { - // Example with 2 arguments - angular.copy($scope.master, $scope.user); - }; + $scope.update = function(user) { + // Example with 2 arguments + angular.copy(user, $scope.master); + }; - $scope.reset(); - }]); - </script> - </file> - </example> + $scope.reset(); + }]); + </file> + </example> */ -function copy(source, destination) { +function copy(source, destination, maxDepth) { var stackSource = []; var stackDest = []; + maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN; if (destination) { - if (isTypedArray(destination)) { - throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated."); + if (isTypedArray(destination) || isArrayBuffer(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."); + throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.'); } // Empty the destination object @@ -896,35 +987,39 @@ function copy(source, destination) { stackSource.push(source); stackDest.push(destination); - return copyRecurse(source, destination); + return copyRecurse(source, destination, maxDepth); } - return copyElement(source); + return copyElement(source, maxDepth); - function copyRecurse(source, destination) { + function copyRecurse(source, destination, maxDepth) { + maxDepth--; + if (maxDepth < 0) { + return '...'; + } var h = destination.$$hashKey; - var result, key; + var key; if (isArray(source)) { for (var i = 0, ii = source.length; i < ii; i++) { - destination.push(copyElement(source[i])); + destination.push(copyElement(source[i], maxDepth)); } } 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]); + destination[key] = copyElement(source[key], maxDepth); } } else if (source && typeof source.hasOwnProperty === 'function') { // Slow path, which must rely on hasOwnProperty for (key in source) { if (source.hasOwnProperty(key)) { - destination[key] = copyElement(source[key]); + destination[key] = copyElement(source[key], maxDepth); } } } 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]); + destination[key] = copyElement(source[key], maxDepth); } } } @@ -932,7 +1027,7 @@ function copy(source, destination) { return destination; } - function copyElement(source) { + function copyElement(source, maxDepth) { // Simple values if (!isObject(source)) { return source; @@ -946,26 +1041,14 @@ function copy(source, destination) { if (isWindow(source) || isScope(source)) { throw ngMinErr('cpws', - "Can't copy! Making copies of Window or Scope instances is not supported."); + 'Can\'t copy! Making copies of Window or Scope instances is not supported.'); } var needsRecurse = false; - var destination; + var destination = copyType(source); - if (isArray(source)) { - destination = []; - needsRecurse = true; - } else if (isTypedArray(source)) { - destination = new source.constructor(source); - } else if (isDate(source)) { - destination = new Date(source.getTime()); - } else if (isRegExp(source)) { - destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]); - destination.lastIndex = source.lastIndex; - } else if (isFunction(source.cloneNode)) { - destination = source.cloneNode(true); - } else { - destination = Object.create(getPrototypeOf(source)); + if (destination === undefined) { + destination = isArray(source) ? [] : Object.create(getPrototypeOf(source)); needsRecurse = true; } @@ -973,34 +1056,54 @@ function copy(source, destination) { stackDest.push(destination); return needsRecurse - ? copyRecurse(source, destination) + ? copyRecurse(source, destination, maxDepth) : destination; } -} -/** - * Creates a shallow copy of an object, an array or a primitive. - * - * Assumes that there are no proto properties for objects. - */ -function shallowCopy(src, dst) { - if (isArray(src)) { - dst = dst || []; + function copyType(source) { + switch (toString.call(source)) { + case '[object Int8Array]': + case '[object Int16Array]': + case '[object Int32Array]': + case '[object Float32Array]': + case '[object Float64Array]': + case '[object Uint8Array]': + case '[object Uint8ClampedArray]': + case '[object Uint16Array]': + case '[object Uint32Array]': + return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length); + + case '[object ArrayBuffer]': + // Support: IE10 + if (!source.slice) { + // If we're in this case we know the environment supports ArrayBuffer + /* eslint-disable no-undef */ + var copied = new ArrayBuffer(source.byteLength); + new Uint8Array(copied).set(new Uint8Array(source)); + /* eslint-enable */ + return copied; + } + return source.slice(0); - for (var i = 0, ii = src.length; i < ii; i++) { - dst[i] = src[i]; + case '[object Boolean]': + case '[object Number]': + case '[object String]': + case '[object Date]': + return new source.constructor(source.valueOf()); + + case '[object RegExp]': + var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]); + re.lastIndex = source.lastIndex; + return re; + + case '[object Blob]': + return new source.constructor([source], {type: source.type}); } - } else if (isObject(src)) { - dst = dst || {}; - for (var key in src) { - if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { - dst[key] = src[key]; - } + if (isFunction(source.cloneNode)) { + return source.cloneNode(true); } } - - return dst || src; } @@ -1032,44 +1135,78 @@ function shallowCopy(src, dst) { * @param {*} o1 Object or value to compare. * @param {*} o2 Object or value to compare. * @returns {boolean} True if arguments are equal. + * + * @example + <example module="equalsExample" name="equalsExample"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <form novalidate> + <h3>User 1</h3> + Name: <input type="text" ng-model="user1.name"> + Age: <input type="number" ng-model="user1.age"> + + <h3>User 2</h3> + Name: <input type="text" ng-model="user2.name"> + Age: <input type="number" ng-model="user2.age"> + + <div> + <br/> + <input type="button" value="Compare" ng-click="compare()"> + </div> + User 1: <pre>{{user1 | json}}</pre> + User 2: <pre>{{user2 | json}}</pre> + Equal: <pre>{{result}}</pre> + </form> + </div> + </file> + <file name="script.js"> + angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) { + $scope.user1 = {}; + $scope.user2 = {}; + $scope.compare = function() { + $scope.result = angular.equals($scope.user1, $scope.user2); + }; + }]); + </file> + </example> */ function equals(o1, o2) { if (o1 === o2) return true; if (o1 === null || o2 === null) return false; + // eslint-disable-next-line no-self-compare if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN var t1 = typeof o1, t2 = typeof o2, length, key, keySet; - if (t1 == t2) { - if (t1 == 'object') { - if (isArray(o1)) { - if (!isArray(o2)) return false; - if ((length = o1.length) == o2.length) { - for (key = 0; key < length; key++) { - if (!equals(o1[key], o2[key])) return false; - } - return true; - } - } else if (isDate(o1)) { - if (!isDate(o2)) return false; - return equals(o1.getTime(), o2.getTime()); - } else if (isRegExp(o1)) { - return isRegExp(o2) ? o1.toString() == o2.toString() : false; - } else { - if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || - isArray(o2) || isDate(o2) || isRegExp(o2)) return false; - keySet = createMap(); - for (key in o1) { - if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (t1 === t2 && t1 === 'object') { + if (isArray(o1)) { + if (!isArray(o2)) return false; + if ((length = o1.length) === o2.length) { + for (key = 0; key < length; key++) { if (!equals(o1[key], o2[key])) return false; - keySet[key] = true; - } - for (key in o2) { - if (!(key in keySet) && - key.charAt(0) !== '$' && - isDefined(o2[key]) && - !isFunction(o2[key])) return false; } return true; } + } else if (isDate(o1)) { + if (!isDate(o2)) return false; + return equals(o1.getTime(), o2.getTime()); + } else if (isRegExp(o1)) { + if (!isRegExp(o2)) return false; + return o1.toString() === o2.toString(); + } else { + if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || + isArray(o2) || isDate(o2) || isRegExp(o2)) return false; + keySet = createMap(); + for (key in o1) { + if (key.charAt(0) === '$' || isFunction(o1[key])) continue; + if (!equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for (key in o2) { + if (!(key in keySet) && + key.charAt(0) !== '$' && + isDefined(o2[key]) && + !isFunction(o2[key])) return false; + } + return true; } } return false; @@ -1079,8 +1216,8 @@ var csp = function() { if (!isDefined(csp.rules)) { - var ngCspElement = (document.querySelector('[ng-csp]') || - document.querySelector('[data-ng-csp]')); + var ngCspElement = (window.document.querySelector('[ng-csp]') || + window.document.querySelector('[data-ng-csp]')); if (ngCspElement) { var ngCspAttribute = ngCspElement.getAttribute('ng-csp') || @@ -1101,9 +1238,8 @@ var csp = function() { function noUnsafeEval() { try { - /* jshint -W031, -W054 */ + // eslint-disable-next-line no-new, no-new-func new Function(''); - /* jshint +W031, +W054 */ return false; } catch (e) { return true; @@ -1155,7 +1291,8 @@ var jq = function() { var i, ii = ngAttrPrefixes.length, prefix, name; for (i = 0; i < ii; ++i) { prefix = ngAttrPrefixes[i]; - if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) { + el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]'); + if (el) { name = el.getAttribute(prefix + 'jq'); break; } @@ -1173,7 +1310,6 @@ function sliceArgs(args, startIndex) { } -/* jshint -W101 */ /** * @ngdoc function * @name angular.bind @@ -1191,7 +1327,6 @@ function sliceArgs(args, startIndex) { * @param {...*} args Optional arguments to be prebound to the `fn` function call. * @returns {function()} Function that wraps the `fn` with all the specified bindings. */ -/* jshint +W101 */ function bind(self, fn) { var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; if (isFunction(fn) && !(fn instanceof RegExp)) { @@ -1207,7 +1342,7 @@ function bind(self, fn) { : fn.call(self); }; } else { - // in IE, native methods are not functions so they cannot be bound (note: they don't need to be) + // In IE, native methods are not functions so they cannot be bound (note: they don't need to be). return fn; } } @@ -1220,7 +1355,7 @@ function toJsonReplacer(key, value) { val = undefined; } else if (isWindow(value)) { val = '$WINDOW'; - } else if (value && document === value) { + } else if (value && window.document === value) { val = '$DOCUMENT'; } else if (isScope(value)) { val = '$SCOPE'; @@ -1240,13 +1375,34 @@ function toJsonReplacer(key, value) { * Serializes input into a JSON-formatted string. Properties with leading $$ characters will be * stripped since angular uses this notation internally. * - * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON. + * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON. * @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace. * If set to an integer, the JSON output will contain that many spaces per indentation. * @returns {string|undefined} JSON-ified string representing `obj`. + * @knownIssue + * + * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date` + * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the + * `Date.prototype.toJSON` method as follows: + * + * ``` + * var _DatetoJSON = Date.prototype.toJSON; + * Date.prototype.toJSON = function() { + * try { + * return _DatetoJSON.call(this); + * } catch(e) { + * if (e instanceof RangeError) { + * return null; + * } + * throw e; + * } + * }; + * ``` + * + * See https://github.com/angular/angular.js/pull/14221 for more information. */ function toJson(obj, pretty) { - if (typeof obj === 'undefined') return undefined; + if (isUndefined(obj)) return undefined; if (!isNumber(pretty)) { pretty = pretty ? 2 : null; } @@ -1273,9 +1429,13 @@ function fromJson(json) { } +var ALL_COLONS = /:/g; function timezoneToOffset(timezone, fallback) { + // Support: IE 9-11 only, Edge 13-14+ + // 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; + return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset; } @@ -1288,8 +1448,9 @@ function addDateMinutes(date, minutes) { function convertTimezoneToLocal(date, timezone, reverse) { reverse = reverse ? -1 : 1; - var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); - return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset())); + var dateTimezoneOffset = date.getTimezoneOffset(); + var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); + return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset)); } @@ -1302,13 +1463,13 @@ function startingTag(element) { // turns out IE does not let you set .html() on elements which // are not allowed to have children. So we just ignore it. element.empty(); - } catch (e) {} + } catch (e) { /* empty */ } var elemHtml = jqLite('<div>').append(element).html(); try { 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); } @@ -1330,7 +1491,7 @@ function tryDecodeURIComponent(value) { try { return decodeURIComponent(value); } catch (e) { - // Ignore any invalid uri component + // Ignore any invalid uri component. } } @@ -1341,7 +1502,7 @@ function tryDecodeURIComponent(value) { */ function parseKeyValue(/**string*/keyValue) { var obj = {}; - forEach((keyValue || "").split('&'), function(keyValue) { + forEach((keyValue || '').split('&'), function(keyValue) { var splitPoint, key, val; if (keyValue) { key = keyValue = keyValue.replace(/\+/g,'%20'); @@ -1406,7 +1567,7 @@ function encodeUriSegment(val) { * This method is intended for encoding *key* or *value* parts of query component. We need a custom * method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be * encoded per http://tools.ietf.org/html/rfc3986: - * query = *( pchar / "/" / "?" ) + * query = *( pchar / "/" / "?" ) * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" * pct-encoded = "%" HEXDIG HEXDIG @@ -1436,6 +1597,57 @@ function getNgAttribute(element, ngAttr) { return null; } +function allowAutoBootstrap(document) { + var script = document.currentScript; + + if (!script) { + // IE does not have `document.currentScript` + return true; + } + + // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack + if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) { + return false; + } + + var attributes = script.attributes; + var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')]; + + return srcs.every(function(src) { + if (!src) { + return true; + } + if (!src.value) { + return false; + } + + var link = document.createElement('a'); + link.href = src.value; + + if (document.location.origin === link.origin) { + // Same-origin resources are always allowed, even for non-whitelisted schemes. + return true; + } + // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web. + // This is to prevent angular.js bundled with browser extensions from being used to bypass the + // content security policy in web pages and other browser extensions. + switch (link.protocol) { + case 'http:': + case 'https:': + case 'ftp:': + case 'blob:': + case 'file:': + case 'data:': + return true; + default: + return false; + } + }); +} + +// Cached as it has to run during loading so that document.currentScript is available. +var isAutoBootstrapAllowed = allowAutoBootstrap(window.document); + /** * @ngdoc directive * @name ngApp @@ -1456,10 +1668,17 @@ function getNgAttribute(element, ngAttr) { * designates the **root element** of the application and is typically placed near the root element * of the page - e.g. on the `<body>` or `<html>` tags. * - * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` - * found in the document will be used to define the root element to auto-bootstrap as an - * application. To run multiple applications in an HTML document you must manually bootstrap them using - * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other. + * There are a few things to keep in mind when using `ngApp`: + * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp` + * found in the document will be used to define the root element to auto-bootstrap as an + * application. To run multiple applications in an HTML document you must manually bootstrap them using + * {@link angular.bootstrap} instead. + * - AngularJS applications cannot be nested within each other. + * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`. + * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and + * {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. * * You can specify an **AngularJS module** to be used as the root module for the application. This * module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It @@ -1472,7 +1691,7 @@ function getNgAttribute(element, ngAttr) { * * `ngApp` is the easiest, and most common way to bootstrap an application. * - <example module="ngAppDemo"> + <example module="ngAppDemo" name="ng-app"> <file name="index.html"> <div ng-controller="ngAppDemoController"> I can add: {{a}} + {{b}} = {{ a+b }} @@ -1488,7 +1707,7 @@ function getNgAttribute(element, ngAttr) { * * Using `ngStrictDi`, you would see something like this: * - <example ng-app-included="true"> + <example ng-app-included="true" name="strict-di"> <file name="index.html"> <div ng-app="ngAppStrictDemo" ng-strict-di> <div ng-controller="GoodController1"> @@ -1537,7 +1756,7 @@ function getNgAttribute(element, ngAttr) { }]) .controller('GoodController2', GoodController2); function GoodController2($scope) { - $scope.name = "World"; + $scope.name = 'World'; } GoodController2.$inject = ['$scope']; </file> @@ -1568,7 +1787,7 @@ function angularInit(element, bootstrap) { module, config = {}; - // The element `element` has priority over any other element + // The element `element` has priority over any other element. forEach(ngAttrPrefixes, function(prefix) { var name = prefix + 'app'; @@ -1587,7 +1806,12 @@ function angularInit(element, bootstrap) { } }); if (appElement) { - config.strictDi = getNgAttribute(appElement, "strict-di") !== null; + if (!isAutoBootstrapAllowed) { + window.console.error('Angular: disabling automatic bootstrap. <script> protocol indicates ' + + 'an extension, document.location.href does not match.'); + return; + } + config.strictDi = getNgAttribute(appElement, 'strict-di') !== null; bootstrap(appElement, module ? [module] : [], config); } } @@ -1599,16 +1823,25 @@ function angularInit(element, bootstrap) { * @description * Use this function to manually start up angular application. * - * See: {@link guide/bootstrap Bootstrap} - * - * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually. - * They must use {@link ng.directive:ngApp ngApp}. + * For more information, see the {@link guide/bootstrap Bootstrap guide}. * * Angular will detect if it has been loaded into the browser more than once and only allow the * first loaded script to be bootstrapped and will report a warning to the browser console for * each of the subsequent scripts. This prevents strange results in applications, where otherwise * multiple instances of Angular try to work on the DOM. * + * <div class="alert alert-warning"> + * **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually. + * They must use {@link ng.directive:ngApp ngApp}. + * </div> + * + * <div class="alert alert-warning"> + * **Note:** Do not bootstrap the app on an element with a directive that uses {@link ng.$compile#transclusion transclusion}, + * such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}. + * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector}, + * causing animations to stop working and making the injector inaccessible from outside the app. + * </div> + * * ```html * <!doctype html> * <html> @@ -1652,11 +1885,11 @@ function bootstrap(element, modules, config) { element = jqLite(element); if (element.injector()) { - var tag = (element[0] === document) ? 'document' : startingTag(element); - //Encode angle brackets to prevent input from being sanitized to empty string #8683 + var tag = (element[0] === window.document) ? 'document' : startingTag(element); + // Encode angle brackets to prevent input from being sanitized to empty string #8683. throw ngMinErr( 'btstrpd', - "App Already Bootstrapped with this Element '{0}'", + 'App already bootstrapped with this element \'{0}\'', tag.replace(/</,'<').replace(/>/,'>')); } @@ -1773,7 +2006,7 @@ function bindJQuery() { extend(jQuery.fn, { scope: JQLitePrototype.scope, isolateScope: JQLitePrototype.isolateScope, - controller: JQLitePrototype.controller, + controller: /** @type {?} */ (JQLitePrototype).controller, injector: JQLitePrototype.injector, inheritedData: JQLitePrototype.inheritedData }); @@ -1785,7 +2018,7 @@ function bindJQuery() { jQuery.cleanData = function(elems) { var events; for (var i = 0, elem; (elem = elems[i]) != null; i++) { - events = jQuery._data(elem, "events"); + events = jQuery._data(elem, 'events'); if (events && events.$destroy) { jQuery(elem).triggerHandler('$destroy'); } @@ -1807,7 +2040,7 @@ function bindJQuery() { */ function assertArg(arg, name, reason) { if (!arg) { - throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); + throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required')); } return arg; } @@ -1829,7 +2062,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) { */ function assertNotHasOwnProperty(name, context) { if (name === 'hasOwnProperty') { - throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); + throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); } } @@ -1899,6 +2132,27 @@ function createMap() { return Object.create(null); } +function stringify(value) { + if (value == null) { // null || undefined + return ''; + } + switch (typeof value) { + case 'string': + break; + case 'number': + value = '' + value; + break; + default: + if (hasCustomToString(value) && !isArray(value) && !isDate(value)) { + value = value.toString(); + } else { + value = toJson(value); + } + } + + return value; +} + var NODE_TYPE_ELEMENT = 1; var NODE_TYPE_ATTRIBUTE = 2; var NODE_TYPE_TEXT = 3; @@ -1982,9 +2236,12 @@ 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 info = {}; + var assertNotHasOwnProperty = function(name, context) { if (name === 'hasOwnProperty') { throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context); @@ -1997,9 +2254,9 @@ function setupModuleLoader(window) { } return ensure(modules, name, function() { if (!requires) { - throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " + - "the module name or forgot to load it. If registering a module ensure that you " + - "specify the dependencies as the second argument.", name); + throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' + + 'the module name or forgot to load it. If registering a module ensure that you ' + + 'specify the dependencies as the second argument.', name); } /** @type {!Array.<Array.<*>>} */ @@ -2021,6 +2278,45 @@ function setupModuleLoader(window) { _runBlocks: runBlocks, /** + * @ngdoc method + * @name angular.Module#info + * @module ng + * + * @param {Object=} info Information about the module + * @returns {Object|Module} The current info object for this module if called as a getter, + * or `this` if called as a setter. + * + * @description + * Read and write custom information about this module. + * For example you could put the version of the module in here. + * + * ```js + * angular.module('myModule', []).info({ version: '1.0.0' }); + * ``` + * + * The version could then be read back out by accessing the module elsewhere: + * + * ``` + * var version = angular.module('myModule').info().version; + * ``` + * + * You can also retrieve this information during runtime via the + * {@link $injector#modules `$injector.modules`} property: + * + * ```js + * var version = $injector.modules['myModule'].info().version; + * ``` + */ + info: function(value) { + if (isDefined(value)) { + if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value'); + info = value; + return this; + } + return info; + }, + + /** * @ngdoc property * @name angular.Module#requires * @module ng @@ -2103,13 +2399,13 @@ function setupModuleLoader(window) { * @ngdoc method * @name angular.Module#decorator * @module ng - * @param {string} The name of the service to decorate. - * @param {Function} This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. + * @param {string} name The name of the service to decorate. + * @param {Function} decorFn This function will be invoked when the service needs to be + * instantiated and should return the decorated service instance. * @description * See {@link auto.$provide#decorator $provide.decorator()}. */ - decorator: invokeLaterAndSetModuleName('$provide', 'decorator'), + decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks), /** * @ngdoc method @@ -2194,112 +2490,12 @@ function setupModuleLoader(window) { * @module ng * @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp) * @param {Object} options Component definition object (a simplified - * {@link ng.$compile#directive-definition-object directive definition object}), - * has the following properties (all optional): - * - * - `controller` – `{(string|function()=}` – Controller constructor function that should be - * associated with newly created scope or the name of a {@link ng.$compile#-controller- - * registered controller} if passed as a string. Empty function by default. - * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller. - * If present, the controller will be published to scope under the `controllerAs` name. - * If not present, this will default to be the same as the component name. - * - `template` – `{string=|function()=}` – html template as a string or a function that - * returns an html template as a string which should be used as the contents of this component. - * Empty string by default. - * - * If `template` is a function, then it is {@link auto.$injector#invoke injected} with - * the following locals: - * - * - `$element` - Current element - * - `$attrs` - Current attributes object for the element - * - * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html - * template that should be used as the contents of this component. - * - * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with - * the following locals: - * - * - `$element` - Current element - * - `$attrs` - Current attributes object for the element - * - `bindings` – `{object=}` – Define DOM attribute binding to component properties. - * Component properties are always bound to the component controller and not to the scope. - * - `transclude` – `{boolean=}` – Whether {@link $compile#transclusion transclusion} is enabled. - * Enabled by default. - * - `isolate` – `{boolean=}` – Whether the new scope is isolated. Isolated by default. - * - `restrict` - `{string=}` - String of subset of {@link ng.$compile#-restrict- EACM} which - * restricts the component to specific directive declaration style. If omitted, this defaults to 'E'. - * - `$canActivate` – `{function()=}` – TBD. - * - `$routeConfig` – `{object=}` – TBD. + * {@link ng.$compile#directive-definition-object directive definition object}) * * @description - * Register a component definition with the compiler. This is short for registering a specific - * subset of directives which represents actual UI components in your application. Component - * definitions are very simple and do not require the complexity behind defining directives. - * Component definitions usually consist only of the template and the controller backing it. - * In order to make the definition easier, components enforce best practices like controllerAs - * and default behaviors like scope isolation, restrict to elements and allow transclusion. - * - * Here are a few examples of how you would usually define components: - * - * ```js - * var myMod = angular.module(...); - * myMod.component('myComp', { - * template: '<div>My name is {{myComp.name}}</div>', - * controller: function() { - * this.name = 'shahar'; - * } - * }); - * - * myMod.component('myComp', { - * template: '<div>My name is {{myComp.name}}</div>', - * bindings: {name: '@'} - * }); - * - * myMod.component('myComp', { - * templateUrl: 'views/my-comp.html', - * controller: 'MyCtrl as ctrl', - * bindings: {name: '@'} - * }); - * - * ``` - * - * See {@link ng.$compileProvider#directive $compileProvider.directive()}. + * See {@link ng.$compileProvider#component $compileProvider.component()}. */ - component: function(name, options) { - function factory($injector) { - function makeInjectable(fn) { - if (angular.isFunction(fn)) { - return function(tElement, tAttrs) { - return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); - }; - } else { - return fn; - } - } - - var template = (!options.template && !options.templateUrl ? '' : options.template); - return { - controller: options.controller || function() {}, - controllerAs: identifierForController(options.controller) || options.controllerAs || name, - template: makeInjectable(template), - templateUrl: makeInjectable(options.templateUrl), - transclude: options.transclude === undefined ? true : options.transclude, - scope: options.isolate === false ? true : {}, - bindToController: options.bindings || {}, - restrict: options.restrict || 'E' - }; - } - - if (options.$canActivate) { - factory.$canActivate = options.$canActivate; - } - if (options.$routeConfig) { - factory.$routeConfig = options.$routeConfig; - } - factory.$inject = ['$injector']; - - return moduleInstance.directive(name, factory); - }, + component: invokeLaterAndSetModuleName('$compileProvider', 'component'), /** * @ngdoc method @@ -2355,10 +2551,11 @@ function setupModuleLoader(window) { * @param {string} method * @returns {angular.Module} */ - function invokeLaterAndSetModuleName(provider, method) { + function invokeLaterAndSetModuleName(provider, method, queue) { + if (!queue) queue = invokeQueue; return function(recipeName, factoryFunction) { if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name; - invokeQueue.push([provider, method, arguments]); + queue.push([provider, method, arguments]); return moduleInstance; }; } @@ -2368,11 +2565,44 @@ function setupModuleLoader(window) { } -/* global: toDebugString: true */ +/* global shallowCopy: true */ + +/** + * Creates a shallow copy of an object, an array or a primitive. + * + * Assumes that there are no proto properties for objects. + */ +function shallowCopy(src, dst) { + if (isArray(src)) { + dst = dst || []; + + for (var i = 0, ii = src.length; i < ii; i++) { + dst[i] = src[i]; + } + } else if (isObject(src)) { + dst = dst || {}; + + for (var key in src) { + if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + } -function serializeObject(obj) { + return dst || src; +} + +/* global toDebugString: true */ + +function serializeObject(obj, maxDepth) { var seen = []; + // There is no direct way to stringify object until reaching a specific depth + // and a very deep object can cause a performance issue, so we copy the object + // based on this specific depth and then stringify it. + if (isValidObjectMaxDepth(maxDepth)) { + obj = copy(obj, null, maxDepth); + } return JSON.stringify(obj, function(key, val) { val = toJsonReplacer(key, val); if (isObject(val)) { @@ -2385,13 +2615,13 @@ function serializeObject(obj) { }); } -function toDebugString(obj) { +function toDebugString(obj, maxDepth) { if (typeof obj === 'function') { return obj.toString().replace(/ \{[\s\S]*$/, ''); } else if (isUndefined(obj)) { return 'undefined'; } else if (typeof obj !== 'string') { - return serializeObject(obj); + return serializeObject(obj, maxDepth); } return obj; } @@ -2407,7 +2637,6 @@ function toDebugString(obj) { formDirective, scriptDirective, selectDirective, - styleDirective, optionDirective, ngBindDirective, ngBindHtmlDirective, @@ -2452,26 +2681,30 @@ function toDebugString(obj) { $AnchorScrollProvider, $AnimateProvider, $CoreAnimateCssProvider, + $$CoreAnimateJsProvider, $$CoreAnimateQueueProvider, - $$CoreAnimateRunnerProvider, + $$AnimateRunnerFactoryProvider, + $$AnimateAsyncRunFactoryProvider, $BrowserProvider, $CacheFactoryProvider, $ControllerProvider, $DateProvider, $DocumentProvider, + $$IsDocumentHiddenProvider, $ExceptionHandlerProvider, $FilterProvider, $$ForceReflowProvider, $InterpolateProvider, $IntervalProvider, - $$HashMapProvider, $HttpProvider, $HttpParamSerializerProvider, $HttpParamSerializerJQLikeProvider, $HttpBackendProvider, $xhrFactoryProvider, + $jsonpCallbacksProvider, $LocationProvider, $LogProvider, + $$MapProvider, $ParseProvider, $RootScopeProvider, $QProvider, @@ -2507,16 +2740,19 @@ function toDebugString(obj) { * - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". */ var version = { - full: '1.5.0-beta.2', // all of these placeholder strings will be replaced by grunt's - major: 1, // package task - minor: 5, - dot: 0, - codeName: 'effective-delegation' + // These placeholder strings will be replaced by grunt's `build` task. + // They need to be double- or single-quoted. + full: '1.6.3', + major: 1, + minor: 6, + dot: 3, + codeName: 'scriptalicious-bootstrapping' }; function publishExternalAPI(angular) { extend(angular, { + 'errorHandlingConfig': errorHandlingConfig, 'bootstrap': bootstrap, 'copy': copy, 'extend': extend, @@ -2542,11 +2778,14 @@ function publishExternalAPI(angular) { 'isDate': isDate, 'lowercase': lowercase, 'uppercase': uppercase, - 'callbacks': {counter: 0}, + 'callbacks': {$$counter: 0}, 'getTestability': getTestability, + 'reloadWithDebugInfo': reloadWithDebugInfo, '$$minErr': minErr, '$$csp': csp, - 'reloadWithDebugInfo': reloadWithDebugInfo + '$$encodeUriSegment': encodeUriSegment, + '$$encodeUriQuery': encodeUriQuery, + '$$stringify': stringify }); angularModule = setupModuleLoader(window); @@ -2565,7 +2804,6 @@ function publishExternalAPI(angular) { form: formDirective, script: scriptDirective, select: selectDirective, - style: styleDirective, option: optionDirective, ngBind: ngBindDirective, ngBindHtml: ngBindHtmlDirective, @@ -2613,12 +2851,15 @@ function publishExternalAPI(angular) { $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, $animateCss: $CoreAnimateCssProvider, + $$animateJs: $$CoreAnimateJsProvider, $$animateQueue: $$CoreAnimateQueueProvider, - $$AnimateRunner: $$CoreAnimateRunnerProvider, + $$AnimateRunner: $$AnimateRunnerFactoryProvider, + $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, $browser: $BrowserProvider, $cacheFactory: $CacheFactoryProvider, $controller: $ControllerProvider, $document: $DocumentProvider, + $$isDocumentHidden: $$IsDocumentHiddenProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, $$forceReflow: $$ForceReflowProvider, @@ -2629,6 +2870,7 @@ function publishExternalAPI(angular) { $httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider, $httpBackend: $HttpBackendProvider, $xhrFactory: $xhrFactoryProvider, + $jsonpCallbacks: $jsonpCallbacksProvider, $location: $LocationProvider, $log: $LogProvider, $parse: $ParseProvider, @@ -2645,11 +2887,12 @@ function publishExternalAPI(angular) { $window: $WindowProvider, $$rAF: $$RAFProvider, $$jqLite: $$jqLiteProvider, - $$HashMap: $$HashMapProvider, + $$Map: $$MapProvider, $$cookieReader: $$CookieReaderProvider }); } - ]); + ]) + .info({ angularVersion: '1.6.3' }); } /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -2663,11 +2906,10 @@ function publishExternalAPI(angular) { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -/* global JQLitePrototype: true, - addEventListenerFn: true, - removeEventListenerFn: true, +/* global + JQLitePrototype: true, BOOLEAN_ATTR: true, - ALIASED_ATTR: true, + ALIASED_ATTR: true */ ////////////////////////////////// @@ -2685,29 +2927,36 @@ 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: * - * - [`addClass()`](http://api.jquery.com/addClass/) + * - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument * - [`after()`](http://api.jquery.com/after/) * - [`append()`](http://api.jquery.com/append/) * - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters - * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData + * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData * - [`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/) @@ -2722,16 +2971,16 @@ function publishExternalAPI(angular) { * - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors * - [`prepend()`](http://api.jquery.com/prepend/) * - [`prop()`](http://api.jquery.com/prop/) - * - [`ready()`](http://api.jquery.com/ready/) + * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`) * - [`remove()`](http://api.jquery.com/remove/) - * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - * - [`removeClass()`](http://api.jquery.com/removeClass/) + * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes + * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument * - [`removeData()`](http://api.jquery.com/removeData/) * - [`replaceWith()`](http://api.jquery.com/replaceWith/) * - [`text()`](http://api.jquery.com/text/) - * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers. - * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter + * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument + * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers + * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter * - [`val()`](http://api.jquery.com/val/) * - [`wrap()`](http://api.jquery.com/wrap/) * @@ -2759,6 +3008,9 @@ function publishExternalAPI(angular) { * - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top * parent element is reached. * + * @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See + * https://github.com/angular/angular.js/issues/14251 for more information. + * * @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. * @returns {Object} jQuery object. */ @@ -2766,13 +3018,7 @@ function publishExternalAPI(angular) { JQLite.expando = 'ng339'; var jqCache = JQLite.cache = {}, - jqId = 1, - addEventListenerFn = function(element, type, fn) { - element.addEventListener(type, fn, false); - }, - removeEventListenerFn = function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; + jqId = 1; /* * !!! This is an undocumented "private" function !!! @@ -2785,22 +3031,31 @@ JQLite._data = function(node) { function jqNextId() { return ++jqId; } -var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; -var MOZ_HACK_REGEXP = /^moz([A-Z])/; -var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"}; +var DASH_LOWERCASE_REGEXP = /-([a-z])/g; +var MS_HACK_REGEXP = /^-ms-/; +var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' }; var jqLiteMinErr = minErr('jqLite'); /** - * Converts snake_case to camelCase. - * Also there is special case for Moz prefix starting with upper case letter. + * Converts kebab-case to camelCase. + * There is also a special case for the ms prefix starting with a lowercase letter. * @param name Name to normalize */ -function camelCase(name) { - return name. - replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) { - return offset ? letter.toUpperCase() : letter; - }). - replace(MOZ_HACK_REGEXP, 'Moz$1'); +function cssKebabToCamel(name) { + return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-')); +} + +function fnCamelCaseReplace(all, letter) { + return letter.toUpperCase(); +} + +/** + * Converts kebab-case to camelCase. + * @param name Name to normalize + */ +function kebabToCamel(name) { + return name + .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace); } var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/; @@ -2815,7 +3070,7 @@ var wrapMap = { 'col': [2, '<table><colgroup>', '</colgroup></table>'], 'tr': [2, '<table><tbody>', '</tbody></table>'], 'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'], - '_default': [0, "", ""] + '_default': [0, '', ''] }; wrapMap.optgroup = wrapMap.option; @@ -2857,10 +3112,10 @@ function jqLiteBuildFragment(html, context) { nodes.push(context.createTextNode(html)); } else { // Convert html into DOM nodes - tmp = tmp || fragment.appendChild(context.createElement("div")); - tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase(); + tmp = fragment.appendChild(context.createElement('div')); + tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); wrap = wrapMap[tag] || wrapMap._default; - tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2]; + tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') + wrap[2]; // Descend through wrappers to the right content i = wrap[0]; @@ -2871,12 +3126,12 @@ function jqLiteBuildFragment(html, context) { nodes = concat(nodes, tmp.childNodes); tmp = fragment.firstChild; - tmp.textContent = ""; + tmp.textContent = ''; } // Remove wrapper from fragment - fragment.textContent = ""; - fragment.innerHTML = ""; // Clear inner HTML + fragment.textContent = ''; + fragment.innerHTML = ''; // Clear inner HTML forEach(nodes, function(node) { fragment.appendChild(node); }); @@ -2885,7 +3140,7 @@ function jqLiteBuildFragment(html, context) { } function jqLiteParseHTML(html, context) { - context = context || document; + context = context || window.document; var parsed; if ((parsed = SINGLE_TAG_REGEXP.exec(html))) { @@ -2899,12 +3154,21 @@ 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 +var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) { + // eslint-disable-next-line no-bitwise return !!(this.compareDocumentPosition(arg) & 16); - // jshint bitwise: true }; ///////////////////////////////////////////// @@ -2920,7 +3184,7 @@ function JQLite(element) { argIsString = true; } if (!(this instanceof JQLite)) { - if (argIsString && element.charAt(0) != '<') { + if (argIsString && element.charAt(0) !== '<') { throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element'); } return new JQLite(element); @@ -2928,6 +3192,8 @@ function JQLite(element) { if (argIsString) { jqLiteAddNodes(this, jqLiteParseHTML(element)); + } else if (isFunction(element)) { + jqLiteReady(element); } else { jqLiteAddNodes(this, element); } @@ -2960,7 +3226,7 @@ function jqLiteOff(element, type, fn, unsupported) { if (!type) { for (type in events) { if (type !== '$destroy') { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); } delete events[type]; } @@ -2972,7 +3238,7 @@ function jqLiteOff(element, type, fn, unsupported) { arrayRemove(listenerFns || [], fn); } if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) { - removeEventListenerFn(element, type, handle); + element.removeEventListener(type, handle); delete events[type]; } }; @@ -3023,6 +3289,7 @@ function jqLiteExpandoStore(element, createIfNecessary) { function jqLiteData(element, key, value) { if (jqLiteAcceptsData(element)) { + var prop; var isSimpleSetter = isDefined(value); var isSimpleGetter = !isSimpleSetter && key && !isObject(key); @@ -3031,16 +3298,18 @@ function jqLiteData(element, key, value) { var data = expandoStore && expandoStore.data; if (isSimpleSetter) { // data('key', value) - data[key] = value; + data[kebabToCamel(key)] = value; } else { if (massGetter) { // data() return data; } else { if (isSimpleGetter) { // data('key') // don't force creation of expandoStore if it doesn't exist yet - return data && data[key]; + return data && data[kebabToCamel(key)]; } else { // mass-setter: data({key1: val1, key2: val2}) - extend(data, key); + for (prop in key) { + data[kebabToCamel(prop)] = key[prop]; + } } } } @@ -3049,17 +3318,17 @@ function jqLiteData(element, key, value) { function jqLiteHasClass(element, selector) { if (!element.getAttribute) return false; - return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " "). - indexOf(" " + selector + " ") > -1); + return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' '). + indexOf(' ' + selector + ' ') > -1); } function jqLiteRemoveClass(element, cssClasses) { if (cssClasses && element.setAttribute) { forEach(cssClasses.split(' '), function(cssClass) { element.setAttribute('class', trim( - (" " + (element.getAttribute('class') || '') + " ") - .replace(/[\n\t]/g, " ") - .replace(" " + trim(cssClass) + " ", " ")) + (' ' + (element.getAttribute('class') || '') + ' ') + .replace(/[\n\t]/g, ' ') + .replace(' ' + trim(cssClass) + ' ', ' ')) ); }); } @@ -3068,7 +3337,7 @@ function jqLiteRemoveClass(element, cssClasses) { function jqLiteAddClass(element, cssClasses) { if (cssClasses && element.setAttribute) { var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') - .replace(/[\n\t]/g, " "); + .replace(/[\n\t]/g, ' '); forEach(cssClasses.split(' '), function(cssClass) { cssClass = trim(cssClass); @@ -3115,7 +3384,7 @@ function jqLiteController(element, name) { function jqLiteInheritedData(element, name, value) { // if element is the document object work with the html element instead // this makes $(document).scope() possible - if (element.nodeType == NODE_TYPE_DOCUMENT) { + if (element.nodeType === NODE_TYPE_DOCUMENT) { element = element.documentElement; } var names = isArray(name) ? name : [name]; @@ -3149,7 +3418,7 @@ function jqLiteRemove(element, keepData) { function jqLiteDocumentLoaded(action, win) { win = win || window; if (win.document.readyState === 'complete') { - // Force the action to be run async for consistent behaviour + // Force the action to be run async for consistent behavior // from the action's point of view // i.e. it will definitely not be in a $apply win.setTimeout(action); @@ -3159,30 +3428,32 @@ function jqLiteDocumentLoaded(action, win) { } } +function jqLiteReady(fn) { + function trigger() { + window.document.removeEventListener('DOMContentLoaded', trigger); + window.removeEventListener('load', trigger); + fn(); + } + + // check if document is already loaded + if (window.document.readyState === 'complete') { + window.setTimeout(fn); + } else { + // We can not use jqLite since we are not done loading and jQuery could be loaded later. + + // Works for modern browsers and IE9 + window.document.addEventListener('DOMContentLoaded', trigger); + + // Fallback to window.onload for others + window.addEventListener('load', trigger); + } +} + ////////////////////////////////////////// // Functions which are declared directly. ////////////////////////////////////////// var JQLitePrototype = JQLite.prototype = { - ready: function(fn) { - var fired = false; - - function trigger() { - if (fired) return; - fired = true; - fn(); - } - - // check if document is already loaded - if (document.readyState === 'complete') { - setTimeout(trigger); - } else { - this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9 - // we can not use jqLite since we are not done loading and jQuery could be loaded later. - // jshint -W064 - JQLite(window).on('load', trigger); // fallback to window.onload for others - // jshint +W064 - } - }, + ready: jqLiteReady, toString: function() { var value = []; forEach(this, function(e) { value.push('' + e);}); @@ -3217,7 +3488,8 @@ var ALIASED_ATTR = { 'ngMaxlength': 'maxlength', 'ngMin': 'min', 'ngMax': 'max', - 'ngPattern': 'pattern' + 'ngPattern': 'pattern', + 'ngStep': 'step' }; function getBooleanAttrName(element, name) { @@ -3268,7 +3540,7 @@ forEach({ hasClass: jqLiteHasClass, css: function(element, name, value) { - name = camelCase(name); + name = cssKebabToCamel(name); if (isDefined(value)) { element.style[name] = value; @@ -3278,33 +3550,33 @@ forEach({ }, attr: function(element, name, value) { + var ret; var nodeType = element.nodeType; - if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) { + if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT || + !element.getAttribute) { return; } + var lowercasedName = lowercase(name); - if (BOOLEAN_ATTR[lowercasedName]) { - if (isDefined(value)) { - if (!!value) { - element[name] = true; - element.setAttribute(name, lowercasedName); - } else { - element[name] = false; - element.removeAttribute(lowercasedName); - } + var isBooleanAttr = BOOLEAN_ATTR[lowercasedName]; + + if (isDefined(value)) { + // setter + + if (value === null || (value === false && isBooleanAttr)) { + element.removeAttribute(name); } else { - return (element[name] || - (element.attributes.getNamedItem(name) || noop).specified) - ? lowercasedName - : undefined; - } - } else if (isDefined(value)) { - element.setAttribute(name, value); - } else if (element.getAttribute) { - // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code - // some elements (e.g. Document) don't have get attribute, so return undefined - var ret = element.getAttribute(name, 2); - // normalize non-existing attributes to undefined (as jQuery) + element.setAttribute(name, isBooleanAttr ? lowercasedName : value); + } + } else { + // getter + + ret = element.getAttribute(name); + + if (isBooleanAttr && ret !== null) { + ret = lowercasedName; + } + // Normalize non-existing attributes to undefined (as jQuery). return ret === null ? undefined : ret; } }, @@ -3339,7 +3611,7 @@ forEach({ result.push(option.value || option.text); } }); - return result.length === 0 ? null : result; + return result; } return element.value; } @@ -3367,7 +3639,7 @@ forEach({ // in a way that survives minification. // jqLiteEmpty takes no arguments but is a setter. if (fn !== jqLiteEmpty && - (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) { + (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 @@ -3509,7 +3781,7 @@ forEach({ eventFns = events[type] = []; eventFns.specialHandlerWrapper = specialHandlerWrapper; if (type !== '$destroy' && !noEventListener) { - addEventListenerFn(element, type, handle); + element.addEventListener(type, handle); } } @@ -3591,12 +3863,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, @@ -3607,12 +3874,15 @@ forEach({ after: function(element, newElement) { var index = element, parent = element.parentNode; - newElement = new JQLite(newElement); - for (var i = 0, ii = newElement.length; i < ii; i++) { - var node = newElement[i]; - parent.insertBefore(node, index.nextSibling); - index = node; + if (parent) { + newElement = new JQLite(newElement); + + for (var i = 0, ii = newElement.length; i < ii; i++) { + var node = newElement[i]; + parent.insertBefore(node, index.nextSibling); + index = node; + } } }, @@ -3706,14 +3976,15 @@ forEach({ } return isDefined(value) ? value : this; }; - - // bind legacy bind/unbind to on/off - JQLite.prototype.bind = JQLite.prototype.on; - JQLite.prototype.unbind = JQLite.prototype.off; }); +// bind legacy bind/unbind to on/off +JQLite.prototype.bind = JQLite.prototype.on; +JQLite.prototype.unbind = JQLite.prototype.off; + // Provider for private $$jqLite service +/** @this */ function $$jqLiteProvider() { this.$get = function $$jqLite() { return extend(JQLite, { @@ -3756,7 +4027,7 @@ function hashKey(obj, nextUidFn) { } var objType = typeof obj; - if (objType == 'function' || (objType == 'object' && obj !== null)) { + if (objType === 'function' || (objType === 'object' && obj !== null)) { key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)(); } else { key = objType + ':' + obj; @@ -3765,50 +4036,70 @@ function hashKey(obj, nextUidFn) { return key; } -/** - * HashMap which can use objects as keys - */ -function HashMap(array, isolatedUid) { - if (isolatedUid) { - var uid = 0; - this.nextUid = function() { - return ++uid; - }; - } - forEach(array, this.put, this); +// A minimal ES2015 Map implementation. +// Should be bug/feature equivalent to the native implementations of supported browsers +// (for the features required in Angular). +// See https://kangax.github.io/compat-table/es6/#test-Map +var nanKey = Object.create(null); +function NgMapShim() { + this._keys = []; + this._values = []; + this._lastKey = NaN; + this._lastIndex = -1; } -HashMap.prototype = { - /** - * Store key value pair - * @param key key to store can be any type - * @param value value to store can be any type - */ - put: function(key, value) { - this[hashKey(key, this.nextUid)] = value; +NgMapShim.prototype = { + _idx: function(key) { + if (key === this._lastKey) { + return this._lastIndex; + } + this._lastKey = key; + this._lastIndex = this._keys.indexOf(key); + return this._lastIndex; + }, + _transformKey: function(key) { + return isNumberNaN(key) ? nanKey : key; }, - - /** - * @param key - * @returns {Object} the value for the key - */ get: function(key) { - return this[hashKey(key, this.nextUid)]; + key = this._transformKey(key); + var idx = this._idx(key); + if (idx !== -1) { + return this._values[idx]; + } }, + set: function(key, value) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + idx = this._lastIndex = this._keys.length; + } + this._keys[idx] = key; + this._values[idx] = value; - /** - * Remove the key/value pair - * @param key - */ - remove: function(key) { - var value = this[key = hashKey(key, this.nextUid)]; - delete this[key]; - return value; + // Support: IE11 + // Do not `return this` to simulate the partial IE11 implementation + }, + delete: function(key) { + key = this._transformKey(key); + var idx = this._idx(key); + if (idx === -1) { + return false; + } + this._keys.splice(idx, 1); + this._values.splice(idx, 1); + this._lastKey = NaN; + this._lastIndex = -1; + return true; } }; -var $$HashMapProvider = [function() { +// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations +// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map` +// implementations get more stable, we can reconsider switching to `window.Map` (when available). +var NgMap = NgMapShim; + +var $$MapProvider = [/** @this */function() { this.$get = [function() { - return HashMap; + return NgMap; }]; }]; @@ -3869,20 +4160,25 @@ var $$HashMapProvider = [function() { /** * @ngdoc module * @name auto + * @installation * @description * * Implicit module which gets automatically added to each {@link auto.$injector $injector}. */ -var ARROW_ARG = /^([^\(]+?)=>/; -var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; +var ARROW_ARG = /^([^(]+?)=>/; +var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; var $injectorMinErr = minErr('$injector'); +function stringifyFn(fn) { + return Function.prototype.toString.call(fn); +} + function extractArgs(fn) { - var fnText = fn.toString().replace(STRIP_COMMENTS, ''), + var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''), args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS); return args; } @@ -3988,6 +4284,28 @@ function annotate(fn, strictDi, name) { */ /** + * @ngdoc property + * @name $injector#modules + * @type {Object} + * @description + * A hash containing all the modules that have been loaded into the + * $injector. + * + * You can use this property to find out information about a module via the + * {@link angular.Module#info `myModule.info(...)`} method. + * + * For example: + * + * ``` + * var info = $injector.modules['ngAnimate'].info(); + * ``` + * + * **Do not use this property to attempt to modify the modules after the application + * has been bootstrapped.** + */ + + +/** * @ngdoc method * @name $injector#get * @@ -4125,7 +4443,6 @@ function annotate(fn, strictDi, name) { - /** * @ngdoc service * @name $provide @@ -4150,18 +4467,20 @@ function annotate(fn, strictDi, name) { * these cases the {@link auto.$provide $provide} service has additional helper methods to register * services without specifying a provider. * - * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the + * * {@link auto.$provide#provider provider(name, provider)} - registers a **service provider** with the * {@link auto.$injector $injector} - * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by + * * {@link auto.$provide#constant constant(name, obj)} - registers a value/object that can be accessed by * providers and services. - * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by + * * {@link auto.$provide#value value(name, obj)} - registers a value/object that can only be accessed by * services, not providers. - * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, + * * {@link auto.$provide#factory factory(name, fn)} - registers a service **factory function** * that will be wrapped in a **service provider** object, whose `$get` property will contain the * given factory function. - * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class` + * * {@link auto.$provide#service service(name, Fn)} - registers a **constructor function** * that will be wrapped in a **service provider** object, whose `$get` property will instantiate * a new object using the given constructor function. + * * {@link auto.$provide#decorator decorator(name, decorFn)} - registers a **decorator function** that + * will be able to modify or replace the implementation of another service. * * See the individual methods for more information and examples. */ @@ -4303,8 +4622,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. @@ -4344,14 +4675,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. @@ -4376,8 +4706,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}. * @@ -4404,18 +4737,20 @@ function annotate(fn, strictDi, name) { * @name $provide#decorator * @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 - * 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. + * Register a **decorator function** with the {@link auto.$injector $injector}. A decorator function + * intercepts the creation of a service, allowing it to override or modify the behavior of the + * service. The return value of the decorator function may be the original service, or a new service + * that replaces (or wraps and delegates to) the original service. + * + * You can find out more about using decorators in the {@link guide/decorators} guide. * * @param {string} name The name of the service to decorate. * @param {Function|Array.<string|Function>} decorator This function will be invoked when the service needs to be - * instantiated and should return the decorated service instance. The function is called using + * provided and should return the decorated service instance. The function is called using * the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable. * Local injection arguments: * - * * `$delegate` - The original service instance, which can be monkey patched, configured, + * * `$delegate` - The original service instance, which can be replaced, monkey patched, configured, * decorated or delegated to. * * @example @@ -4435,7 +4770,7 @@ function createInjector(modulesToLoad, strictDi) { var INSTANTIATING = {}, providerSuffix = 'Provider', path = [], - loadedModules = new HashMap([], true), + loadedModules = new NgMap(), providerCache = { $provide: { provider: supportObject(provider), @@ -4451,7 +4786,7 @@ function createInjector(modulesToLoad, strictDi) { if (angular.isString(caller)) { path.push(caller); } - throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); + throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- ')); })), instanceCache = {}, protoInstanceInjector = @@ -4463,6 +4798,7 @@ function createInjector(modulesToLoad, strictDi) { instanceInjector = protoInstanceInjector; providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) }; + instanceInjector.modules = providerInjector.modules = createMap(); var runBlocks = loadModules(modulesToLoad); instanceInjector = protoInstanceInjector.get('$injector'); instanceInjector.strictDi = strictDi; @@ -4490,16 +4826,16 @@ function createInjector(modulesToLoad, strictDi) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { - throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); + throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name); } - return providerCache[name + providerSuffix] = provider_; + return (providerCache[name + providerSuffix] = provider_); } function enforceReturnValue(name, factory) { - return function enforcedReturnValue() { + return /** @this */ function enforcedReturnValue() { var result = instanceInjector.invoke(factory, this); if (isUndefined(result)) { - throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name); + throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name); } return result; }; @@ -4543,7 +4879,7 @@ function createInjector(modulesToLoad, strictDi) { var runBlocks = [], moduleFn; forEach(modulesToLoad, function(module) { if (loadedModules.get(module)) return; - loadedModules.put(module, true); + loadedModules.set(module, true); function runInvokeQueue(queue) { var i, ii; @@ -4558,6 +4894,7 @@ function createInjector(modulesToLoad, strictDi) { try { if (isString(module)) { moduleFn = angularModule(module); + instanceInjector.modules[module] = moduleFn; runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks); @@ -4572,15 +4909,15 @@ function createInjector(modulesToLoad, strictDi) { if (isArray(module)) { module = module[module.length - 1]; } - if (e.message && e.stack && e.stack.indexOf(e.message) == -1) { + if (e.message && e.stack && e.stack.indexOf(e.message) === -1) { // Safari & FF's stack traces don't contain error.message content // unlike those of Chrome and IE // So if stack doesn't contain message, we create a new string that contains both. // Since error.stack is read-only in Safari, I'm overriding e and not e.stack here. - /* jshint -W022 */ + // eslint-disable-next-line no-ex-assign e = e.message + '\n' + e.stack; } - throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", + throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}', module, e.stack || e.message || e); } }); @@ -4604,7 +4941,8 @@ function createInjector(modulesToLoad, strictDi) { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; - return cache[serviceName] = factory(serviceName, caller); + cache[serviceName] = factory(serviceName, caller); + return cache[serviceName]; } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; @@ -4616,48 +4954,71 @@ function createInjector(modulesToLoad, strictDi) { } } - function invoke(fn, self, locals, serviceName) { - if (typeof locals === 'string') { - serviceName = locals; - locals = null; - } + function injectionArgs(fn, locals, serviceName) { var args = [], - $inject = createInjector.$$annotate(fn, strictDi, serviceName), - length, i, - key; + $inject = createInjector.$$annotate(fn, strictDi, serviceName); - for (i = 0, length = $inject.length; i < length; i++) { - key = $inject[i]; + for (var i = 0, length = $inject.length; i < length; i++) { + var key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } - args.push( - locals && locals.hasOwnProperty(key) - ? locals[key] - : getService(key, serviceName) - ); + args.push(locals && locals.hasOwnProperty(key) ? locals[key] : + getService(key, serviceName)); + } + return args; + } + + function isClass(func) { + // Support: IE 9-11 only + // IE 9-11 do not support classes and IE9 leaks with the code below. + if (msie || typeof func !== 'function') { + return false; + } + var result = func.$$ngIsClass; + if (!isBoolean(result)) { + // Support: Edge 12-13 only + // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/ + result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func)); + } + return result; + } + + function invoke(fn, self, locals, serviceName) { + if (typeof locals === 'string') { + serviceName = locals; + locals = null; } + + var args = injectionArgs(fn, locals, serviceName); if (isArray(fn)) { - fn = fn[length]; + fn = fn[fn.length - 1]; } - // http://jsperf.com/angularjs-invoke-apply-vs-switch - // #5388 - return fn.apply(self, args); + if (!isClass(fn)) { + // http://jsperf.com/angularjs-invoke-apply-vs-switch + // #5388 + return fn.apply(self, args); + } else { + args.unshift(null); + return new (Function.prototype.bind.apply(fn, args))(); + } } + function instantiate(Type, locals, serviceName) { // Check if Type is annotated and use just the given function at n-1 as parameter // e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]); - // Object creation: http://jsperf.com/create-constructor/2 - var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null); - var returnedValue = invoke(Type, instance, locals, serviceName); - - return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance; + var ctor = (isArray(Type) ? Type[Type.length - 1] : Type); + var args = injectionArgs(Type, locals, serviceName); + // Empty object at position 0 is ignored for invocation with `new`, but required. + args.unshift(null); + return new (Function.prototype.bind.apply(ctor, args))(); } + return { invoke: invoke, instantiate: instantiate, @@ -4675,6 +5036,7 @@ createInjector.$$annotate = annotate; /** * @ngdoc provider * @name $anchorScrollProvider + * @this * * @description * Use `$anchorScrollProvider` to disable automatic scrolling whenever @@ -4713,7 +5075,7 @@ function $AnchorScrollProvider() { * When called, it scrolls to the element related to the specified `hash` or (if omitted) to the * current value of {@link ng.$location#hash $location.hash()}, according to the rules specified * in the - * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document). + * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-indicated-part-of-the-document). * * It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to * match any anchor whenever it changes. This can be disabled by calling @@ -4746,7 +5108,7 @@ function $AnchorScrollProvider() { * </div> * * @example - <example module="anchorScrollExample"> + <example module="anchorScrollExample" name="anchor-scroll"> <file name="index.html"> <div id="scrollArea" ng-controller="ScrollController"> <a ng-click="gotoBottom()">Go to bottom</a> @@ -4756,7 +5118,7 @@ function $AnchorScrollProvider() { <file name="script.js"> angular.module('anchorScrollExample', []) .controller('ScrollController', ['$scope', '$location', '$anchorScroll', - function ($scope, $location, $anchorScroll) { + function($scope, $location, $anchorScroll) { $scope.gotoBottom = function() { // set the location.hash to the id of // the element you wish to scroll to. @@ -4785,7 +5147,7 @@ function $AnchorScrollProvider() { * See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details. * * @example - <example module="anchorScrollOffsetExample"> + <example module="anchorScrollOffsetExample" name="anchor-scroll-offset"> <file name="index.html"> <div class="fixed-header" ng-controller="headerCtrl"> <a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]"> @@ -4802,7 +5164,7 @@ function $AnchorScrollProvider() { $anchorScroll.yOffset = 50; // always scroll by 50 extra pixels }]) .controller('headerCtrl', ['$anchorScroll', '$location', '$scope', - function ($anchorScroll, $location, $scope) { + function($anchorScroll, $location, $scope) { $scope.gotoAnchor = function(x) { var newHash = 'anchor' + x; if ($location.hash() !== newHash) { @@ -4909,7 +5271,8 @@ function $AnchorScrollProvider() { } function scroll(hash) { - hash = isString(hash) ? hash : $location.hash(); + // Allow numeric hashes + hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash(); var elm; // empty hash, scroll to the top of the page @@ -4921,7 +5284,7 @@ function $AnchorScrollProvider() { // first anchor with given name :-D else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm); - // no element and hash == 'top', scroll to the top of the page + // no element and hash === 'top', scroll to the top of the page else if (hash === 'top') scrollTo(null); } @@ -4996,33 +5359,14 @@ function prepareAnimateOptions(options) { : {}; } -var $$CoreAnimateRunnerProvider = function() { - this.$get = ['$q', '$$rAF', function($q, $$rAF) { - function AnimateRunner() {} - AnimateRunner.all = noop; - AnimateRunner.chain = noop; - AnimateRunner.prototype = { - end: noop, - cancel: noop, - resume: noop, - pause: noop, - complete: noop, - then: function(pass, fail) { - return $q(function(resolve) { - $$rAF(function() { - resolve(); - }); - }).then(pass, fail); - } - }; - return AnimateRunner; - }]; +var $$CoreAnimateJsProvider = /** @this */ function() { + this.$get = noop; }; // this is prefixed with Core since it conflicts with // the animateQueueProvider defined in ngAnimate/animateQueue.js -var $$CoreAnimateQueueProvider = function() { - var postDigestQueue = new HashMap(); +var $$CoreAnimateQueueProvider = /** @this */ function() { + var postDigestQueue = new NgMap(); var postDigestElements = []; this.$get = ['$$AnimateRunner', '$rootScope', @@ -5034,17 +5378,28 @@ var $$CoreAnimateQueueProvider = function() { pin: noop, push: function(element, event, options, domOperation) { - domOperation && domOperation(); + if (domOperation) { + domOperation(); + } options = options || {}; - options.from && element.css(options.from); - options.to && element.css(options.to); + if (options.from) { + element.css(options.from); + } + if (options.to) { + element.css(options.to); + } if (options.addClass || options.removeClass) { addRemoveClassesPostDigest(element, options.addClass, options.removeClass); } - return new $$AnimateRunner(); // jshint ignore:line + var runner = new $$AnimateRunner(); + + // since there are no animations to run the runner needs to be + // notified that the animation call is complete. + runner.complete(); + return runner; } }; @@ -5083,10 +5438,14 @@ var $$CoreAnimateQueueProvider = function() { }); forEach(element, function(elm) { - toAdd && jqLiteAddClass(elm, toAdd); - toRemove && jqLiteRemoveClass(elm, toRemove); + if (toAdd) { + jqLiteAddClass(elm, toAdd); + } + if (toRemove) { + jqLiteRemoveClass(elm, toRemove); + } }); - postDigestQueue.remove(element); + postDigestQueue.delete(element); } }); postDigestElements.length = 0; @@ -5101,7 +5460,7 @@ var $$CoreAnimateQueueProvider = function() { if (classesAdded || classesRemoved) { - postDigestQueue.put(element, data); + postDigestQueue.set(element, data); postDigestElements.push(element); if (postDigestElements.length === 1) { @@ -5124,8 +5483,9 @@ var $$CoreAnimateQueueProvider = function() { * * To see the functional implementation check out `src/ngAnimate/animate.js`. */ -var $AnimateProvider = ['$provide', function($provide) { +var $AnimateProvider = ['$provide', /** @this */ function($provide) { var provider = this; + var classNameFilter = null; this.$$registeredAnimations = Object.create(null); @@ -5170,7 +5530,7 @@ var $AnimateProvider = ['$provide', function($provide) { */ this.register = function(name, factory) { if (name && name.charAt(0) !== '.') { - throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name); + throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name); } var key = name + '-animation'; @@ -5194,16 +5554,16 @@ var $AnimateProvider = ['$provide', function($provide) { */ this.classNameFilter = function(expression) { if (arguments.length === 1) { - this.$$classNameFilter = (expression instanceof RegExp) ? expression : null; - 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); - + classNameFilter = (expression instanceof RegExp) ? expression : null; + if (classNameFilter) { + var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]'); + if (reservedRegex.test(classNameFilter.toString())) { + classNameFilter = null; + throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME); } } } - return this.$$classNameFilter; + return classNameFilter; }; this.$get = ['$$animateQueue', function($$animateQueue) { @@ -5217,7 +5577,11 @@ var $AnimateProvider = ['$provide', function($provide) { afterElement = null; } } - afterElement ? afterElement.after(element) : parentElement.prepend(element); + if (afterElement) { + afterElement.after(element); + } else { + parentElement.prepend(element); + } } /** @@ -5283,15 +5647,20 @@ var $AnimateProvider = ['$provide', function($provide) { * // remove all the animation event listeners listening for `enter` * $animate.off('enter'); * + * // remove listeners for all animation events from the container element + * $animate.off(container); + * * // remove all the animation event listeners listening for `enter` on the given element and its children * $animate.off('enter', container); * - * // remove the event listener function provided by `listenerFn` that is set - * // to listen for `enter` on the given `element` as well as its children + * // remove the event listener function provided by `callback` that is set + * // to listen for `enter` on the given `container` as well as its children * $animate.off('enter', container, callback); * ``` * - * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...) + * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move, + * addClass, removeClass, etc...), or the container element. If it is the element, all other + * arguments are ignored. * @param {DOMElement=} container the container element the event listener was placed on * @param {Function=} callback the callback function that was registered as the listener */ @@ -5355,7 +5724,9 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {Promise} animationPromise The animation promise that is returned when an animation is started. */ cancel: function(runner) { - runner.end && runner.end(); + if (runner.end) { + runner.end(); + } }, /** @@ -5372,7 +5743,13 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {DOMElement} parent the parent element which will append the element as * a child (so long as the after element is not present) * @param {DOMElement=} after the sibling element after which the element will be appended - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5398,7 +5775,13 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {DOMElement} parent the parent element which will append the element as * a child (so long as the after element is not present) * @param {DOMElement=} after the sibling element after which the element will be appended - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5419,7 +5802,13 @@ var $AnimateProvider = ['$provide', function($provide) { * digest once the animation has completed. * * @param {DOMElement} element the element which will be removed from the DOM - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5443,7 +5832,13 @@ var $AnimateProvider = ['$provide', function($provide) { * * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5467,7 +5862,13 @@ var $AnimateProvider = ['$provide', function($provide) { * * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5492,7 +5893,13 @@ var $AnimateProvider = ['$provide', function($provide) { * @param {DOMElement} element the element which the CSS classes will be applied to * @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces) * @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces) - * @param {object=} options an optional collection of options/styles that will be applied to the element + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5509,18 +5916,37 @@ var $AnimateProvider = ['$provide', function($provide) { * @kind function * * @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element. - * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take - * on the provided styles. For example, if a transition animation is set for the given className then the provided from and - * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles - * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter). + * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take + * on the provided styles. For example, if a transition animation is set for the given 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 + * (Note that if no animation is detected then this value will not be applied to the element.) + * @param {object=} options an optional collection of options/styles that will be applied to the element. + * The object can have the following properties: + * + * - **addClass** - `{string}` - space-separated CSS classes to add to element + * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to` + * - **removeClass** - `{string}` - space-separated CSS classes to remove from element + * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from` * * @return {Promise} the animation callback promise */ @@ -5537,10 +5963,201 @@ var $AnimateProvider = ['$provide', function($provide) { }]; }]; +var $$AnimateAsyncRunFactoryProvider = /** @this */ 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) { + if (passed) { + callback(); + } else { + waitForTick(callback); + } + }; + }; + }]; +}; + +var $$AnimateRunnerFactoryProvider = /** @this */ function() { + this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout', + function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $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) { + if ($$isDocumentHidden()) { + timeoutTick(fn); + } else { + rafTick(fn); + } + }; + this._state = 0; + } + + AnimateRunner.prototype = { + setHost: function(host) { + this.host = host || {}; + }, + + done: function(fn) { + if (this._state === DONE_COMPLETE_STATE) { + fn(); + } else { + this._doneCallbacks.push(fn); + } + }, + + progress: noop, + + getPromise: function() { + if (!this.promise) { + var self = this; + this.promise = $q(function(resolve, reject) { + self.done(function(status) { + if (status === false) { + reject(); + } else { + resolve(); + } + }); + }); + } + return this.promise; + }, + + then: function(resolveHandler, rejectHandler) { + return this.getPromise().then(resolveHandler, rejectHandler); + }, + + 'catch': function(handler) { + return this.getPromise()['catch'](handler); + }, + + 'finally': function(handler) { + return this.getPromise()['finally'](handler); + }, + + pause: function() { + if (this.host.pause) { + this.host.pause(); + } + }, + + resume: function() { + if (this.host.resume) { + this.host.resume(); + } + }, + + end: function() { + if (this.host.end) { + this.host.end(); + } + this._resolve(true); + }, + + cancel: function() { + if (this.host.cancel) { + this.host.cancel(); + } + this._resolve(false); + }, + + complete: function(response) { + var self = this; + if (self._state === INITIAL_STATE) { + self._state = DONE_PENDING_STATE; + self._tick(function() { + self._resolve(response); + }); + } + }, + + _resolve: function(response) { + if (this._state !== DONE_COMPLETE_STATE) { + forEach(this._doneCallbacks, function(fn) { + fn(response); + }); + this._doneCallbacks.length = 0; + this._state = DONE_COMPLETE_STATE; + } + } + }; + + return AnimateRunner; + }]; +}; + +/* exported $CoreAnimateCssProvider */ + /** * @ngdoc service * @name $animateCss * @kind object + * @this * * @description * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, @@ -5549,37 +6166,18 @@ var $AnimateProvider = ['$provide', function($provide) { * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. */ var $CoreAnimateCssProvider = function() { - this.$get = ['$$rAF', '$q', function($$rAF, $q) { + this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) { - var RAFPromise = function() {}; - RAFPromise.prototype = { - done: function(cancel) { - this.defer && this.defer[cancel === true ? 'reject' : 'resolve'](); - }, - end: function() { - this.done(); - }, - cancel: function() { - this.done(true); - }, - getPromise: function() { - if (!this.defer) { - this.defer = $q.defer(); - } - return this.defer.promise; - }, - then: function(f1,f2) { - return this.getPromise().then(f1,f2); - }, - 'catch': function(f1) { - return this.getPromise()['catch'](f1); - }, - 'finally': function(f1) { - return this.getPromise()['finally'](f1); + 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); } - }; - return function(element, options) { // there is no point in applying the styles since // there is no animation that goes on at all in // this version of $animateCss. @@ -5592,7 +6190,7 @@ var $CoreAnimateCssProvider = function() { options.from = null; } - var closed, runner = new RAFPromise(); + var closed, runner = new $$AnimateRunner(); return { start: run, end: run @@ -5600,16 +6198,16 @@ var $CoreAnimateCssProvider = function() { function run() { $$rAF(function() { - close(); + applyAnimationContents(); if (!closed) { - runner.done(); + runner.complete(); } closed = true; }); return runner; } - function close() { + function applyAnimationContents() { if (options.addClass) { element.addClass(options.addClass); options.addClass = null; @@ -5652,7 +6250,6 @@ var $CoreAnimateCssProvider = function() { */ function Browser(window, document, $log, $sniffer) { var self = this, - rawDocument = document[0], location = window.location, history = window.history, setTimeout = window.setTimeout, @@ -5715,10 +6312,16 @@ function Browser(window, document, $log, $sniffer) { var cachedState, lastHistoryState, lastBrowserUrl = location.href, baseElement = document.find('base'), - pendingLocation = null; + pendingLocation = null, + getCurrentState = !$sniffer.history ? noop : function getCurrentState() { + try { + return history.state; + } catch (e) { + // MSIE can reportedly throw when there is no state (UNCONFIRMED). + } + }; cacheState(); - lastHistoryState = cachedState; /** * @name $browser#url @@ -5772,10 +6375,8 @@ function Browser(window, document, $log, $sniffer) { if ($sniffer.history && (!sameBase || !sameState)) { history[replace ? 'replaceState' : 'pushState'](state, '', url); cacheState(); - // Do the assignment again so that those two variables are referentially identical. - lastHistoryState = cachedState; } else { - if (!sameBase || pendingLocation) { + if (!sameBase) { pendingLocation = url; } if (replace) { @@ -5789,6 +6390,9 @@ function Browser(window, document, $log, $sniffer) { pendingLocation = url; } } + if (pendingLocation) { + pendingLocation = url; + } return self; // getter } else { @@ -5796,7 +6400,7 @@ function Browser(window, document, $log, $sniffer) { // 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 pendingLocation || location.href.replace(/%27/g,"'"); + return pendingLocation || location.href.replace(/%27/g,'\''); } }; @@ -5819,16 +6423,7 @@ function Browser(window, document, $log, $sniffer) { function cacheStateAndFireUrlChange() { pendingLocation = null; - cacheState(); - fireUrlChange(); - } - - function getCurrentState() { - try { - return history.state; - } catch (e) { - // MSIE can reportedly throw when there is no state (UNCONFIRMED). - } + fireStateOrUrlChange(); } // This variable should be used *only* inside the cacheState function. @@ -5842,11 +6437,16 @@ function Browser(window, document, $log, $sniffer) { if (equals(cachedState, lastCachedState)) { cachedState = lastCachedState; } + lastCachedState = cachedState; + lastHistoryState = cachedState; } - function fireUrlChange() { - if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) { + function fireStateOrUrlChange() { + var prevLastHistoryState = lastHistoryState; + cacheState(); + + if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) { return; } @@ -5881,8 +6481,8 @@ function Browser(window, document, $log, $sniffer) { self.onUrlChange = function(callback) { // TODO(vojta): refactor to use node's syntax for events if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url + // We listen on both (hashchange/popstate) when available, as some browsers don't + // fire popstate when user changes the address bar and don't fire hashchange when url // changed by push/replaceState // html5 history api - popstate event @@ -5912,7 +6512,7 @@ function Browser(window, document, $log, $sniffer) { * Needs to be exported to be able to check for changes that have been done in sync, * as hashchange/popstate events fire in async. */ - self.$$checkUrlChange = fireUrlChange; + self.$$checkUrlChange = fireStateOrUrlChange; ////////////////////////////////////////////////////////////// // Misc API @@ -5929,7 +6529,7 @@ function Browser(window, document, $log, $sniffer) { */ self.baseHref = function() { var href = baseElement.attr('href'); - return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : ''; + return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : ''; }; /** @@ -5980,6 +6580,7 @@ function Browser(window, document, $log, $sniffer) { } +/** @this */ function $BrowserProvider() { this.$get = ['$window', '$log', '$sniffer', '$document', function($window, $log, $sniffer, $document) { @@ -5990,6 +6591,7 @@ function $BrowserProvider() { /** * @ngdoc service * @name $cacheFactory + * @this * * @description * Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to @@ -6026,7 +6628,7 @@ function $BrowserProvider() { * - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. * * @example - <example module="cacheExampleApp"> + <example module="cacheExampleApp" name="cache-factory"> <file name="index.html"> <div ng-controller="CacheController"> <input ng-model="newCacheKey" placeholder="Key"> @@ -6075,7 +6677,7 @@ function $CacheFactoryProvider() { function cacheFactory(cacheId, options) { if (cacheId in caches) { - throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); + throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId); } var size = 0, @@ -6125,7 +6727,7 @@ function $CacheFactoryProvider() { * })); * ``` */ - return caches[cacheId] = { + return (caches[cacheId] = { /** * @ngdoc method @@ -6203,8 +6805,8 @@ function $CacheFactoryProvider() { if (!lruEntry) return; - if (lruEntry == freshEnd) freshEnd = lruEntry.p; - if (lruEntry == staleEnd) staleEnd = lruEntry.n; + if (lruEntry === freshEnd) freshEnd = lruEntry.p; + if (lruEntry === staleEnd) staleEnd = lruEntry.n; link(lruEntry.n,lruEntry.p); delete lruHash[key]; @@ -6269,17 +6871,17 @@ function $CacheFactoryProvider() { info: function() { return extend({}, stats, {size: size}); } - }; + }); /** * makes the `entry` the freshEnd of the LRU linked list */ function refresh(entry) { - if (entry != freshEnd) { + if (entry !== freshEnd) { if (!staleEnd) { staleEnd = entry; - } else if (staleEnd == entry) { + } else if (staleEnd === entry) { staleEnd = entry.n; } @@ -6295,7 +6897,7 @@ function $CacheFactoryProvider() { * bidirectionally links two entries of the LRU linked list */ function link(nextEntry, prevEntry) { - if (nextEntry != prevEntry) { + if (nextEntry !== prevEntry) { if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify } @@ -6343,6 +6945,7 @@ function $CacheFactoryProvider() { /** * @ngdoc service * @name $templateCache + * @this * * @description * The first time a template is used, it is loaded in the template cache for quick retrieval. You @@ -6370,12 +6973,14 @@ function $CacheFactoryProvider() { * }); * ``` * - * To retrieve the template later, simply use it in your HTML: - * ```html - * <div ng-include=" 'templateId.html' "></div> + * To retrieve the template later, simply use it in your component: + * ```js + * myApp.component('myComponent', { + * templateUrl: 'templateId.html' + * }); * ``` * - * or get it via Javascript: + * or get it via the `$templateCache` service: * ```js * $templateCache.get('templateId.html') * ``` @@ -6397,7 +7002,7 @@ function $TemplateCacheProvider() { * * * Does the change somehow allow for arbitrary javascript to be executed? * * Or allows for someone to change the prototype of built-in objects? * - * Or gives undesired access to variables likes document or window? * + * Or gives undesired access to variables like document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ /* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! @@ -6441,8 +7046,9 @@ function $TemplateCacheProvider() { * There are many different options for a directive. * * The difference resides in the return value of the factory function. - * You can either return a "Directive Definition Object" (see below) that defines the directive properties, - * or just the `postLink` function (all other properties will have the default values). + * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)} + * that defines the directive properties, or just the `postLink` function (all other properties will have + * the default values). * * <div class="alert alert-success"> * **Best Practice:** It's recommended to use the "directive definition object" form. @@ -6455,33 +7061,34 @@ function $TemplateCacheProvider() { * * myModule.directive('directiveName', function factory(injectables) { * var directiveDefinitionObject = { - * priority: 0, - * template: '<div></div>', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-priority- priority}: 0, + * {@link $compile#-template- template}: '<div></div>', // or // function(tElement, tAttrs) { ... }, * // or - * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... }, - * transclude: false, - * restrict: 'A', - * templateNamespace: 'html', - * scope: false, - * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, - * controllerAs: 'stringIdentifier', - * bindToController: false, - * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], - * compile: function compile(tElement, tAttrs, transclude) { + * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... }, + * {@link $compile#-transclude- transclude}: false, + * {@link $compile#-restrict- restrict}: 'A', + * {@link $compile#-templatenamespace- templateNamespace}: 'html', + * {@link $compile#-scope- scope}: false, + * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... }, + * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier', + * {@link $compile#-bindtocontroller- bindToController}: false, + * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'], + * {@link $compile#-multielement- multiElement}: false, + * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) { * return { - * pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * post: function postLink(scope, iElement, iAttrs, controller) { ... } + * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * } * // or * // return function postLink( ... ) { ... } * }, * // or - * // link: { - * // pre: function preLink(scope, iElement, iAttrs, controller) { ... }, - * // post: function postLink(scope, iElement, iAttrs, controller) { ... } + * // {@link $compile#-link- link}: { + * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... }, + * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... } * // } * // or - * // link: function postLink( ... ) { ... } + * // {@link $compile#-link- link}: function postLink( ... ) { ... } * }; * return directiveDefinitionObject; * }); @@ -6506,6 +7113,126 @@ function $TemplateCacheProvider() { * }); * ``` * + * ### Life-cycle hooks + * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the + * directive: + * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and + * had their bindings initialized (and before the pre & post linking functions for the directives on + * this element). This is a good place to put initialization code for your controller. + * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The + * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an + * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a + * component such as cloning the bound value to prevent accidental mutation of the outer value. Note that this will + * also be called when your bindings are initialized. + * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on + * changes. Any actions that you wish to take in response to the changes that you detect must be + * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook + * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not + * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments; + * if detecting changes, you must store the previous value(s) for comparison to the current values. + * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing + * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in + * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent + * components will have their `$onDestroy()` hook called before child components. + * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link + * function this hook can be used to set up DOM event handlers and do direct DOM manipulation. + * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since + * they are waiting for their template to load asynchronously and their own compilation and linking has been + * suspended until that occurs. + * + * #### Comparison with Angular 2 life-cycle hooks + * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are + * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2: + * + * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`. + * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor. + * In Angular 2 you can only define hooks on the prototype of the Component class. + * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to + * `ngDoCheck` in Angular 2 + * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be + * propagated throughout the application. + * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an + * error or do nothing depending upon the state of `enableProdMode()`. + * + * #### Life-cycle hook examples + * + * This example shows how you can check for mutations to a Date object even though the identity of the object + * has not changed. + * + * <example name="doCheckDateExample" module="do-check-module"> + * <file name="app.js"> + * angular.module('do-check-module', []) + * .component('app', { + * template: + * 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' + + * 'Date: {{ $ctrl.date }}' + + * '<test date="$ctrl.date"></test>', + * controller: function() { + * this.date = new Date(); + * this.month = this.date.getMonth(); + * this.updateDate = function() { + * this.date.setMonth(this.month); + * }; + * } + * }) + * .component('test', { + * bindings: { date: '<' }, + * template: + * '<pre>{{ $ctrl.log | json }}</pre>', + * controller: function() { + * var previousValue; + * this.log = []; + * this.$doCheck = function() { + * var currentValue = this.date && this.date.valueOf(); + * if (previousValue !== currentValue) { + * this.log.push('doCheck: date mutated: ' + this.date); + * previousValue = currentValue; + * } + * }; + * } + * }); + * </file> + * <file name="index.html"> + * <app></app> + * </file> + * </example> + * + * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the + * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large + * arrays or objects can have a negative impact on your application performance) + * + * <example name="doCheckArrayExample" module="do-check-module"> + * <file name="index.html"> + * <div ng-init="items = []"> + * <button ng-click="items.push(items.length)">Add Item</button> + * <button ng-click="items = []">Reset Items</button> + * <pre>{{ items }}</pre> + * <test items="items"></test> + * </div> + * </file> + * <file name="app.js"> + * angular.module('do-check-module', []) + * .component('test', { + * bindings: { items: '<' }, + * template: + * '<pre>{{ $ctrl.log | json }}</pre>', + * controller: function() { + * this.log = []; + * + * this.$doCheck = function() { + * if (this.items_ref !== this.items) { + * this.log.push('doCheck: items changed'); + * this.items_ref = this.items; + * } + * if (!angular.equals(this.items_clone, this.items)) { + * this.log.push('doCheck: items mutated'); + * this.items_clone = angular.copy(this.items); + * } + * }; + * } + * }); + * </file> + * </example> * * * ### Directive Definition Object @@ -6514,10 +7241,10 @@ function $TemplateCacheProvider() { * compiler}. The attributes are: * * #### `multiElement` - * When this property is set to true, the HTML compiler will collect DOM nodes between + * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between * nodes with the attributes `directive-name-start` and `directive-name-end`, and group them * together as the directive elements. It is recommended that this feature be used on directives - * which are not strictly behavioural (such as {@link ngClick}), and which + * which are not strictly behavioral (such as {@link ngClick}), and which * do not manipulate or replace child nodes (such as {@link ngInclude}). * * #### `priority` @@ -6535,19 +7262,21 @@ function $TemplateCacheProvider() { * and other directives used in the directive's template will also be excluded from execution. * * #### `scope` - * The scope property can be `true`, an object or a falsy value: + * The scope property can be `false`, `true`, or an object: * - * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope. + * * **`false` (default):** No scope will be created for the directive. The directive will use its + * parent's 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. + * only one new scope is created. * - * * **`{...}` (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. + * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template. + * 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. Note that an isolate scope + * directive without a `template` or `templateUrl` will not apply the isolate scope + * to its children elements. * * 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 @@ -6555,35 +7284,62 @@ function $TemplateCacheProvider() { * is bound to the parent scope, via matching attributes on the directive's element: * * * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is - * always a string since DOM attributes are strings. If no `attr` name is specified then the - * attribute name is assumed to be the same as the local name. - * Given `<widget my-attr="hello {{name}}">` and widget definition - * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect - * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the - * `localName` property on the widget scope. The `name` is read from the parent scope (not - * component scope). - * - * * `=` or `=attr` - set up bi-directional binding between a local scope property and the - * parent scope property of name defined via the value of the `attr` attribute. If no `attr` - * name is specified then the attribute name is assumed to be the same as the local name. - * Given `<widget my-attr="parentModel">` and widget definition of - * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the + * always a string since DOM attributes are strings. If no `attr` name is specified then the + * attribute name is assumed to be the same as the local name. Given `<my-component + * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`, + * the directive's scope property `localName` will reflect the interpolated value of `hello + * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's + * scope. The `name` is read from the parent scope (not the directive's scope). + * + * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression + * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope. + * If no `attr` name is specified then the attribute name is assumed to be the same as the local + * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: { + * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the + * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in + * `localModel` and vice versa. Optional attributes should be marked as such with a question mark: + * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't + * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`}) + * will be thrown upon discovering changes to the local value, since it will be impossible to sync + * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`} + * method is used for tracking changes, and the equality check is based on object identity. + * However, if an object literal or an array literal is passed as the binding expression, the + * equality check is done by value (using the {@link angular.equals} function). It's also possible + * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection + * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional). + * + * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an + * expression passed via the attribute `attr`. The expression is evaluated in the context of the + * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the + * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`. + * + * For example, given `<my-component my-attr="parentModel">` and directive definition of + * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the * value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected - * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent - * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You - * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If - * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use - * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional). - * - * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. - * If no `attr` name is specified then the attribute name is assumed to be the same as the - * local name. Given `<widget my-attr="count = count + value">` and widget definition of - * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to - * a function wrapper for the `count = count + value` expression. Often it's desirable to - * pass data from the isolated scope via an expression to the parent scope, this can be - * done by passing a map of local variable names and values into the expression wrapper fn. - * For example, if the expression is `increment(amount)` then we can specify the amount value - * by calling the `localFn` as `localFn({amount: 22})`. + * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however + * two caveats: + * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply + * sets the same value. That means if your bound value is an object, changes to its properties + * in the isolated scope will be reflected in the parent scope (because both reference the same object). + * 2. one-way binding watches changes to the **identity** of the parent value. That means the + * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference + * to the value has changed. In most cases, this should not be of concern, but can be important + * to know if you one-way bind to an object, and then replace that object in the isolated scope. + * If you now change a property of the object in your parent scope, the change will not be + * propagated to the isolated scope, because the identity of the object on the parent scope + * has not changed. Instead you must assign a new object. + * + * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings + * back to the parent. However, it does not make this completely impossible. + * + * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If + * no `attr` name is specified then the attribute name is assumed to be the same as the local name. + * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: { + * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for + * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope + * via an expression to the parent scope. This can be done by passing a map of local variable names + * and values into the expression wrapper fn. For example, if the expression is `increment(amount)` + * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`. * * In general it's possible to apply more than one directive to one element, but there might be limitations * depending on the type of scope required by the directives. The following points will help explain these limitations. @@ -6601,9 +7357,30 @@ function $TemplateCacheProvider() { * * * #### `bindToController` - * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will - * allow a component to have its properties bound to the controller, rather than to scope. When the controller - * is instantiated, the initial values of the isolate scope bindings are already available. + * This property is used to bind scope properties directly to the controller. It can be either + * `true` or an object hash with the same format as the `scope` property. + * + * When an isolate scope is used for a directive (see above), `bindToController: true` will + * allow a component to have its properties bound to the controller, rather than to scope. + * + * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller + * properties. You can access these bindings once they have been initialized by providing a controller method called + * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings + * initialized. + * + * <div class="alert alert-warning"> + * **Deprecation warning:** although bindings for non-ES6 class controllers are currently + * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization + * code that relies upon bindings inside a `$onInit` method on the controller, instead. + * </div> + * + * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property. + * This will set up the scope bindings to the controller directly. Note that `scope` can still be used + * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate + * scope (useful for component directives). + * + * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`. + * * * #### `controller` * Controller constructor function. The controller is instantiated before the @@ -6615,25 +7392,41 @@ function $TemplateCacheProvider() { * * `$element` - Current element * * `$attrs` - Current attributes object for the element * * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope: - * `function([scope], cloneLinkingFn, futureParentElement)`. - * * `scope`: optional argument to override the scope. - * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content. - * * `futureParentElement`: + * `function([scope], cloneLinkingFn, futureParentElement, slotName)`: + * * `scope`: (optional) override the scope. + * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content. + * * `futureParentElement` (optional): * * defines the parent to which the `cloneLinkingFn` will add the cloned elements. * * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`. * * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) - * and when the `cloneLinkinFn` is passed, + * and when the `cloneLinkingFn` is passed, * as those elements need to created and cloned in a special way when they are defined outside their * usual containers (e.g. like `<svg>`). * * See also the `directive.templateNamespace` property. - * + * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`) + * then the default transclusion is provided. + * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns + * `true` if the specified slot contains content (i.e. one or more DOM nodes). * * #### `require` * Require another directive and inject its controller as the fourth argument to the linking function. The - * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the - * injected argument will be an array in corresponding order. If no such directive can be - * found, or if the directive does not have a controller, then an error is raised (unless no link function - * is specified, in which case error checking is skipped). The name can be prefixed with: + * `require` property can be a string, an array or an object: + * * a **string** containing the name of the directive to pass to the linking function + * * an **array** containing the names of directives to pass to the linking function. The argument passed to the + * linking function will be an array of controllers in the same order as the names in the `require` property + * * an **object** whose property values are the names of the directives to pass to the linking function. The argument + * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding + * controllers. + * + * If the `require` property is an object and `bindToController` is truthy, then the required controllers are + * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers + * have been constructed but before `$onInit` is called. + * If the name of the required controller is the same as the local name (the key), the name can be + * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`. + * See the {@link $compileProvider#component} helper for an example of how this can be used. + * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is + * raised (unless no link function is specified and the required controllers are not being bound to the directive + * controller, in which case error checking is skipped). The name can be prefixed with: * * * (no prefix) - Locate the required controller on the current element. Throw an error if not found. * * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found. @@ -6726,14 +7519,6 @@ function $TemplateCacheProvider() { * The contents are compiled and provided to the directive as a **transclusion function**. See the * {@link $compile#transclusion Transclusion} section below. * - * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the - * directive's element or the entire element: - * - * * `true` - transclude the content (i.e. the child nodes) of the directive's element. - * * `'element'` - transclude the whole of the directive's element including any directives on this - * element that defined at a lower priority than this directive. When used, the `template` - * property is ignored. - * * * #### `compile` * @@ -6761,7 +7546,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 @@ -6820,8 +7605,8 @@ function $TemplateCacheProvider() { * any other controller. * * * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope. - * This is the same as the `$transclude` - * parameter of directive controllers, see there for details. + * This is the same as the `$transclude` parameter of directive controllers, + * see {@link ng.$compile#-controller- the controller section for details}. * `function([scope], cloneLinkingFn, futureParentElement)`. * * #### Pre-linking function @@ -6863,6 +7648,34 @@ function $TemplateCacheProvider() { * Testing Transclusion Directives}. * </div> * + * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the + * directive's element, the entire element or multiple parts of the element contents: + * + * * `true` - transclude the content (i.e. the child nodes) of the directive's element. + * * `'element'` - transclude the whole of the directive's element including any directives on this + * element that defined at a lower priority than this directive. When used, the `template` + * property is ignored. + * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template. + * + * **Mult-slot transclusion** is declared by providing an object for the `transclude` property. + * + * This object is a map where the keys are the name of the slot to fill and the value is an element selector + * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`) + * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc). + * + * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives} + * + * If the element selector is prefixed with a `?` then that slot is optional. + * + * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `<my-custom-element>` elements to + * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive. + * + * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements + * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call + * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and + * injectable into the directive's controller. + * + * * #### Transclusion Functions * * When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion @@ -6880,10 +7693,10 @@ function $TemplateCacheProvider() { * * When you call a transclusion function you can pass in a **clone attach function**. This function accepts * two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded - * content and the `scope` is the newly created transclusion scope, to which the clone is bound. + * content and the `scope` is the newly created transclusion scope, which the clone will be linked to. * * <div class="alert alert-info"> - * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function + * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function * since you then get a fresh clone of the original DOM and also have access to the new transclusion scope. * </div> * @@ -6915,7 +7728,7 @@ function $TemplateCacheProvider() { * </div> * * The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat} - * automatically destroy their transluded clones as necessary so you do not need to worry about this if + * automatically destroy their transcluded clones as necessary so you do not need to worry about this if * you are simply using {@link ngTransclude} to inject the transclusion into your directive. * * @@ -6940,19 +7753,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 @@ -6960,10 +7773,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 @@ -6999,7 +7811,7 @@ function $TemplateCacheProvider() { * to illustrate how `$compile` works. * </div> * - <example module="compileExample"> + <example module="compileExample" name="compile"> <file name="index.html"> <script> angular.module('compileExample', [], function($compileProvider) { @@ -7084,8 +7896,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. @@ -7121,10 +7940,23 @@ function $TemplateCacheProvider() { * * For information on how the compiler works, see the * {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. + * + * @knownIssue + * + * ### Double Compilation + * + Double compilation occurs when an already compiled part of the DOM gets + compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues, + and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it + section on double compilation} for an in-depth explanation and ways to avoid it. + * */ var $compileMinErr = minErr('$compile'); +function UNINITIALIZED_VALUE() {} +var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE(); + /** * @ngdoc provider * @name $compileProvider @@ -7132,11 +7964,12 @@ var $compileMinErr = minErr('$compile'); * @description */ $CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +/** @this */ function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', - COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/, + COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/, + CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/, ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'), REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/; @@ -7144,22 +7977,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, isController) { - var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/; + var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/; - var bindings = {}; + var bindings = createMap(); forEach(scope, function(definition, scopeName) { + if (definition in bindingCache) { + bindings[scopeName] = bindingCache[definition]; + return; + } var match = definition.match(LOCAL_REGEXP); if (!match) { throw $compileMinErr('iscp', - "Invalid {3} for directive '{0}'." + - " Definition: {... {1}: '{2}' ...}", + 'Invalid {3} for directive \'{0}\'.' + + ' Definition: {... {1}: \'{2}\' ...}', directiveName, scopeName, definition, - (isController ? "controller bindings definition" : - "isolate scope definition")); + (isController ? 'controller bindings definition' : + 'isolate scope definition')); } bindings[scopeName] = { @@ -7168,6 +8006,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { optional: match[3] === '?', attrName: match[4] || scopeName }; + if (match[4]) { + bindingCache[definition] = bindings[scopeName]; + } }); return bindings; @@ -7192,20 +8033,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { 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); - } + if (bindings.bindToController && !directive.controller) { + // There is no controller + throw $compileMinErr('noctrl', + 'Cannot bind to controller without directive \'{0}\'s controller.', + directiveName); } return bindings; } @@ -7213,15 +8045,40 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function assertValidDirectiveName(name) { var letter = name.charAt(0); if (!letter || letter !== lowercase(letter)) { - throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name); + throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name); } if (name !== name.trim()) { throw $compileMinErr('baddir', - "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces", + 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces', name); } } + function getDirectiveRequire(directive) { + var require = directive.require || (directive.controller && directive.name); + + if (!isArray(require) && isObject(require)) { + forEach(require, function(value, key) { + var match = value.match(REQUIRE_PREFIX_REGEXP); + var name = value.substring(match[0].length); + if (!name) require[key] = match[0] + key; + }); + } + + return require; + } + + function getDirectiveRestrict(restrict, name) { + if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) { + throw $compileMinErr('badrestrict', + 'Restrict property \'{0}\' of directive \'{1}\' is invalid', + restrict, + name); + } + + return restrict || 'EA'; + } + /** * @ngdoc method * @name $compileProvider#directive @@ -7233,11 +8090,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which * will match as <code>ng-bind</code>), or an object map of directives where the keys are the * names and the values are the factories. - * @param {Function|Array} directiveFactory An injectable directive factory function. See - * {@link guide/directive} for more info. + * @param {Function|Array} directiveFactory An injectable directive factory function. See the + * {@link guide/directive directive guide} and the {@link $compile compile API} for more info. * @returns {ng.$compileProvider} Self for chaining. */ - this.directive = function registerDirective(name, directiveFactory) { + this.directive = function registerDirective(name, directiveFactory) { + assertArg(name, 'name'); assertNotHasOwnProperty(name, 'directive'); if (isString(name)) { assertValidDirectiveName(name); @@ -7258,13 +8116,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { directive.priority = directive.priority || 0; directive.index = index; directive.name = directive.name || name; - directive.require = directive.require || (directive.controller && directive.name); - directive.restrict = directive.restrict || 'EA'; - var bindings = directive.$$bindings = - parseDirectiveBindings(directive, directive.name); - if (isObject(bindings.isolateScope)) { - directive.$$isolateBindings = bindings.isolateScope; - } + directive.require = getDirectiveRequire(directive); + directive.restrict = getDirectiveRestrict(directive.restrict, name); directive.$$moduleName = directiveFactory.$$moduleName; directives.push(directive); } catch (e) { @@ -7281,6 +8134,147 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return this; }; + /** + * @ngdoc method + * @name $compileProvider#component + * @module ng + * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`) + * @param {Object} options Component definition object (a simplified + * {@link ng.$compile#directive-definition-object directive definition object}), + * with the following properties (all optional): + * + * - `controller` – `{(string|function()=}` – controller constructor function that should be + * associated with newly created scope or the name of a {@link ng.$compile#-controller- + * registered controller} if passed as a string. An empty `noop` function by default. + * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope. + * If present, the controller will be published to scope under the `controllerAs` name. + * If not present, this will default to be `$ctrl`. + * - `template` – `{string=|function()=}` – html template as a string or a function that + * returns an html template as a string which should be used as the contents of this component. + * Empty string by default. + * + * If `template` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html + * template that should be used as the contents of this component. + * + * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with + * the following locals: + * + * - `$element` - Current element + * - `$attrs` - Current attributes object for the element + * + * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties. + * Component properties are always bound to the component controller and not to the scope. + * See {@link ng.$compile#-bindtocontroller- `bindToController`}. + * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled. + * Disabled by default. + * - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to + * this component's controller. The object keys specify the property names under which the required + * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}. + * - `$...` – additional properties to attach to the directive factory function and the controller + * constructor function. (This is used by the component router to annotate) + * + * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls. + * @description + * Register a **component definition** with the compiler. This is a shorthand for registering a special + * type of directive, which represents a self-contained UI component in your application. Such components + * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`). + * + * Component definitions are very simple and do not require as much configuration as defining general + * directives. Component definitions usually consist only of a template and a controller backing it. + * + * In order to make the definition easier, components enforce best practices like use of `controllerAs`, + * `bindToController`. They always have **isolate scope** and are restricted to elements. + * + * Here are a few examples of how you would usually define components: + * + * ```js + * var myMod = angular.module(...); + * myMod.component('myComp', { + * template: '<div>My name is {{$ctrl.name}}</div>', + * controller: function() { + * this.name = 'shahar'; + * } + * }); + * + * myMod.component('myComp', { + * template: '<div>My name is {{$ctrl.name}}</div>', + * bindings: {name: '@'} + * }); + * + * myMod.component('myComp', { + * templateUrl: 'views/my-comp.html', + * controller: 'MyCtrl', + * controllerAs: 'ctrl', + * bindings: {name: '@'} + * }); + * + * ``` + * For more examples, and an in-depth guide, see the {@link guide/component component guide}. + * + * <br /> + * See also {@link ng.$compileProvider#directive $compileProvider.directive()}. + */ + this.component = function registerComponent(name, options) { + var controller = options.controller || function() {}; + + function factory($injector) { + function makeInjectable(fn) { + if (isFunction(fn) || isArray(fn)) { + return /** @this */ function(tElement, tAttrs) { + return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs}); + }; + } else { + return fn; + } + } + + var template = (!options.template && !options.templateUrl ? '' : options.template); + var ddo = { + controller: controller, + controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl', + template: makeInjectable(template), + templateUrl: makeInjectable(options.templateUrl), + transclude: options.transclude, + scope: {}, + bindToController: options.bindings || {}, + restrict: 'E', + require: options.require + }; + + // Copy annotations (starting with $) over to the DDO + forEach(options, function(val, key) { + if (key.charAt(0) === '$') ddo[key] = val; + }); + + return ddo; + } + + // TODO(pete) remove the following `forEach` before we release 1.6.0 + // The component-router@0.2.0 looks for the annotations on the controller constructor + // Nothing in Angular looks for annotations on the factory function but we can't remove + // it from 1.5.x yet. + + // Copy any annotation properties (starting with $) over to the factory and controller constructor functions + // These could be used by libraries such as the new component router + forEach(options, function(val, key) { + if (key.charAt(0) === '$') { + factory[key] = val; + // Don't try to copy over annotations to named controller + if (isFunction(controller)) controller[key] = val; + } + }); + + factory.$inject = ['$injector']; + + return this.directive(name, factory); + }; + /** * @ngdoc method @@ -7372,13 +8366,172 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return debugInfoEnabled; }; + /** + * @ngdoc method + * @name $compileProvider#preAssignBindingsEnabled + * + * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the + * current preAssignBindingsEnabled state + * @returns {*} current value if used as getter or itself (chaining) if used as setter + * + * @kind function + * + * @description + * Call this method to enable/disable whether directive controllers are assigned bindings before + * calling the controller's constructor. + * If enabled (true), the compiler assigns the value of each of the bindings to the + * properties of the controller object before the constructor of this object is called. + * + * If disabled (false), the compiler calls the constructor first before assigning bindings. + * + * The default value is true in Angular 1.5.x but will switch to false in Angular 1.6.x. + */ + var preAssignBindingsEnabled = false; + this.preAssignBindingsEnabled = function(enabled) { + if (isDefined(enabled)) { + preAssignBindingsEnabled = enabled; + return this; + } + return preAssignBindingsEnabled; + }; + + + var TTL = 10; + /** + * @ngdoc method + * @name $compileProvider#onChangesTtl + * @description + * + * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and + * assuming that the model is unstable. + * + * The current default is 10 iterations. + * + * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result + * in several iterations of calls to these hooks. However if an application needs more than the default 10 + * iterations to stabilize then you should investigate what is causing the model to continuously change during + * the `$onChanges` hook execution. + * + * Increasing the TTL could have performance implications, so you should not change it without proper justification. + * + * @param {number} limit The number of `$onChanges` hook iterations. + * @returns {number|object} the current limit (or `this` if called as a setter for chaining) + */ + this.onChangesTtl = function(value) { + if (arguments.length) { + TTL = value; + return this; + } + return TTL; + }; + + var commentDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#commentDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on comments should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on comments for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check comments when looking for directives. + * This should however only be used if you are sure that no comment directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on comments + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.commentDirectivesEnabled = function(value) { + if (arguments.length) { + commentDirectivesEnabledConfig = value; + return this; + } + return commentDirectivesEnabledConfig; + }; + + + var cssClassDirectivesEnabledConfig = true; + /** + * @ngdoc method + * @name $compileProvider#cssClassDirectivesEnabled + * @description + * + * It indicates to the compiler + * whether or not directives on element classes should be compiled. + * Defaults to `true`. + * + * Calling this function with false disables the compilation of directives + * on element classes for the whole application. + * This results in a compilation performance gain, + * as the compiler doesn't have to check element classes when looking for directives. + * This should however only be used if you are sure that no class directives are used in + * the application (including any 3rd party directives). + * + * @param {boolean} enabled `false` if the compiler may ignore directives on element classes + * @returns {boolean|object} the current value (or `this` if called as a setter for chaining) + */ + this.cssClassDirectivesEnabled = function(value) { + if (arguments.length) { + cssClassDirectivesEnabledConfig = value; + return this; + } + return cssClassDirectivesEnabledConfig; + }; + this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', + '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse, - $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { + $controller, $rootScope, $sce, $animate, $$sanitizeUri) { + + var SIMPLE_ATTR_NAME = /^\w/; + var specialAttrHolder = window.document.createElement('div'); + + + var commentDirectivesEnabled = commentDirectivesEnabledConfig; + var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig; + + + var onChangesTtl = TTL; + // The onChanges hooks should all be run together in a single digest + // When changes occur, the call to trigger their hooks will be added to this queue + var onChangesQueue; + + // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest + function flushOnChangesQueue() { + try { + if (!(--onChangesTtl)) { + // We have hit the TTL limit so reset everything + onChangesQueue = undefined; + throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL); + } + // We must run this hook in an apply since the $$postDigest runs outside apply + $rootScope.$apply(function() { + var errors = []; + for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) { + try { + onChangesQueue[i](); + } catch (e) { + errors.push(e); + } + } + // Reset the queue to trigger a new schedule next time there is a change + onChangesQueue = undefined; + if (errors.length) { + throw errors; + } + }); + } finally { + onChangesTtl++; + } + } + - var Attributes = function(element, attributesToCopy) { + function Attributes(element, attributesToCopy) { if (attributesToCopy) { var keys = Object.keys(attributesToCopy); var i, l, key; @@ -7392,7 +8545,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } this.$$element = element; - }; + } Attributes.prototype = { /** @@ -7517,9 +8670,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { (nodeName === 'img' && key === 'src')) { // sanitize a[href] and img[src] values this[key] = value = $$sanitizeUri(value, key === 'src'); - } else if (nodeName === 'img' && key === 'srcset') { + } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) { // sanitize img[srcset] values - var result = ""; + var result = ''; // first check if there are spaces because it's not the same pattern var trimmedSrcset = trim(value); @@ -7537,7 +8690,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // sanitize the uri result += $$sanitizeUri(trim(rawUris[innerIdx]), true); // add the descriptor - result += (" " + trim(rawUris[innerIdx + 1])); + result += (' ' + trim(rawUris[innerIdx + 1])); } // split the last item into uri and descriptor @@ -7548,7 +8701,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // and add the last descriptor if any if (lastTuple.length === 2) { - result += (" " + trim(lastTuple[1])); + result += (' ' + trim(lastTuple[1])); } this[key] = value = result; } @@ -7557,19 +8710,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (value === null || isUndefined(value)) { this.$$element.removeAttr(attrName); } else { - this.$$element.attr(attrName, value); + if (SIMPLE_ATTR_NAME.test(attrName)) { + this.$$element.attr(attrName, value); + } else { + setSpecialAttr(this.$$element[0], attrName, value); + } } } // fire observers var $$observers = this.$$observers; - $$observers && forEach($$observers[observer], function(fn) { - try { - fn(value); - } catch (e) { - $exceptionHandler(e); - } - }); + if ($$observers) { + forEach($$observers[observer], function(fn) { + try { + fn(value); + } catch (e) { + $exceptionHandler(e); + } + }); + } }, @@ -7588,7 +8747,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) { @@ -7610,6 +8770,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }; + function setSpecialAttr(element, attrName, value) { + // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute` + // so we have to jump through some hoops to get such an attribute + // https://github.com/angular/angular.js/pull/13318 + specialAttrHolder.innerHTML = '<span ' + attrName + '>'; + var attributes = specialAttrHolder.firstChild.attributes; + var attribute = attributes[0]; + // We have to remove the attribute from its container element before we can add it to the destination element + attributes.removeNamedItem(attribute.name); + attribute.value = value; + element.attributes.setNamedItem(attribute); + } function safeAddClass($element, className) { try { @@ -7623,7 +8795,7 @@ 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); @@ -7656,6 +8828,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope'); } : noop; + compile.$$createComment = function(directiveName, comment) { + var content = ''; + if (debugInfoEnabled) { + content = ' ' + (directiveName || '') + ': '; + if (comment) content += comment + ' '; + } + return window.document.createComment(content); + }; + return compile; //================================ @@ -7667,19 +8848,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // modify it. $compileNodes = jqLite($compileNodes); } - // 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]; - } - }); var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); compile.$$addScopeClass($compileNodes); var namespace = null; return function publicLinkFn(scope, cloneConnectFn, options) { + if (!$compileNodes) { + throw $compileMinErr('multilink', 'This element has already been linked.'); + } assertArg(scope, 'scope'); if (previousCompileContext && previousCompileContext.needsNewScope) { @@ -7734,6 +8911,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (cloneConnectFn) cloneConnectFn($linkNode, scope); if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn); + + if (!cloneConnectFn) { + $compileNodes = compositeLinkFn = null; + } return $linkNode; }; } @@ -7744,7 +8925,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (!node) { return 'html'; } else { - return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html'; + return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html'; } } @@ -7766,12 +8947,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { var linkFns = [], + // `nodeList` can be either an element's `.childNodes` (live NodeList) + // or a jqLite/jQuery collection or an array + notLiveList = isArray(nodeList) || (nodeList instanceof jqLite), attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound; + for (var i = 0; i < nodeList.length; i++) { attrs = new Attributes(); - // we must always refer to nodeList[i] since the nodes can be replaced underneath us. + // Support: IE 11 only + // Workaround for #11781 and #14924 + if (msie === 11) { + mergeConsecutiveTextNodes(nodeList, i, notLiveList); + } + + // We must always refer to `nodeList[i]` hereafter, + // since the nodes can be replaced underneath us. directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined, ignoreDirective); @@ -7818,7 +9010,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { stableNodeList = new Array(nodeListLength); // create a sparse array by only copying the elements which have a linkFn - for (i = 0; i < linkFns.length; i+=3) { + for (i = 0; i < linkFns.length; i += 3) { idx = linkFns[i]; stableNodeList[idx] = nodeList[idx]; } @@ -7862,9 +9054,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) { + var node = nodeList[idx]; + var parent = node.parentNode; + var sibling; + + if (node.nodeType !== NODE_TYPE_TEXT) { + return; + } + + while (true) { + sibling = parent ? node.nextSibling : nodeList[idx + 1]; + if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) { + break; + } + + node.nodeValue = node.nodeValue + sibling.nodeValue; + + if (sibling.parentNode) { + sibling.parentNode.removeChild(sibling); + } + if (notLiveList && sibling === nodeList[idx + 1]) { + nodeList.splice(idx + 1, 1); + } + } + } - var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { + function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) { + function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) { if (!transcludedScope) { transcludedScope = scope.$new(false, containingScope); @@ -7876,13 +9093,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { transcludeControllers: controllers, futureParentElement: futureParentElement }); - }; + } // We need to attach the transclusion slots onto the `boundTranscludeFn` // so that they are available inside the `controllersBoundTransclude` function var boundSlots = boundTranscludeFn.$$slots = createMap(); for (var slotName in transcludeFn.$$slots) { - boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + if (transcludeFn.$$slots[slotName]) { + boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn); + } else { + boundSlots[slotName] = null; + } } return boundTranscludeFn; @@ -7902,13 +9123,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var nodeType = node.nodeType, attrsMap = attrs.$attr, match, + nodeName, className; switch (nodeType) { case NODE_TYPE_ELEMENT: /* Element */ + + nodeName = nodeName_(node); + // use the node name: <directive> addDirective(directives, - directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective); + directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective); // iterate over the attributes for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes, @@ -7918,11 +9143,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attr = nAttrs[j]; name = attr.name; - value = trim(attr.value); + value = attr.value; // support ngAttr attribute binding ngAttrName = directiveNormalize(name); - if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) { + isNgAttr = NG_ATTR_BINDING.test(ngAttrName); + if (isNgAttr) { name = name.replace(PREFIX_REGEXP, '') .substr(8).replace(/_(.)/g, function(match, letter) { return letter.toUpperCase(); @@ -7949,14 +9175,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { attrEndName); } + if (nodeName === 'input' && node.getAttribute('type') === 'hidden') { + // Hidden input elements can have strange behaviour when navigating back to the page + // This tells the browser not to try to cache and reinstate previous values + node.setAttribute('autocomplete', 'off'); + } + // use class as directive + if (!cssClassDirectivesEnabled) break; className = node.className; if (isObject(className)) { // Maybe SVGAnimatedString className = className.animVal; } if (isString(className) && className !== '') { - while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { + while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) { nName = directiveNormalize(match[2]); if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { attrs[nName] = trim(match[3]); @@ -7966,29 +9199,11 @@ 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 */ - try { - match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); - if (match) { - nName = directiveNormalize(match[1]); - if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { - attrs[nName] = trim(match[2]); - } - } - } catch (e) { - // turns out that under some circumstances IE9 throws errors when one attempts to read - // comment's node value. - // Just ignore it and continue. (Can't seem to reproduce in test case.) - } + if (!commentDirectivesEnabled) break; + collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective); break; } @@ -7996,8 +9211,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { return directives; } + function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) { + // function created because of performance, try/catch disables + // the optimization of the whole function #14848 + try { + var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue); + if (match) { + var nName = directiveNormalize(match[1]); + if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) { + attrs[nName] = trim(match[2]); + } + } + } catch (e) { + // turns out that under some circumstances IE9 throws errors when one attempts to read + // comment's node value. + // Just ignore it and continue. (Can't seem to reproduce in test case.) + } + } + /** - * Given a node with an directive-start it collects all of the siblings until it finds + * Given a node with a directive-start it collects all of the siblings until it finds * directive-end. * @param node * @param attrStart @@ -8011,10 +9244,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { do { if (!node) { throw $compileMinErr('uterdir', - "Unterminated attribute, found '{0}' but no matching '{1}' found.", + 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.', attrStart, attrEnd); } - if (node.nodeType == NODE_TYPE_ELEMENT) { + if (node.nodeType === NODE_TYPE_ELEMENT) { if (node.hasAttribute(attrStart)) depth++; if (node.hasAttribute(attrEnd)) depth--; } @@ -8037,7 +9270,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} */ function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { - return function(scope, element, attrs, controllers, transcludeFn) { + return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) { element = groupScan(element[0], attrStart, attrEnd); return linkFn(scope, element, attrs, controllers, transcludeFn); }; @@ -8055,23 +9288,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { * @returns {Function} */ function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { - if (eager) { - return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); - } - - var compiled; + var compiled; - return function() { - if (!compiled) { - compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); - - // Null out all of these references in order to make them eligible for garbage collection - // since this is a potentially long lived closure - $compileNodes = transcludeFn = previousCompileContext = null; - } + if (eager) { + return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); + } + return /** @this */ function lazyCompilation() { + if (!compiled) { + compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext); - return compiled.apply(this, arguments); - }; + // Null out all of these references in order to make them eligible for garbage collection + // since this is a potentially long lived closure + $compileNodes = transcludeFn = previousCompileContext = null; + } + return compiled.apply(this, arguments); + }; } /** @@ -8138,7 +9369,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { break; // prevent further processing of directives } - if (directiveValue = directive.scope) { + directiveValue = directive.scope; + + if (directiveValue) { // skip the check for directives with async templates, we'll check the derived sync // directive when the template arrives @@ -8172,7 +9405,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { || (directive.transclude && !directive.$$tlb))) { var candidateDirective; - for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) { + for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) { if ((candidateDirective.transclude && !candidateDirective.$$tlb) || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) { mightHaveMultipleTransclusionError = true; @@ -8184,14 +9417,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } if (!directive.templateUrl && directive.controller) { - directiveValue = directive.controller; controllerDirectives = controllerDirectives || createMap(); - assertNoDuplicate("'" + directiveName + "' controller", + assertNoDuplicate('\'' + directiveName + '\' controller', controllerDirectives[directiveName], directive, $compileNode); controllerDirectives[directiveName] = directive; } - if (directiveValue = directive.transclude) { + directiveValue = directive.transclude; + + if (directiveValue) { hasTranscludeDirective = true; // Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion. @@ -8202,16 +9436,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { nonTlbTranscludeDirective = directive; } - if (directiveValue == 'element') { + if (directiveValue === 'element') { hasElementTranscludeDirective = true; terminalPriority = directive.priority; $template = $compileNode; $compileNode = templateAttrs.$$element = - jqLite(document.createComment(' ' + directiveName + ': ' + - templateAttrs[directiveName] + ' ')); + jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName])); compileNode = $compileNode[0]; replaceWith(jqCollection, sliceArgs($template), compileNode); + // Support: Chrome < 50 + // https://github.com/angular/angular.js/issues/14041 + + // In the versions of V8 prior to Chrome 50, the document fragment that is created + // in the `replaceWith` function is improperly garbage collected despite still + // being referenced by the `parentNode` property of all of the child nodes. By adding + // a reference to the fragment via a different property, we can avoid that incorrect + // behavior. + // TODO: remove this line after Chrome 50 has been released + $template[0].$$parentNode = $template[0].parentNode; + childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority, replaceDirective && replaceDirective.name, { // Don't pass in: @@ -8226,32 +9470,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else { var slots = createMap(); - $template = jqLite(jqLiteClone(compileNode)).contents(); - if (isObject(directiveValue)) { + if (!isObject(directiveValue)) { + $template = jqLite(jqLiteClone(compileNode)).contents(); + } else { - // We have transclusion slots - collect them up and compile them and store their - // transclusion functions + // We have transclusion slots, + // collect them up, compile them and store their transclusion functions $template = []; - var slotNames = createMap(); + + var slotMap = createMap(); var filledSlots = createMap(); - // Parse the slot names: if they start with a ? then they are optional - forEach(directiveValue, function(slotName, key) { - var optional = (slotName.charAt(0) === '?'); - slotName = optional ? slotName.substring(1) : slotName; - slotNames[key] = slotName; - slots[slotName] = []; + // Parse the element selectors + forEach(directiveValue, function(elementSelector, slotName) { + // If an element selector starts with a ? then it is optional + var optional = (elementSelector.charAt(0) === '?'); + elementSelector = optional ? elementSelector.substring(1) : elementSelector; + + slotMap[elementSelector] = slotName; + + // We explicitly assign `null` since this implies that a slot was defined but not filled. + // Later when calling boundTransclusion functions with a slot name we only error if the + // slot is `undefined` + slots[slotName] = null; + // filledSlots contains `true` for all slots that are either optional or have been // filled. This is used to check that we have not missed any required slots filledSlots[slotName] = optional; }); // Add the matching elements into their slot - forEach($compileNode.children(), function(node) { - var slotName = slotNames[directiveNormalize(nodeName_(node))]; + forEach($compileNode.contents(), function(node) { + var slotName = slotMap[directiveNormalize(nodeName_(node))]; if (slotName) { filledSlots[slotName] = true; + slots[slotName] = slots[slotName] || []; slots[slotName].push(node); } else { $template.push(node); @@ -8265,9 +9519,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - forEach(Object.keys(slots), function(slotName) { - slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn); - }); + for (var slotName in slots) { + if (slots[slotName]) { + // Only define a transclusion function if the slot was filled + slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn); + } + } } $compileNode.empty(); // clear contents @@ -8297,9 +9554,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', directiveName, ''); } @@ -8339,6 +9596,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { replaceDirective = directive; } + // eslint-disable-next-line no-func-assign nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, { controllerDirectives: controllerDirectives, @@ -8351,10 +9609,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } else if (directive.compile) { try { linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); + var context = directive.$$originalDirective || directive; if (isFunction(linkFn)) { - addLinkFns(null, linkFn, attrStart, attrEnd); + addLinkFns(null, bind(context, linkFn), attrStart, attrEnd); } else if (linkFn) { - addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); + addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd); } } catch (e) { $exceptionHandler(e, startingTag($compileNode)); @@ -8401,80 +9660,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } } - - function getControllers(directiveName, require, $element, elementControllers) { - var value; - - if (isString(require)) { - var match = require.match(REQUIRE_PREFIX_REGEXP); - var name = require.substring(match[0].length); - var inheritType = match[1] || match[3]; - var optional = match[2] === '?'; - - //If only parents then start at the parent element - if (inheritType === '^^') { - $element = $element.parent(); - //Otherwise attempt getting the controller from elementControllers in case - //the element is transcluded (and has no data) and to avoid .data if possible - } else { - value = elementControllers && elementControllers[name]; - value = value && value.instance; - } - - if (!value) { - var dataName = '$' + name + 'Controller'; - value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); - } - - if (!value && !optional) { - throw $compileMinErr('ctreq', - "Controller '{0}', required by directive '{1}', can't be found!", - name, directiveName); - } - } else if (isArray(require)) { - value = []; - for (var i = 0, ii = require.length; i < ii; i++) { - value[i] = getControllers(directiveName, require[i], $element, elementControllers); - } - } - - return value || null; - } - - function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) { - var elementControllers = createMap(); - for (var controllerKey in controllerDirectives) { - var directive = controllerDirectives[controllerKey]; - var locals = { - $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, - $element: $element, - $attrs: attrs, - $transclude: transcludeFn - }; - - var controller = directive.controller; - if (controller == '@') { - controller = attrs[directive.name]; - } - - var controllerInstance = $controller(controller, locals, true, directive.controllerAs); - - // For directives with element transclusion the element is a comment, - // but jQuery .data doesn't support attaching data to comment nodes as it's hard to - // clean up (http://bugs.jquery.com/ticket/8335). - // Instead, we save the controllers for the element in a local hash and attach to .data - // later, once we have the actual element. - elementControllers[directive.name] = controllerInstance; - if (!hasElementTranscludeDirective) { - $element.data('$' + directive.name + 'Controller', controllerInstance.instance); - } - } - return elementControllers; - } - function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { - var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, - attrs, removeScopeBindingWatches, removeControllerBindingWatches; + var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element, + attrs, scopeBindingInfo; if (compileNode === linkNode) { attrs = templateAttrs; @@ -8496,10 +9684,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // is later passed as `parentBoundTranscludeFn` to `publicLinkFn` transcludeFn = controllersBoundTransclude; transcludeFn.$$boundTransclude = boundTranscludeFn; + // expose the slots on the `$transclude` function + transcludeFn.isSlotFilled = function(slotName) { + return !!boundTranscludeFn.$$slots[slotName]; + }; } if (controllerDirectives) { - elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope); + elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective); } if (newIsolateScopeDirective) { @@ -8509,11 +9701,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { compile.$$addScopeClass($element, true); isolateScope.$$isolateBindings = newIsolateScopeDirective.$$isolateBindings; - removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope, + scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope, isolateScope.$$isolateBindings, newIsolateScopeDirective); - if (removeScopeBindingWatches) { - isolateScope.$on('$destroy', removeScopeBindingWatches); + if (scopeBindingInfo.removeWatches) { + isolateScope.$on('$destroy', scopeBindingInfo.removeWatches); } } @@ -8523,23 +9715,70 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { var controller = elementControllers[name]; var bindings = controllerDirective.$$bindings.bindToController; - if (controller.identifier && bindings) { - removeControllerBindingWatches = - initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); - } + if (preAssignBindingsEnabled) { + if (bindings) { + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } else { + controller.bindingInfo = {}; + } - 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 = + 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); + if (controller.bindingInfo.removeWatches) { + controller.bindingInfo.removeWatches(); + } + controller.bindingInfo = + initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); + } + } else { + controller.instance = controller(); + $element.data('$' + controllerDirective.name + 'Controller', controller.instance); + controller.bindingInfo = initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective); } } + // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy + forEach(controllerDirectives, function(controllerDirective, name) { + var require = controllerDirective.require; + if (controllerDirective.bindToController && !isArray(require) && isObject(require)) { + extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers)); + } + }); + + // Handle the init and destroy lifecycle hooks on all controllers that have them + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$onChanges)) { + try { + controllerInstance.$onChanges(controller.bindingInfo.initialChanges); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$onInit)) { + try { + controllerInstance.$onInit(); + } catch (e) { + $exceptionHandler(e); + } + } + if (isFunction(controllerInstance.$doCheck)) { + controllerScope.$watch(function() { controllerInstance.$doCheck(); }); + controllerInstance.$doCheck(); + } + if (isFunction(controllerInstance.$onDestroy)) { + controllerScope.$on('$destroy', function callOnDestroyHook() { + controllerInstance.$onDestroy(); + }); + } + }); + // PRELINKING for (i = 0, ii = preLinkFns.length; i < ii; i++) { linkFn = preLinkFns[i]; @@ -8559,7 +9798,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) { scopeToChild = isolateScope; } - childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + if (childLinkFn) { + childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn); + } // POSTLINKING for (i = postLinkFns.length - 1; i >= 0; i--) { @@ -8573,6 +9814,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { ); } + // Trigger $postLink lifecycle hooks + forEach(elementControllers, function(controller) { + var controllerInstance = controller.instance; + if (isFunction(controllerInstance.$postLink)) { + controllerInstance.$postLink(); + } + }); + // This is the function that is injected as `$transclude`. // Note: all arguments are optional! function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) { @@ -8592,20 +9841,98 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element; } if (slotName) { + // slotTranscludeFn can be one of three things: + // * a transclude function - a filled slot + // * `null` - an optional slot that was not filled + // * `undefined` - a slot that was not declared (i.e. invalid) var slotTranscludeFn = boundTranscludeFn.$$slots[slotName]; - if (!slotTranscludeFn) { + if (slotTranscludeFn) { + return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else if (isUndefined(slotTranscludeFn)) { throw $compileMinErr('noslot', 'No parent directive that requires a transclusion with slot name "{0}". ' + 'Element: {1}', slotName, startingTag($element)); } - return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); + } else { + return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } - return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild); } } } + function getControllers(directiveName, require, $element, elementControllers) { + var value; + + if (isString(require)) { + var match = require.match(REQUIRE_PREFIX_REGEXP); + var name = require.substring(match[0].length); + var inheritType = match[1] || match[3]; + var optional = match[2] === '?'; + + //If only parents then start at the parent element + if (inheritType === '^^') { + $element = $element.parent(); + //Otherwise attempt getting the controller from elementControllers in case + //the element is transcluded (and has no data) and to avoid .data if possible + } else { + value = elementControllers && elementControllers[name]; + value = value && value.instance; + } + + if (!value) { + var dataName = '$' + name + 'Controller'; + value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName); + } + + if (!value && !optional) { + throw $compileMinErr('ctreq', + 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!', + name, directiveName); + } + } else if (isArray(require)) { + value = []; + for (var i = 0, ii = require.length; i < ii; i++) { + value[i] = getControllers(directiveName, require[i], $element, elementControllers); + } + } else if (isObject(require)) { + value = {}; + forEach(require, function(controller, property) { + value[property] = getControllers(directiveName, controller, $element, elementControllers); + }); + } + + return value || null; + } + + function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) { + var elementControllers = createMap(); + for (var controllerKey in controllerDirectives) { + var directive = controllerDirectives[controllerKey]; + var locals = { + $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope, + $element: $element, + $attrs: attrs, + $transclude: transcludeFn + }; + + var controller = directive.controller; + if (controller === '@') { + controller = attrs[directive.name]; + } + + var controllerInstance = $controller(controller, locals, true, directive.controllerAs); + + // For directives with element transclusion the element is a comment. + // In this case .data will not attach any data. + // Instead, we save the controllers for the element in a local hash and attach to .data + // later, once we have the actual element. + elementControllers[directive.name] = controllerInstance; + $element.data('$' + directive.name + 'Controller', controllerInstance.instance); + } + return elementControllers; + } + // Depending upon the context in which a directive finds itself it might need to have a new isolated // or child scope created. For instance: // * if the directive has been pulled into a template because another directive with a higher priority @@ -8639,17 +9966,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (hasDirectives.hasOwnProperty(name)) { for (var directive, directives = $injector.get(name + Suffix), i = 0, ii = directives.length; i < ii; i++) { - try { - directive = directives[i]; - if ((isUndefined(maxPriority) || maxPriority > directive.priority) && - directive.restrict.indexOf(location) != -1) { - if (startAttrName) { - directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); + directive = directives[i]; + 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; } - } catch (e) { $exceptionHandler(e); } + tDirectives.push(directive); + match = directive; + } } } return match; @@ -8687,14 +10019,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { */ function mergeTemplateAttributes(dst, src) { var srcAttr = src.$attr, - dstAttr = dst.$attr, - $element = dst.$$element; + dstAttr = dst.$attr; // reapply the old attributes to the new element forEach(dst, function(value, key) { - if (key.charAt(0) != '$') { + if (key.charAt(0) !== '$') { if (src[key] && src[key] !== value) { - value += (key === 'style' ? ';' : ' ') + src[key]; + if (value.length) { + value += (key === 'style' ? ';' : ' ') + src[key]; + } else { + value = src[key]; + } } dst.$set(key, value, true, srcAttr[key]); } @@ -8702,18 +10037,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // copy the new attributes on the old attrs object forEach(src, function(value, key) { - if (key == 'class') { - safeAddClass($element, value); - dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value; - } else if (key == 'style') { - $element.attr('style', $element.attr('style') + ';' + value); - dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value; - // `dst` will never contain hasOwnProperty as DOM parser won't let it. - // You will get an "InvalidCharacterError: DOM Exception 5" error if you - // have an attribute like "has-own-property" or "data-has-own-property", etc. - } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) { + // Check if we already set this attribute in the loop above. + // `dst` will never contain hasOwnProperty as DOM parser won't let it. + // You will get an "InvalidCharacterError: DOM Exception 5" error if you + // have an attribute like "has-own-property" or "data-has-own-property", etc. + if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') { dst[key] = value; - dstAttr[key] = srcAttr[key]; + + if (key !== 'class' && key !== 'style') { + dstAttr[key] = srcAttr[key]; + } } }); } @@ -8750,9 +10083,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } compileNode = $template[0]; - if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { + if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) { throw $compileMinErr('tplrt', - "Template for directive '{0}' must have exactly one root element. {1}", + 'Template for directive \'{0}\' must have exactly one root element. {1}', origAsyncDirective.name, templateUrl); } @@ -8778,7 +10111,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); forEach($rootElement, function(node, i) { - if (node == compileNode) { + if (node === compileNode) { $rootElement[i] = $compileNode[0]; } }); @@ -8815,6 +10148,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { childBoundTranscludeFn); } linkQueue = null; + }).catch(function(error) { + if (error instanceof Error) { + $exceptionHandler(error); + } }); return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) { @@ -8893,7 +10230,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { switch (type) { case 'svg': case 'math': - var wrapper = document.createElement('div'); + var wrapper = window.document.createElement('div'); wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>'; return wrapper.childNodes[0].childNodes; default: @@ -8903,36 +10240,49 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { function getTrustedContext(node, attrNormalizedName) { - if (attrNormalizedName == "srcdoc") { + if (attrNormalizedName === 'srcdoc') { return $sce.HTML; } var tag = nodeName_(node); + // All tags with src attributes require a RESOURCE_URL value, except for + // img and various html5 media tags. + if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') { + if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) { + return $sce.RESOURCE_URL; + } // maction[xlink:href] can source SVG. It's not limited to <maction>. - if (attrNormalizedName == "xlinkHref" || - (tag == "form" && attrNormalizedName == "action") || - (tag != "img" && (attrNormalizedName == "src" || - attrNormalizedName == "ngSrc"))) { + } else if (attrNormalizedName === 'xlinkHref' || + (tag === 'form' && attrNormalizedName === 'action') || + // links can be stylesheets or imports, which can run script in the current origin + (tag === 'link' && attrNormalizedName === 'href') + ) { return $sce.RESOURCE_URL; } } - function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) { + function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) { var trustedContext = getTrustedContext(node, name); - allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing; + var mustHaveExpression = !isNgAttr; + var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr; - var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing); + var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing); // no interpolation found -> ignore if (!interpolateFn) return; - - if (name === "multiple" && nodeName_(node) === "select") { - throw $compileMinErr("selmulti", - "Binding to the 'multiple' attribute is not supported. Element: {0}", + if (name === 'multiple' && nodeName_(node) === 'select') { + throw $compileMinErr('selmulti', + 'Binding to the \'multiple\' attribute is not supported. Element: {0}', startingTag(node)); } + if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { + throw $compileMinErr('nodomevents', + 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' + + 'ng- versions (such as ng-click instead of onclick) instead.'); + } + directives.push({ priority: 100, compile: function() { @@ -8940,12 +10290,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { pre: function attrInterpolatePreLinkFn(scope, element, attr) { var $$observers = (attr.$$observers || (attr.$$observers = createMap())); - if (EVENT_HANDLER_ATTR_REGEXP.test(name)) { - throw $compileMinErr('nodomevents', - "Interpolations for HTML DOM event attributes are disallowed. Please use the " + - "ng- versions (such as ng-click instead of onclick) instead."); - } - // If the attribute has changed since last $interpolate()ed var newValue = attr[name]; if (newValue !== value) { @@ -8974,7 +10318,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { //skip animations when the first digest occurs (when //both the new and the old values are the same) since //the CSS classes are the non-interpolated values - if (name === 'class' && newValue != oldValue) { + if (name === 'class' && newValue !== oldValue) { attr.$updateClass(newValue, oldValue); } else { attr.$set(name, newValue); @@ -9005,7 +10349,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if ($rootElement) { for (i = 0, ii = $rootElement.length; i < ii; i++) { - if ($rootElement[i] == firstElementToRemove) { + if ($rootElement[i] === firstElementToRemove) { $rootElement[i++] = newNode; for (var j = i, j2 = j + removeCount - 1, jj = $rootElement.length; @@ -9037,7 +10381,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { // - remove them from the DOM // - allow them to still be traversed with .nextSibling // - allow a single fragment.qSA to fetch all elements being removed - var fragment = document.createDocumentFragment(); + var fragment = window.document.createDocumentFragment(); for (i = 0; i < removeCount; i++) { fragment.appendChild(elementsToRemove[i]); } @@ -9079,40 +10423,50 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } - // Set up $watches for isolate scope and controller bindings. This process - // only occurs for isolate scopes and new scopes with controllerAs. + // Set up $watches for isolate scope and controller bindings. function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) { var removeWatchCollection = []; - forEach(bindings, function(definition, scopeName) { + var initialChanges = {}; + var changes; + forEach(bindings, function initializeBinding(definition, scopeName) { var attrName = definition.attrName, optional = definition.optional, - mode = definition.mode, // @, =, or & + mode = definition.mode, // @, =, <, or & lastValue, - parentGet, parentSet, compare; + parentGet, parentSet, compare, removeWatch; switch (mode) { case '@': if (!optional && !hasOwnProperty.call(attrs, attrName)) { - destination[scopeName] = attrs[attrName] = void 0; + destination[scopeName] = attrs[attrName] = undefined; } - attrs.$observe(attrName, function(value) { - if (isString(value)) { + removeWatch = attrs.$observe(attrName, function(value) { + if (isString(value) || isBoolean(value)) { + var oldValue = destination[scopeName]; + recordChanges(scopeName, value, oldValue); destination[scopeName] = value; } }); attrs.$$observers[attrName].$$scope = scope; - if (isString(attrs[attrName])) { + lastValue = attrs[attrName]; + if (isString(lastValue)) { // If the attribute has been provided then we trigger an interpolation to ensure // the value is there for use in the link fn - destination[scopeName] = $interpolate(attrs[attrName])(scope); + destination[scopeName] = $interpolate(lastValue)(scope); + } else if (isBoolean(lastValue)) { + // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted + // the value to boolean rather than a string, so we special case this situation + destination[scopeName] = lastValue; } + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + removeWatchCollection.push(removeWatch); break; case '=': if (!hasOwnProperty.call(attrs, attrName)) { if (optional) break; - attrs[attrName] = void 0; + attrs[attrName] = undefined; } if (optional && !attrs[attrName]) break; @@ -9120,14 +10474,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { if (parentGet.literal) { compare = equals; } else { - compare = function(a, b) { return a === b || (a !== a && b !== b); }; + // eslint-disable-next-line no-self-compare + compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); }; } parentSet = parentGet.assign || function() { // reset the change, or we will throw this exception on every $digest lastValue = destination[scopeName] = parentGet(scope); throw $compileMinErr('nonassign', - "Expression '{0}' used with directive '{1}' is non-assignable!", - attrs[attrName], directive.name); + 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!', + attrs[attrName], attrName, directive.name); }; lastValue = destination[scopeName] = parentGet(scope); var parentValueWatch = function parentValueWatch(parentValue) { @@ -9141,10 +10496,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { parentSet(scope, parentValue = destination[scopeName]); } } - return lastValue = parentValue; + lastValue = parentValue; + return lastValue; }; parentValueWatch.$stateful = true; - var removeWatch; if (definition.collection) { removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch); } else { @@ -9153,6 +10508,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { removeWatchCollection.push(removeWatch); break; + case '<': + if (!hasOwnProperty.call(attrs, attrName)) { + if (optional) break; + attrs[attrName] = undefined; + } + if (optional && !attrs[attrName]) break; + + parentGet = $parse(attrs[attrName]); + var deepWatch = parentGet.literal; + + var initialValue = destination[scopeName] = parentGet(scope); + initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]); + + removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) { + if (oldValue === newValue) { + if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) { + return; + } + oldValue = initialValue; + } + recordChanges(scopeName, newValue, oldValue); + destination[scopeName] = newValue; + }, deepWatch); + + removeWatchCollection.push(removeWatch); + break; + case '&': // Don't assign Object.prototype method to scope parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop; @@ -9167,22 +10549,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) { } }); - return removeWatchCollection.length && function removeWatches() { - for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { - removeWatchCollection[i](); + function recordChanges(key, currentValue, previousValue) { + if (isFunction(destination.$onChanges) && currentValue !== previousValue && + // eslint-disable-next-line no-self-compare + (currentValue === currentValue || previousValue === previousValue)) { + // If we have not already scheduled the top level onChangesQueue handler then do so now + if (!onChangesQueue) { + scope.$$postDigest(flushOnChangesQueue); + onChangesQueue = []; + } + // If we have not already queued a trigger of onChanges for this controller then do so now + if (!changes) { + changes = {}; + onChangesQueue.push(triggerOnChangesHook); + } + // If the has been a change on this property already then we need to reuse the previous value + if (changes[key]) { + previousValue = changes[key].previousValue; + } + // Store this change + changes[key] = new SimpleChange(previousValue, currentValue); + } + } + + function triggerOnChangesHook() { + destination.$onChanges(changes); + // Now clear the changes so that we schedule onChanges when more changes arrive + changes = undefined; + } + + return { + initialChanges: initialChanges, + removeWatches: removeWatchCollection.length && function removeWatches() { + for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) { + removeWatchCollection[i](); + } } }; } }]; } -var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i; +function SimpleChange(previous, current) { + this.previousValue = previous; + this.currentValue = current; +} +SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; }; + + +var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i; +var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g; + /** * Converts all accepted directives format into proper directive name. * @param name Name to normalize */ function directiveNormalize(name) { - return camelCase(name.replace(PREFIX_REGEXP, '')); + return name + .replace(PREFIX_REGEXP, '') + .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace); } /** @@ -9254,7 +10679,7 @@ function tokenDifference(str1, str2) { for (var i = 0; i < tokens1.length; i++) { var token = tokens1[i]; for (var j = 0; j < tokens2.length; j++) { - if (token == tokens2[j]) continue outer; + if (token === tokens2[j]) continue outer; } values += (values.length > 0 ? ' ' : '') + token; } @@ -9271,8 +10696,9 @@ function removeComments(jqNodes) { while (i--) { var node = jqNodes[i]; - if (node.nodeType === NODE_TYPE_COMMENT) { - splice.call(jqNodes, i, 1); + if (node.nodeType === NODE_TYPE_COMMENT || + (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) { + splice.call(jqNodes, i, 1); } } return jqNodes; @@ -9281,7 +10707,7 @@ function removeComments(jqNodes) { var $controllerMinErr = minErr('$controller'); -var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; +var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/; function identifierForController(controller, ident) { if (ident && isString(ident)) return ident; if (isString(controller)) { @@ -9294,6 +10720,8 @@ function identifierForController(controller, ident) { /** * @ngdoc provider * @name $controllerProvider + * @this + * * @description * The {@link ng.$controller $controller service} is used by Angular to create new * controllers. @@ -9307,6 +10735,15 @@ function $ControllerProvider() { /** * @ngdoc method + * @name $controllerProvider#has + * @param {string} name Controller name to check. + */ + this.has = function(name) { + return controllers.hasOwnProperty(name); + }; + + /** + * @ngdoc method * @name $controllerProvider#register * @param {string|Object} name Controller name, or an object map of controllers where the keys are * the names and the values are the constructors. @@ -9326,6 +10763,11 @@ function $ControllerProvider() { * @ngdoc method * @name $controllerProvider#allowGlobals * @description If called, allows `$controller` to find controller constructors on `window` + * + * @deprecated + * sinceVersion="v1.3.0" + * removeVersion="v1.7.0" + * This method of finding controllers has been deprecated. */ this.allowGlobals = function() { globals = true; @@ -9346,7 +10788,7 @@ function $ControllerProvider() { * * check if a controller with given name is registered via `$controllerProvider` * * check if evaluating the string on the current scope returns a constructor * * if $controllerProvider#allowGlobals, check `window[constructor]` on the global - * `window` object (not recommended) + * `window` object (deprecated, not recommended) * * The string can use the `controller as property` syntax, where the controller instance is published * as the specified property on the `scope`; the `scope` must be injected into `locals` param for this @@ -9361,7 +10803,7 @@ function $ControllerProvider() { * It's just a simple call to {@link auto.$injector $injector}, but extracted into * a service, so that one can override this service with [BC version](https://gist.github.com/1649788). */ - return function(expression, locals, later, ident) { + return function $controller(expression, locals, later, ident) { // PRIVATE API: // param `later` --- indicates that the controller's constructor is invoked at a later time. // If true, $controller will allocate the object with the correct @@ -9379,16 +10821,21 @@ function $ControllerProvider() { match = expression.match(CNTRL_REG); if (!match) { throw $controllerMinErr('ctrlfmt', - "Badly formed controller string '{0}'. " + - "Must match `__name__ as __id__` or `__name__`.", expression); + 'Badly formed controller string \'{0}\'. ' + + 'Must match `__name__ as __id__` or `__name__`.', expression); } - constructor = match[1], + constructor = match[1]; identifier = identifier || match[3]; expression = controllers.hasOwnProperty(constructor) ? controllers[constructor] : getter(locals.$scope, constructor, true) || (globals ? getter($window, constructor, true) : undefined); + if (!expression) { + throw $controllerMinErr('ctrlreg', + 'The controller with the name \'{0}\' is not registered.', constructor); + } + assertArgFn(expression, constructor, true); } @@ -9411,8 +10858,7 @@ function $ControllerProvider() { addIdentifier(locals, identifier, instance, constructor || expression.name); } - var instantiate; - return instantiate = extend(function() { + return extend(function $controllerInit() { var result = $injector.invoke(expression, instance, locals, constructor); if (result !== instance && (isObject(result) || isFunction(result))) { instance = result; @@ -9440,7 +10886,7 @@ function $ControllerProvider() { function addIdentifier(locals, identifier, instance, name) { if (!(locals && isObject(locals.$scope))) { throw minErr('$controller')('noscp', - "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", + 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.', name, identifier); } @@ -9453,12 +10899,13 @@ function $ControllerProvider() { * @ngdoc service * @name $document * @requires $window + * @this * * @description * A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object. * * @example - <example module="documentExample"> + <example module="documentExample" name="document"> <file name="index.html"> <div ng-controller="ExampleController"> <p>$document title: <b ng-bind="title"></b></p> @@ -9480,10 +10927,38 @@ function $DocumentProvider() { }]; } + +/** + * @private + * @this + * Listens for document visibility change and makes the current status accessible. + */ +function $$IsDocumentHiddenProvider() { + this.$get = ['$document', '$rootScope', function($document, $rootScope) { + var doc = $document[0]; + var hidden = doc && doc.hidden; + + $document.on('visibilitychange', changeListener); + + $rootScope.$on('$destroy', function() { + $document.off('visibilitychange', changeListener); + }); + + function changeListener() { + hidden = doc.hidden; + } + + return function() { + return hidden; + }; + }]; +} + /** * @ngdoc service * @name $exceptionHandler * @requires ng.$log + * @this * * @description * Any uncaught exception in angular expressions is delegated to this service. @@ -9495,18 +10970,21 @@ function $DocumentProvider() { * * ## Example: * + * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught + * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead + * of `$log.error()`. + * * ```js - * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() { - * return function(exception, cause) { - * exception.message += ' (caused by "' + cause + '")'; - * throw exception; - * }; - * }); + * angular. + * module('exceptionOverwrite', []). + * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) { + * return function myExceptionHandler(exception, cause) { + * logErrorsToBackend(exception, cause); + * $log.warn(exception, cause); + * }; + * }]); * ``` * - * This example will override the normal action of `$exceptionHandler`, to make angular - * exceptions fail hard when they happen, instead of just logging to the console. - * * <hr /> * Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind` * methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler} @@ -9516,7 +10994,7 @@ function $DocumentProvider() { * `try { ... } catch(e) { $exceptionHandler(e); }` * * @param {Error} exception Exception associated with the error. - * @param {string=} cause optional information about the context in which + * @param {string=} cause Optional information about the context in which * the error was thrown. * */ @@ -9528,7 +11006,7 @@ function $ExceptionHandlerProvider() { }]; } -var $$ForceReflowProvider = function() { +var $$ForceReflowProvider = /** @this */ function() { this.$get = ['$document', function($document) { return function(domNode) { //the line below will force the browser to perform a repaint so @@ -9558,13 +11036,8 @@ var JSON_ENDS = { '[': /]$/, '{': /}$/ }; -var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/; +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)) { @@ -9574,6 +11047,7 @@ function serializeValue(v) { } +/** @this */ function $HttpParamSerializerProvider() { /** * @ngdoc service @@ -9586,7 +11060,7 @@ function $HttpParamSerializerProvider() { * * `{'foo': 'bar'}` results in `foo=bar` * * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object) * * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element) - * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object) + * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object) * * Note that serializer will sort the request parameters alphabetically. * */ @@ -9598,7 +11072,7 @@ function $HttpParamSerializerProvider() { forEachSorted(params, function(value, key) { if (value === null || isUndefined(value)) return; if (isArray(value)) { - forEach(value, function(v, k) { + forEach(value, function(v) { parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v))); }); } else { @@ -9611,10 +11085,12 @@ function $HttpParamSerializerProvider() { }; } +/** @this */ function $HttpParamSerializerJQLikeProvider() { /** * @ngdoc service * @name $httpParamSerializerJQLike + * * @description * * Alternative {@link $http `$http`} params serializer that follows @@ -9743,7 +11219,7 @@ function parseHeaders(headers) { * @param {(string|Object)} headers Headers to provide access to. * @returns {function(string=)} Returns a getter function which if called with: * - * - if called with single an argument returns a single header value or null + * - if called with an argument returns a single header value or null * - if called with no arguments returns an object containing all headers. */ function headersGetter(headers) { @@ -9754,7 +11230,7 @@ function headersGetter(headers) { if (name) { var value = headersObj[lowercase(name)]; - if (value === void 0) { + if (value === undefined) { value = null; } return value; @@ -9797,6 +11273,8 @@ function isSuccess(status) { /** * @ngdoc provider * @name $httpProvider + * @this + * * @description * Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service. * */ @@ -9808,10 +11286,9 @@ function $HttpProvider() { * * Object containing default values for all {@link ng.$http $http} requests. * - * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`} - * that will provide the cache for all requests who set their `cache` property to `true`. - * If you set the `defaults.cache = false` then only requests that specify their own custom - * cache object will be cached. See {@link $http#caching $http Caching} for more information. + * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses + * by default. See {@link $http#caching $http Caching} for more information. * * - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token. * Defaults value is `'XSRF-TOKEN'`. @@ -9833,6 +11310,10 @@ function $HttpProvider() { * If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}. * Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}. * + * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the + * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the + * {@link $jsonpCallbacks} service. Defaults to `'callback'`. + * **/ var defaults = this.defaults = { // transform incoming response data @@ -9856,7 +11337,9 @@ function $HttpProvider() { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', - paramSerializer: '$httpParamSerializer' + paramSerializer: '$httpParamSerializer', + + jsonpCallbackParam: 'callback' }; var useApplyAsync = false; @@ -9887,30 +11370,6 @@ 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 @@ -9926,8 +11385,8 @@ function $HttpProvider() { **/ var interceptorFactories = this.interceptors = []; - this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce', + function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) { var defaultCache = $cacheFactory('$http'); @@ -10002,10 +11461,13 @@ function $HttpProvider() { * - **config** – `{Object}` – The configuration object that was used to generate the request. * - **statusText** – `{string}` – HTTP status text of the response. * - * A response status code between 200 and 299 is considered a success status and - * will result in the success callback being called. Note that if the response is a redirect, - * XMLHttpRequest will transparently follow it, meaning that the error callback will not be - * called for such responses. + * A response status code between 200 and 299 is considered a success status and will result in + * the success callback being called. Any response status code outside of that range is + * considered an error status and will result in the error callback being called. + * Also, status codes less than -1 are normalized to zero. -1 usually means the request was + * aborted, e.g. using a `config.timeout`. + * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning + * that the outcome (success or error) will be determined by the final response status code. * * * ## Shortcut methods @@ -10041,14 +11503,6 @@ function $HttpProvider() { * $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 @@ -10056,7 +11510,7 @@ function $HttpProvider() { * object, which currently contains this default configuration: * * - `$httpProvider.defaults.headers.common` (headers that are common for all requests): - * - `Accept: application/json, text/plain, * / *` + * - <code>Accept: application/json, text/plain, \*/\*</code> * - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) * - `Content-Type: application/json` * - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) @@ -10102,6 +11556,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 this, transform functions should have no side-effects. + * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return. + * </div> + * * ### Default Transformations * * The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and @@ -10126,7 +11589,7 @@ function $HttpProvider() { * * ### Overriding the Default Transformations Per Request * - * If you wish override the request/response transformations only for a single request then provide + * If you wish to override the request/response transformations only for a single request then provide * `transformRequest` and/or `transformResponse` properties on the configuration object passed * into `$http`. * @@ -10159,26 +11622,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 * - * Note that even if the response is served from cache, delivery of the data is asynchronous in - * the same way that real requests are. + * 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. * - * 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. + * 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. * - * 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. + * 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. + * + * Take note that: + * + * * 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 * @@ -10300,13 +11772,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 @@ -10328,13 +11800,20 @@ function $HttpProvider() { * 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. + * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * - **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. Functions accept a config object as an argument. + * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object. + * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`. + * The handler will be called in the context of a `$apply` block. + * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload + * object. To bind events to the XMLHttpRequest object, use `eventHandlers`. + * The handler will be called in the context of a `$apply` block. * - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. * - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. * - **transformRequest** – @@ -10348,7 +11827,7 @@ function $HttpProvider() { * transform function or an array of such functions. The transform function takes the http * response body, headers and status and returns its transformed (typically deserialized) version. * See {@link ng.$http#overriding-the-default-transformations-per-request - * Overriding the Default TransformationjqLiks} + * Overriding the Default Transformations} * - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to * prepare the string representation of request parameters (specified as an object). * If specified as string, it is interpreted as function registered with the @@ -10356,10 +11835,9 @@ function $HttpProvider() { * by registering it as a {@link auto.$provide#service service}. * The default serializer is the {@link $httpParamSerializer $httpParamSerializer}; * alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike} - * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the - * GET request, otherwise if a cache instance built with - * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for - * caching. + * - **cache** – `{boolean|Object}` – A boolean value or object created with + * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response. + * See {@link $http#caching $http Caching} for more information. * - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} * that should abort the request when resolved. * - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the @@ -10377,7 +11855,7 @@ function $HttpProvider() { * * * @example -<example module="httpExample"> +<example module="httpExample" name="http-service"> <file name="index.html"> <div ng-controller="FetchController"> <select ng-model="method" aria-label="Request method"> @@ -10389,11 +11867,11 @@ function $HttpProvider() { <button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> <button id="samplejsonpbtn" ng-click="updateModel('JSONP', - 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')"> + 'https://angularjs.org/greet.php?name=Super%20Hero')"> Sample JSONP </button> <button id="invalidjsonpbtn" - ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')"> + ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist')"> Invalid JSONP </button> <pre>http status code: {{status}}</pre> @@ -10402,6 +11880,13 @@ function $HttpProvider() { </file> <file name="script.js"> angular.module('httpExample', []) + .config(['$sceDelegateProvider', function($sceDelegateProvider) { + // We must whitelist the JSONP endpoint that we are using to show that we trust it + $sceDelegateProvider.resourceUrlWhitelist([ + 'self', + 'https://angularjs.org/**' + ]); + }]) .controller('FetchController', ['$scope', '$http', '$templateCache', function($scope, $http, $templateCache) { $scope.method = 'GET'; @@ -10416,7 +11901,7 @@ function $HttpProvider() { $scope.status = response.status; $scope.data = response.data; }, function(response) { - $scope.data = response.data || "Request failed"; + $scope.data = response.data || 'Request failed'; $scope.status = response.status; }); }; @@ -10435,7 +11920,6 @@ function $HttpProvider() { var data = element(by.binding('data')); var fetchBtn = element(by.id('fetchbtn')); var sampleGetBtn = element(by.id('samplegetbtn')); - var sampleJsonpBtn = element(by.id('samplejsonpbtn')); var invalidJsonpBtn = element(by.id('invalidjsonpbtn')); it('should make an xhr GET request', function() { @@ -10447,6 +11931,7 @@ function $HttpProvider() { // Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185 // it('should make a JSONP request to angularjs.org', function() { +// var sampleJsonpBtn = element(by.id('samplejsonpbtn')); // sampleJsonpBtn.click(); // fetchBtn.click(); // expect(status.getText()).toMatch('200'); @@ -10469,92 +11954,62 @@ function $HttpProvider() { throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig); } + if (!isString($sce.valueOf(requestConfig.url))) { + throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url); + } + var config = extend({ method: 'get', transformRequest: defaults.transformRequest, transformResponse: defaults.transformResponse, - paramSerializer: defaults.paramSerializer + paramSerializer: defaults.paramSerializer, + jsonpCallbackParam: defaults.jsonpCallbackParam }, requestConfig); config.headers = mergeHeaders(requestConfig); config.method = uppercase(config.method); config.paramSerializer = isString(config.paramSerializer) ? - $injector.get(config.paramSerializer) : config.paramSerializer; + $injector.get(config.paramSerializer) : config.paramSerializer; - var serverRequest = function(config) { - var headers = config.headers; - var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); + $browser.$$incOutstandingRequestCount(); - // strip content-type if data is undefined - if (isUndefined(reqData)) { - forEach(headers, function(value, header) { - if (lowercase(header) === 'content-type') { - delete headers[header]; - } - }); - } - - if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { - config.withCredentials = defaults.withCredentials; - } - - // send request - return sendReq(config, reqData).then(transformResponse, transformResponse); - }; - - var chain = [serverRequest, undefined]; - var promise = $q.when(config); + var requestInterceptors = []; + var responseInterceptors = []; + var promise = $q.resolve(config); // apply interceptors forEach(reversedInterceptors, function(interceptor) { if (interceptor.request || interceptor.requestError) { - chain.unshift(interceptor.request, interceptor.requestError); + requestInterceptors.unshift(interceptor.request, interceptor.requestError); } if (interceptor.response || interceptor.responseError) { - chain.push(interceptor.response, interceptor.responseError); + responseInterceptors.push(interceptor.response, interceptor.responseError); } }); - while (chain.length) { - var thenFn = chain.shift(); - var rejectFn = chain.shift(); + promise = chainInterceptors(promise, requestInterceptors); + promise = promise.then(serverRequest); + promise = chainInterceptors(promise, responseInterceptors); + promise = promise.finally(completeOutstandingRequest); - promise = promise.then(thenFn, rejectFn); - } + return promise; - if (useLegacyPromise) { - promise.success = function(fn) { - assertArgFn(fn, 'fn'); - promise.then(function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; + function chainInterceptors(promise, interceptors) { + for (var i = 0, ii = interceptors.length; i < ii;) { + var thenFn = interceptors[i++]; + var rejectFn = interceptors[i++]; - promise.error = function(fn) { - assertArgFn(fn, 'fn'); + promise = promise.then(thenFn, rejectFn); + } - promise.then(null, function(response) { - fn(response.data, response.status, response.headers, config); - }); - return promise; - }; - } else { - promise.success = $httpMinErrLegacyFn('success'); - promise.error = $httpMinErrLegacyFn('error'); - } + interceptors.length = 0; - return promise; + return promise; + } - function transformResponse(response) { - // make a copy since the response must be cacheable - var resp = extend({}, response); - resp.data = transformData(response.data, response.headers, response.status, - config.transformResponse); - return (isSuccess(response.status)) - ? resp - : $q.reject(resp); + function completeOutstandingRequest() { + $browser.$$completeOutstandingRequest(noop); } function executeHeaderFns(headers, config) { @@ -10581,7 +12036,7 @@ function $HttpProvider() { defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); - // using for-in instead of forEach to avoid unecessary iteration after header has been found + // using for-in instead of forEach to avoid unnecessary iteration after header has been found defaultHeadersIteration: for (defHeaderName in defHeaders) { lowercaseDefHeaderName = lowercase(defHeaderName); @@ -10598,6 +12053,37 @@ function $HttpProvider() { // execute if header value is a function for merged headers return executeHeaderFns(reqHeaders, shallowCopy(config)); } + + function serverRequest(config) { + var headers = config.headers; + var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest); + + // strip content-type if data is undefined + if (isUndefined(reqData)) { + forEach(headers, function(value, header) { + if (lowercase(header) === 'content-type') { + delete headers[header]; + } + }); + } + + if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) { + config.withCredentials = defaults.withCredentials; + } + + // send request + return sendReq(config, reqData).then(transformResponse, transformResponse); + } + + function transformResponse(response) { + // make a copy since the response must be cacheable + var resp = extend({}, response); + resp.data = transformData(response.data, response.headers, response.status, + config.transformResponse); + return (isSuccess(response.status)) + ? resp + : $q.reject(resp); + } } $http.pendingRequests = []; @@ -10609,7 +12095,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `GET` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -10621,7 +12108,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `DELETE` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -10633,7 +12121,8 @@ function $HttpProvider() { * @description * Shortcut method to perform `HEAD` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -10645,8 +12134,33 @@ function $HttpProvider() { * @description * Shortcut method to perform `JSONP` request. * - * @param {string} url Relative or absolute URL specifying the destination of the request. - * The name of the callback should be the string `JSON_CALLBACK`. + * Note that, since JSONP requests are sensitive because the response is given full access to the browser, + * the url must be declared, via {@link $sce} as a trusted resource URL. + * You can trust a URL by adding it to the whitelist via + * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or + * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}. + * + * JSONP requests must specify a callback to be used in the response from the server. This callback + * is passed as a query parameter in the request. You must specify the name of this parameter by + * setting the `jsonpCallbackParam` property on the request config object. + * + * ``` + * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'}) + * ``` + * + * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`. + * Initially this is set to `'callback'`. + * + * <div class="alert alert-danger"> + * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback + * parameter value should go. + * </div> + * + * If you would like to customise where and how the callbacks are stored then try overriding + * or decorating the {@link $jsonpCallbacks} service. + * + * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested; + * or an object created by a call to `$sce.trustAsResourceUrl(url)`. * @param {Object=} config Optional configuration object * @returns {HttpPromise} Future object */ @@ -10745,16 +12259,33 @@ function $HttpProvider() { cache, cachedResp, reqHeaders = config.headers, - url = buildUrl(config.url, config.paramSerializer(config.params)); + isJsonp = lowercase(config.method) === 'jsonp', + url = config.url; + + if (isJsonp) { + // JSONP is a pretty sensitive operation where we're allowing a script to have full access to + // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL. + url = $sce.getTrustedResourceUrl(url); + } else if (!isString(url)) { + // If it is not a string then the URL must be a $sce trusted object + url = $sce.valueOf(url); + } + + url = buildUrl(url, config.paramSerializer(config.params)); + + if (isJsonp) { + // Check the url and add the JSONP callback placeholder + url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam); + } $http.pendingRequests.push(config); promise.then(removePendingReq, removePendingReq); - if ((config.cache || defaults.cache) && config.cache !== false && (config.method === 'GET' || config.method === 'JSONP')) { cache = isObject(config.cache) ? config.cache - : isObject(defaults.cache) ? defaults.cache + : isObject(/** @type {?} */ (defaults).cache) + ? /** @type {?} */ (defaults).cache : defaultCache; } @@ -10790,11 +12321,35 @@ function $HttpProvider() { } $httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, - config.withCredentials, config.responseType); + config.withCredentials, config.responseType, + createApplyHandlers(config.eventHandlers), + createApplyHandlers(config.uploadEventHandlers)); } return promise; + function createApplyHandlers(eventHandlers) { + if (eventHandlers) { + var applyHandlers = {}; + forEach(eventHandlers, function(eventHandler, key) { + applyHandlers[key] = function(event) { + if (useApplyAsync) { + $rootScope.$applyAsync(callEventHandler); + } else if ($rootScope.$$phase) { + callEventHandler(); + } else { + $rootScope.$apply(callEventHandler); + } + + function callEventHandler() { + eventHandler(event); + } + }; + }); + return applyHandlers; + } + } + /** * Callback registered to $httpBackend(): @@ -10854,8 +12409,26 @@ function $HttpProvider() { function buildUrl(url, serializedParams) { if (serializedParams.length > 0) { - url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams; + url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams; + } + return url; + } + + function sanitizeJsonpCallbackParam(url, key) { + if (/[&?][^=]+=JSON_CALLBACK/.test(url)) { + // Throw if the url already contains a reference to JSON_CALLBACK + throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url); } + + var callbackParamRegex = new RegExp('[&?]' + key + '='); + if (callbackParamRegex.test(url)) { + // Throw if the callback param was already provided + throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url); + } + + // Add in the JSON_CALLBACK callback param value + url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK'; + return url; } }]; @@ -10864,6 +12437,7 @@ function $HttpProvider() { /** * @ngdoc service * @name $xhrFactory + * @this * * @description * Factory function used to create XMLHttpRequest objects. @@ -10893,9 +12467,10 @@ function $xhrFactoryProvider() { /** * @ngdoc service * @name $httpBackend - * @requires $window + * @requires $jsonpCallbacks * @requires $document * @requires $xhrFactory + * @this * * @description * HTTP backend used by the {@link ng.$http service} that delegates to @@ -10908,28 +12483,23 @@ function $xhrFactoryProvider() { * $httpBackend} which can be trained with responses. */ function $HttpBackendProvider() { - this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) { - return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]); + this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) { + return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]); }]; } function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) { // TODO(vojta): fix the signature - return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { - $browser.$$incOutstandingRequestCount(); + return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) { url = url || $browser.url(); - if (lowercase(method) == 'jsonp') { - var callbackId = '_' + (callbacks.counter++).toString(36); - callbacks[callbackId] = function(data) { - callbacks[callbackId].data = data; - callbacks[callbackId].called = true; - }; - - var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), - callbackId, function(status, text) { - completeRequest(callback, status, callbacks[callbackId].data, "", text); - callbacks[callbackId] = noop; + if (lowercase(method) === 'jsonp') { + var callbackPath = callbacks.createCallback(url); + var jsonpDone = jsonpReq(url, callbackPath, function(status, text) { + // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING) + var response = (status === 200) && callbacks.getResponse(callbackPath); + completeRequest(callback, status, response, '', text); + callbacks.removeCallback(callbackPath); }); } else { @@ -10956,7 +12526,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc // Occurs when accessing file resources or on Android 4.1 stock browser // while retrieving files from application cache. if (status === 0) { - status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0; + status = response ? 200 : urlResolve(url).protocol === 'file' ? 404 : 0; } completeRequest(callback, @@ -10974,6 +12544,15 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc xhr.onerror = requestError; xhr.onabort = requestError; + xhr.ontimeout = requestError; + + forEach(eventHandlers, function(value, key) { + xhr.addEventListener(key, value); + }); + + forEach(uploadEventHandlers, function(value, key) { + xhr.upload.addEventListener(key, value); + }); if (withCredentials) { xhr.withCredentials = true; @@ -11007,8 +12586,12 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc function timeoutRequest() { - jsonpDone && jsonpDone(); - xhr && xhr.abort(); + if (jsonpDone) { + jsonpDone(); + } + if (xhr) { + xhr.abort(); + } } function completeRequest(callback, status, response, headersString, statusText) { @@ -11019,33 +12602,33 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc jsonpDone = xhr = null; callback(status, response, headersString, statusText); - $browser.$$completeOutstandingRequest(noop); } }; - function jsonpReq(url, callbackId, done) { + function jsonpReq(url, callbackPath, done) { + url = url.replace('JSON_CALLBACK', callbackPath); // we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.: // - fetches local scripts via XHR and evals them // - adds and immediately removes script elements from the document var script = rawDocument.createElement('script'), callback = null; - script.type = "text/javascript"; + script.type = 'text/javascript'; script.src = url; script.async = true; callback = function(event) { - removeEventListenerFn(script, "load", callback); - removeEventListenerFn(script, "error", callback); + script.removeEventListener('load', callback); + script.removeEventListener('error', callback); rawDocument.body.removeChild(script); script = null; var status = -1; - var text = "unknown"; + var text = 'unknown'; if (event) { - if (event.type === "load" && !callbacks[callbackId].called) { - event = { type: "error" }; + if (event.type === 'load' && !callbacks.wasCalled(callbackPath)) { + event = { type: 'error' }; } text = event.type; - status = event.type === "error" ? 404 : 200; + status = event.type === 'error' ? 404 : 200; } if (done) { @@ -11053,8 +12636,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } }; - addEventListenerFn(script, "load", callback); - addEventListenerFn(script, "error", callback); + script.addEventListener('load', callback); + script.addEventListener('error', callback); rawDocument.body.appendChild(script); return callback; } @@ -11063,18 +12646,19 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc 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); + '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()); + return $interpolateMinErr('interr', 'Can\'t interpolate: {0}\n{1}', text, err.toString()); }; /** * @ngdoc provider * @name $interpolateProvider + * @this * * @description * @@ -11089,7 +12673,7 @@ $interpolateMinErr.interr = function(text, err) { * </div> * * @example -<example module="customInterpolationApp"> +<example name="custom-interpolation-markup" module="customInterpolationApp"> <file name="index.html"> <script> var customInterpolationApp = angular.module('customInterpolationApp', []); @@ -11104,7 +12688,7 @@ $interpolateMinErr.interr = function(text, err) { this.label = "This binding is brought you by // interpolation symbols."; }); </script> -<div ng-app="App" ng-controller="DemoController as demo"> +<div ng-controller="DemoController as demo"> //demo.label// </div> </file> @@ -11171,30 +12755,13 @@ function $InterpolateProvider() { 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; - } - - //TODO: this is the same as the constantWatchDelegate in parse.js + // TODO: this is the same as the constantWatchDelegate in parse.js function constantWatchDelegate(scope, listener, objectEquality, constantInterp) { - var unwatch; - return unwatch = scope.$watch(function constantInterpolateWatch(scope) { + var unwatch = scope.$watch(function constantInterpolateWatch(scope) { unwatch(); return constantInterp(scope); }, listener, objectEquality); + return unwatch; } /** @@ -11240,7 +12807,7 @@ function $InterpolateProvider() { * * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. * - * ####Escaped Interpolation + * #### Escaped Interpolation * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). * It will be rendered as a regular start/end marker, and will not be interpreted as an expression @@ -11261,7 +12828,7 @@ function $InterpolateProvider() { * this is typically useful only when user-data is used in rendering a template from the server, or * when otherwise untrusted data is used by a directive. * - * <example> + * <example name="interpolation"> * <file name="index.html"> * <div ng-init="username='A user'"> * <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\} @@ -11276,6 +12843,30 @@ function $InterpolateProvider() { * </file> * </example> * + * @knownIssue + * It is currently not possible for an interpolated expression to contain the interpolation end + * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e. + * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string. + * + * @knownIssue + * All directives and components must use the standard `{{` `}}` interpolation symbols + * in their templates. If you change the application interpolation symbols the {@link $compile} + * service will attempt to denormalize the standard symbols to the custom symbols. + * The denormalization process is not clever enough to know not to replace instances of the standard + * symbols where they would not normally be treated as interpolation symbols. For example in the following + * code snippet the closing braces of the literal object will get incorrectly denormalized: + * + * ``` + * <div data-context='{"context":{"id":3,"type":"page"}}"> + * ``` + * + * The workaround is to ensure that such instances are separated by whitespace: + * ``` + * <div data-context='{"context":{"id":3,"type":"page"} }"> + * ``` + * + * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information. + * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have * embedded expression in order to return an interpolation function. Strings with no @@ -11317,8 +12908,8 @@ function $InterpolateProvider() { expressionPositions = []; while (index < textLength) { - if (((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) { + if (((startIndex = text.indexOf(startSymbol, index)) !== -1) && + ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1)) { if (index !== startIndex) { concat.push(unescapeText(text.substring(index, startIndex))); } @@ -11383,7 +12974,7 @@ function $InterpolateProvider() { expressions: expressions, $$watchDelegate: function(scope, listener) { var lastValue; - return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) { + return scope.$watchGroup(parseFns, /** @this */ function interpolateFnWatcher(values, oldValues) { var currValue = compute(values); if (isFunction(listener)) { listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope); @@ -11440,6 +13031,7 @@ function $InterpolateProvider() { }]; } +/** @this */ function $IntervalProvider() { this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser', function($rootScope, $window, $q, $$q, $browser) { @@ -11472,7 +13064,8 @@ function $IntervalProvider() { * appropriate moment. See the example below for more details on how and when to do this. * </div> * - * @param {function()} fn A function that should be called repeatedly. + * @param {function()} fn A function that should be called repeatedly. If no additional arguments + * are passed (see below), the function is called with the current iteration count. * @param {number} delay Number of milliseconds between each function call. * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat * indefinitely. @@ -11482,7 +13075,7 @@ function $IntervalProvider() { * @returns {promise} A promise which will be notified on each iteration. * * @example - * <example module="intervalExample"> + * <example module="intervalExample" name="interval-service"> * <file name="index.html"> * <script> * angular.module('intervalExample', []) @@ -11627,6 +13220,8 @@ function $IntervalProvider() { */ interval.cancel = function(promise) { if (promise && promise.$$intervalId in intervals) { + // Interval cancels should not report as unhandled promise. + intervals[promise.$$intervalId].promise.catch(noop); intervals[promise.$$intervalId].reject('canceled'); $window.clearInterval(promise.$$intervalId); delete intervals[promise.$$intervalId]; @@ -11641,6 +13236,87 @@ function $IntervalProvider() { /** * @ngdoc service + * @name $jsonpCallbacks + * @requires $window + * @description + * This service handles the lifecycle of callbacks to handle JSONP requests. + * Override this service if you wish to customise where the callbacks are stored and + * how they vary compared to the requested url. + */ +var $jsonpCallbacksProvider = /** @this */ function() { + this.$get = function() { + var callbacks = angular.callbacks; + var callbackMap = {}; + + function createCallback(callbackId) { + var callback = function(data) { + callback.data = data; + callback.called = true; + }; + callback.id = callbackId; + return callback; + } + + return { + /** + * @ngdoc method + * @name $jsonpCallbacks#createCallback + * @param {string} url the url of the JSONP request + * @returns {string} the callback path to send to the server as part of the JSONP request + * @description + * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback + * to pass to the server, which will be used to call the callback with its payload in the JSONP response. + */ + createCallback: function(url) { + var callbackId = '_' + (callbacks.$$counter++).toString(36); + var callbackPath = 'angular.callbacks.' + callbackId; + var callback = createCallback(callbackId); + callbackMap[callbackPath] = callbacks[callbackId] = callback; + return callbackPath; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#wasCalled + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {boolean} whether the callback has been called, as a result of the JSONP response + * @description + * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the + * callback that was passed in the request. + */ + wasCalled: function(callbackPath) { + return callbackMap[callbackPath].called; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#getResponse + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @returns {*} the data received from the response via the registered callback + * @description + * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback + * in the JSONP response. + */ + getResponse: function(callbackPath) { + return callbackMap[callbackPath].data; + }, + /** + * @ngdoc method + * @name $jsonpCallbacks#removeCallback + * @param {string} callbackPath the path to the callback that was sent in the JSONP request + * @description + * {@link $httpBackend} calls this method to remove the callback after the JSONP request has + * completed or timed-out. + */ + removeCallback: function(callbackPath) { + var callback = callbackMap[callbackPath]; + delete callbacks[callback.id]; + delete callbackMap[callbackPath]; + } + }; + }; +}; + +/** + * @ngdoc service * @name $locale * * @description @@ -11650,7 +13326,7 @@ function $IntervalProvider() { * * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) */ -var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, +var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; var $locationMinErr = minErr('$location'); @@ -11680,42 +13356,50 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) { locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; } +var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/; +function parseAppUrl(url, locationObj) { + + if (DOUBLE_SLASH_REGEX.test(url)) { + throw $locationMinErr('badpath', 'Invalid url "{0}".', url); + } -function parseAppUrl(relativeUrl, locationObj) { - var prefixed = (relativeUrl.charAt(0) !== '/'); + var prefixed = (url.charAt(0) !== '/'); if (prefixed) { - relativeUrl = '/' + relativeUrl; + url = '/' + url; } - var match = urlResolve(relativeUrl); + var match = urlResolve(url); locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ? match.pathname.substring(1) : match.pathname); locationObj.$$search = parseKeyValue(match.search); locationObj.$$hash = decodeURIComponent(match.hash); // make sure path starts with '/'; - if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') { + if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') { locationObj.$$path = '/' + locationObj.$$path; } } +function startsWith(str, search) { + return str.slice(0, search.length) === search; +} /** * - * @param {string} begin - * @param {string} whole - * @returns {string} returns text from whole after begin or undefined if it does not begin with - * expected string. + * @param {string} base + * @param {string} url + * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with + * the expected string. */ -function beginsWith(begin, whole) { - if (whole.indexOf(begin) === 0) { - return whole.substr(begin.length); +function stripBaseUrl(base, url) { + if (startsWith(url, base)) { + return url.substr(base.length); } } function stripHash(url) { var index = url.indexOf('#'); - return index == -1 ? url : url.substr(0, index); + return index === -1 ? url : url.substr(0, index); } function trimEmptyHash(url) { @@ -11734,13 +13418,13 @@ function serverBase(url) { /** - * LocationHtml5Url represents an url + * LocationHtml5Url represents a URL * This object is exposed as $location service when HTML5 mode is enabled and supported * * @constructor * @param {string} appBase application base URL * @param {string} appBaseNoFile application base URL stripped of any filename - * @param {string} basePrefix url path prefix + * @param {string} basePrefix URL path prefix */ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$html5 = true; @@ -11749,12 +13433,12 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * Parse given html5 (regular) url string into properties - * @param {string} url HTML5 url + * Parse given HTML5 (regular) URL string into properties + * @param {string} url HTML5 URL * @private */ this.$$parse = function(url) { - var pathUrl = beginsWith(appBaseNoFile, url); + var pathUrl = stripBaseUrl(appBaseNoFile, url); if (!isString(pathUrl)) { throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); @@ -11779,6 +13463,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' + + this.$$urlUpdatedByLocation = true; }; this.$$parseLinkUrl = function(url, relHref) { @@ -11791,16 +13477,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { var appUrl, prevAppUrl; var rewrittenUrl; - if (isDefined(appUrl = beginsWith(appBase, url))) { + + if (isDefined(appUrl = stripBaseUrl(appBase, url))) { prevAppUrl = appUrl; - if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) { - rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl); + if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) { + rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl); } else { rewrittenUrl = appBase + prevAppUrl; } - } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) { + } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBaseNoFile + appUrl; - } else if (appBaseNoFile == url + '/') { + } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; } if (rewrittenUrl) { @@ -11812,7 +13499,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when developer doesn't opt into html5 mode. * It also serves as the base class for html5 mode fallback on legacy browsers. * @@ -11827,19 +13514,19 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * Parse given hashbang url into properties - * @param {string} url Hashbang url + * Parse given hashbang URL into properties + * @param {string} url Hashbang URL * @private */ this.$$parse = function(url) { - var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url); + var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url); var withoutHashUrl; if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') { - // The rest of the url starts with a hash so we have + // The rest of the URL starts with a hash so we have // got either a hashbang path or a plain hash fragment - withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl); + withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl); if (isUndefined(withoutHashUrl)) { // There was no hashbang prefix so we just have a hash fragment withoutHashUrl = withoutBaseUrl; @@ -11855,7 +13542,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { withoutHashUrl = ''; if (isUndefined(withoutBaseUrl)) { appBase = url; - this.replace(); + /** @type {?} */ (this).replace(); } } } @@ -11887,7 +13574,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { var firstPathSegmentMatch; //Get the relative path from the input URL. - if (url.indexOf(base) === 0) { + if (startsWith(url, base)) { url = url.replace(base, ''); } @@ -11902,7 +13589,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { }; /** - * Compose hashbang url and update `absUrl` property + * Compose hashbang URL and update `absUrl` property * @private */ this.$$compose = function() { @@ -11911,10 +13598,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); + + this.$$urlUpdatedByLocation = true; }; this.$$parseLinkUrl = function(url, relHref) { - if (stripHash(appBase) == stripHash(url)) { + if (stripHash(appBase) === stripHash(url)) { this.$$parse(url); return true; } @@ -11924,7 +13613,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) { /** - * LocationHashbangUrl represents url + * LocationHashbangUrl represents URL * This object is exposed as $location service when html5 history api is enabled but the browser * does not support it. * @@ -11948,9 +13637,9 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var rewrittenUrl; var appUrl; - if (appBase == stripHash(url)) { + if (appBase === stripHash(url)) { rewrittenUrl = url; - } else if ((appUrl = beginsWith(appBaseNoFile, url))) { + } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) { rewrittenUrl = appBase + hashPrefix + appUrl; } else if (appBaseNoFile === url + '/') { rewrittenUrl = appBaseNoFile; @@ -11968,6 +13657,8 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; // include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#' this.$$absUrl = appBase + hashPrefix + this.$$url; + + this.$$urlUpdatedByLocation = true; }; } @@ -11976,6 +13667,12 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) { var locationPrototype = { /** + * Ensure absolute URL is initialized. + * @private + */ + $$absUrl:'', + + /** * Are we in html5 mode? * @private */ @@ -11994,17 +13691,17 @@ var locationPrototype = { * @description * This method is getter only. * - * Return full url representation with all segments encoded according to rules specified in + * Return full URL representation with all segments encoded according to rules specified in * [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt). * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var absUrl = $location.absUrl(); * // => "http://example.com/#/some/path?foo=bar&baz=xoxo" * ``` * - * @return {string} full url + * @return {string} full URL */ absUrl: locationGetter('$$absUrl'), @@ -12015,18 +13712,18 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return url (e.g. `/path?a=b#hash`) when called without any parameter. + * Return URL (e.g. `/path?a=b#hash`) when called without any parameter. * * Change path, search and hash, when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var url = $location.url(); * // => "/some/path?foo=bar&baz=xoxo" * ``` * - * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) + * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`) * @return {string} url */ url: function(url) { @@ -12049,16 +13746,16 @@ var locationPrototype = { * @description * This method is getter only. * - * Return protocol of current url. + * Return protocol of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var protocol = $location.protocol(); * // => "http" * ``` * - * @return {string} protocol of current url + * @return {string} protocol of current URL */ protocol: locationGetter('$$protocol'), @@ -12069,24 +13766,24 @@ var locationPrototype = { * @description * This method is getter only. * - * Return host of current url. + * Return host of current URL. * * Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var host = $location.host(); * // => "example.com" * - * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo + * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo * host = $location.host(); * // => "example.com" * host = location.host; * // => "example.com:8080" * ``` * - * @return {string} host of current url. + * @return {string} host of current URL. */ host: locationGetter('$$host'), @@ -12097,11 +13794,11 @@ var locationPrototype = { * @description * This method is getter only. * - * Return port of current url. + * Return port of current URL. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var port = $location.port(); * // => 80 * ``` @@ -12117,7 +13814,7 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return path of current url when called without any parameter. + * Return path of current URL when called without any parameter. * * Change path when called with parameter and return `$location`. * @@ -12126,17 +13823,17 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var path = $location.path(); * // => "/some/path" * ``` * * @param {(string|number)=} path New path - * @return {string} path + * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter */ path: locationGetterSetter('$$path', function(path) { path = path !== null ? path.toString() : ''; - return path.charAt(0) == '/' ? path : '/' + path; + return path.charAt(0) === '/' ? path : '/' + path; }), /** @@ -12146,13 +13843,13 @@ var locationPrototype = { * @description * This method is getter / setter. * - * Return search part (as object) of current url when called without any parameter. + * Return search part (as object) of current URL when called without any parameter. * * Change search part when called with parameter and return `$location`. * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo * var searchObject = $location.search(); * // => {foo: 'bar', baz: 'xoxo'} * @@ -12168,7 +13865,7 @@ var locationPrototype = { * of `$location` to the specified value. * * If the argument is a hash object containing an array of values, these values will be encoded - * as duplicate search parameters in the url. + * as duplicate search parameters in the URL. * * @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue` * will override only a single search property. @@ -12230,7 +13927,7 @@ var locationPrototype = { * * * ```js - * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue + * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue * var hash = $location.hash(); * // => "hashValue" * ``` @@ -12291,6 +13988,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun // but we're changing the $$state reference to $browser.state() during the $digest // so the modification window is narrow. this.$$state = isUndefined(state) ? null : state; + this.$$urlUpdatedByLocation = true; return this; }; @@ -12298,14 +13996,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun function locationGetter(property) { - return function() { + return /** @this */ function() { return this[property]; }; } function locationGetterSetter(property, preprocess) { - return function(value) { + return /** @this */ function(value) { if (isUndefined(value)) { return this[property]; } @@ -12347,11 +14045,13 @@ function locationGetterSetter(property, preprocess) { /** * @ngdoc provider * @name $locationProvider + * @this + * * @description * Use the `$locationProvider` to configure how the application deep linking paths are stored. */ function $LocationProvider() { - var hashPrefix = '', + var hashPrefix = '!', html5Mode = { enabled: false, requireBase: true, @@ -12362,6 +14062,7 @@ function $LocationProvider() { * @ngdoc method * @name $locationProvider#hashPrefix * @description + * The default value for the prefix is `'!'`. * @param {string=} prefix Prefix for hash part (containing path and search) * @returns {*} current value if used as getter or itself (chaining) if used as setter */ @@ -12388,8 +14089,12 @@ function $LocationProvider() { * whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are * true, and a base tag is not present, an error will be thrown when `$location` is injected. * See the {@link guide/$location $location guide for more information} - * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled, - * enables/disables url rewriting for relative links. + * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled, + * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will + * only happen on links with an attribute that matches the given string. For example, if set + * to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links. + * Note that [attribute name normalization](guide/directive#normalization) does not apply + * here, so `'internalLink'` will **not** match `'internal-link'`. * * @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter */ @@ -12407,7 +14112,7 @@ function $LocationProvider() { html5Mode.requireBase = mode.requireBase; } - if (isBoolean(mode.rewriteLinks)) { + if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) { html5Mode.rewriteLinks = mode.rewriteLinks; } @@ -12467,7 +14172,7 @@ function $LocationProvider() { if (html5Mode.enabled) { if (!baseHref && html5Mode.requireBase) { throw $locationMinErr('nobase', - "$location in HTML5 mode requires a <base> tag to be present!"); + '$location in HTML5 mode requires a <base> tag to be present!'); } appBase = serverBase(initialUrl) + (baseHref || '/'); LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; @@ -12504,10 +14209,11 @@ function $LocationProvider() { } $rootElement.on('click', function(event) { + var rewriteLinks = html5Mode.rewriteLinks; // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) // currently we open nice url link and redirect then - if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return; + if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return; var elm = jqLite(event.target); @@ -12517,6 +14223,8 @@ function $LocationProvider() { if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; } + if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return; + var absHref = elm.prop('href'); // get the actual href attribute - see // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx @@ -12538,7 +14246,7 @@ function $LocationProvider() { // getting double entries in the location history. event.preventDefault(); // update location manually - if ($location.absUrl() != $browser.url()) { + if ($location.absUrl() !== $browser.url()) { $rootScope.$apply(); // hack to work around FF6 bug 684208 when scenario runner clicks on links $window.angular['ff-684208-preventDefault'] = true; @@ -12549,7 +14257,7 @@ function $LocationProvider() { // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) { + if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { $browser.url($location.absUrl(), true); } @@ -12558,7 +14266,7 @@ function $LocationProvider() { // update $location when $browser url changes $browser.onUrlChange(function(newUrl, newState) { - if (isUndefined(beginsWith(appBaseNoFile, newUrl))) { + if (!startsWith(newUrl, appBaseNoFile)) { // If we are navigating outside of the app then force a reload $window.location.href = newUrl; return; @@ -12593,36 +14301,40 @@ function $LocationProvider() { // update browser $rootScope.$watch(function $locationWatch() { - var oldUrl = trimEmptyHash($browser.url()); - var newUrl = trimEmptyHash($location.absUrl()); - var oldState = $browser.state(); - var currentReplace = $location.$$replace; - var urlOrStateChanged = oldUrl !== newUrl || - ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); + if (initializing || $location.$$urlUpdatedByLocation) { + $location.$$urlUpdatedByLocation = false; - if (initializing || urlOrStateChanged) { - initializing = false; + var oldUrl = trimEmptyHash($browser.url()); + var newUrl = trimEmptyHash($location.absUrl()); + var oldState = $browser.state(); + var currentReplace = $location.$$replace; + var urlOrStateChanged = oldUrl !== newUrl || + ($location.$$html5 && $sniffer.history && oldState !== $location.$$state); - $rootScope.$evalAsync(function() { - var newUrl = $location.absUrl(); - var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - $location.$$state, oldState).defaultPrevented; + if (initializing || urlOrStateChanged) { + initializing = false; - // if the location was changed by a `$locationChangeStart` handler then stop - // processing this location change - if ($location.absUrl() !== newUrl) return; + $rootScope.$evalAsync(function() { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; - if (defaultPrevented) { - $location.$$parse(oldUrl); - $location.$$state = oldState; - } else { - if (urlOrStateChanged) { - setBrowserUrlWithFallback(newUrl, currentReplace, - oldState === $location.$$state ? null : $location.$$state); + // if the location was changed by a `$locationChangeStart` handler then stop + // processing this location change + if ($location.absUrl() !== newUrl) return; + + if (defaultPrevented) { + $location.$$parse(oldUrl); + $location.$$state = oldState; + } else { + if (urlOrStateChanged) { + setBrowserUrlWithFallback(newUrl, currentReplace, + oldState === $location.$$state ? null : $location.$$state); + } + afterLocationChange(oldUrl, oldState); } - afterLocationChange(oldUrl, oldState); - } - }); + }); + } } $location.$$replace = false; @@ -12655,7 +14367,7 @@ function $LocationProvider() { * {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. * * @example - <example module="logExample"> + <example module="logExample" name="log-service"> <file name="script.js"> angular.module('logExample', []) .controller('LogController', ['$scope', '$log', function($scope, $log) { @@ -12681,6 +14393,8 @@ function $LocationProvider() { /** * @ngdoc provider * @name $logProvider + * @this + * * @description * Use the `$logProvider` to configure how the application logs messages */ @@ -12698,13 +14412,22 @@ function $LogProvider() { this.debugEnabled = function(flag) { if (isDefined(flag)) { debug = flag; - return this; + return this; } else { return debug; } }; this.$get = ['$window', function($window) { + // Support: IE 9-11, Edge 12-14+ + // IE/Edge display errors in such a way that it requires the user to click in 4 places + // to see the stack trace. There is no way to feature-detect it so there's a chance + // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't + // break apps. Other browsers display errors in a sensible way and some of them map stack + // traces along source maps if available so it makes sense to let browsers display it + // as they want. + var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent); + return { /** * @ngdoc method @@ -12757,12 +14480,12 @@ function $LogProvider() { fn.apply(self, arguments); } }; - }()) + })() }; function formatError(arg) { if (arg instanceof Error) { - if (arg.stack) { + if (arg.stack && formatStackTrace) { arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ? 'Error: ' + arg.message + '\n' + arg.stack : arg.stack; @@ -12782,7 +14505,7 @@ function $LogProvider() { // The reason behind this is that console.log has type "object" in IE8... try { hasApply = !!logFn.apply; - } catch (e) {} + } catch (e) { /* empty */ } if (hasApply) { return function() { @@ -12816,118 +14539,45 @@ function $LogProvider() { var $parseMinErr = minErr('$parse'); +var objectValueOf = {}.constructor.prototype.valueOf; + // Sandboxing Angular Expressions // ------------------------------ -// Angular expressions are generally considered safe because these expressions only have direct -// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by -// obtaining a reference to native JS functions such as the Function constructor. +// Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by +// various means such as obtaining a reference to native JS functions like the Function constructor. // // As an example, consider the following Angular expression: // // {}.toString.constructor('alert("evil JS code")') // -// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits -// against the expression language, but not to prevent exploits that were enabled by exposing -// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good -// practice and therefore we are not even trying to protect against interaction with an object -// explicitly exposed in this way. -// -// In general, it is not possible to access a Window object from an angular expression unless a -// window or some DOM object that has a reference to window is published onto a Scope. -// Similarly we prevent invocations of function known to be dangerous, as well as assignments to -// native objects. +// It is important to realize that if you create an expression from a string that contains user provided +// content then it is possible that your application contains a security vulnerability to an XSS style attack. // // See https://docs.angularjs.org/guide/security -function ensureSafeMemberName(name, fullExpression) { - if (name === "__defineGetter__" || name === "__defineSetter__" - || name === "__lookupGetter__" || name === "__lookupSetter__" - || name === "__proto__") { - throw $parseMinErr('isecfld', - 'Attempting to access a disallowed field in Angular expressions! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function getStringValue(name, fullExpression) { - // From the JavaScript docs: +function getStringValue(name) { // Property names must be strings. This means that non-string objects cannot be used // as keys in an object. Any non-string object, including a number, is typecasted // into a string via the toString method. + // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names // - // So, to ensure that we are checking the same `name` that JavaScript would use, - // we cast it to a string, if possible. - // Doing `name + ''` can cause a repl error if the result to `toString` is not a string, - // this is, this will handle objects that misbehave. - name = name + ''; - if (!isString(name)) { - throw $parseMinErr('iseccst', - 'Cannot convert object to primitive value! ' - + 'Expression: {0}', fullExpression); - } - return name; -} - -function ensureSafeObject(obj, fullExpression) { - // nifty check if obj is Function that is fast and works across iframes and other contexts - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isWindow(obj) - obj.window === obj) { - throw $parseMinErr('isecwindow', - 'Referencing the Window in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// isElement(obj) - obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) { - throw $parseMinErr('isecdom', - 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (// block Object so that we can't get hold of dangerous Object.* methods - obj === Object) { - throw $parseMinErr('isecobj', - 'Referencing Object in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } - return obj; -} - -var CALL = Function.prototype.call; -var APPLY = Function.prototype.apply; -var BIND = Function.prototype.bind; - -function ensureSafeFunction(obj, fullExpression) { - if (obj) { - if (obj.constructor === obj) { - throw $parseMinErr('isecfn', - 'Referencing Function in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } else if (obj === CALL || obj === APPLY || obj === BIND) { - throw $parseMinErr('isecff', - 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}', - fullExpression); - } - } + // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it + // to a string. It's not always possible. If `name` is an object and its `toString` method is + // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown: + // + // TypeError: Cannot convert object to primitive value + // + // For performance reasons, we don't catch this error here and allow it to propagate up the call + // stack. Note that you'll get the same error in JavaScript if you try to access a property using + // such a 'broken' object as a key. + return name + ''; } -function ensureSafeAssignContext(obj, fullExpression) { - if (obj) { - if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor || - obj === {}.constructor || obj === [].constructor || obj === Function.constructor) { - throw $parseMinErr('isecaf', - 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression); - } - } -} var OPERATORS = createMap(); forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; }); -var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'}; ///////////////////////////////////////// @@ -12936,7 +14586,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"' /** * @constructor */ -var Lexer = function(options) { +var Lexer = function Lexer(options) { this.options = options; }; @@ -12950,11 +14600,11 @@ Lexer.prototype = { while (this.index < this.text.length) { var ch = this.text.charAt(this.index); - if (ch === '"' || ch === "'") { + if (ch === '"' || ch === '\'') { this.readString(ch); } else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) { this.readNumber(); - } else if (this.isIdent(ch)) { + } else if (this.isIdentifierStart(this.peekMultichar())) { this.readIdent(); } else if (this.is(ch, '(){}[].,;:?')) { this.tokens.push({index: this.index, text: ch}); @@ -12989,7 +14639,7 @@ Lexer.prototype = { }, isNumber: function(ch) { - return ('0' <= ch && ch <= '9') && typeof ch === "string"; + return ('0' <= ch && ch <= '9') && typeof ch === 'string'; }, isWhitespace: function(ch) { @@ -12998,12 +14648,48 @@ Lexer.prototype = { ch === '\n' || ch === '\v' || ch === '\u00A0'); }, - isIdent: function(ch) { + isIdentifierStart: function(ch) { + return this.options.isIdentifierStart ? + this.options.isIdentifierStart(ch, this.codePointAt(ch)) : + this.isValidIdentifierStart(ch); + }, + + isValidIdentifierStart: function(ch) { return ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || '_' === ch || ch === '$'); }, + isIdentifierContinue: function(ch) { + return this.options.isIdentifierContinue ? + this.options.isIdentifierContinue(ch, this.codePointAt(ch)) : + this.isValidIdentifierContinue(ch); + }, + + isValidIdentifierContinue: function(ch, cp) { + return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch); + }, + + codePointAt: function(ch) { + if (ch.length === 1) return ch.charCodeAt(0); + // eslint-disable-next-line no-bitwise + return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00; + }, + + peekMultichar: function() { + var ch = this.text.charAt(this.index); + var peek = this.peek(); + if (!peek) { + return ch; + } + var cp1 = ch.charCodeAt(0); + var cp2 = peek.charCodeAt(0); + if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) { + return ch + peek; + } + return ch; + }, + isExpOperator: function(ch) { return (ch === '-' || ch === '+' || this.isNumber(ch)); }, @@ -13022,19 +14708,19 @@ Lexer.prototype = { var start = this.index; while (this.index < this.text.length) { var ch = lowercase(this.text.charAt(this.index)); - if (ch == '.' || this.isNumber(ch)) { + if (ch === '.' || this.isNumber(ch)) { number += ch; } else { var peekCh = this.peek(); - if (ch == 'e' && this.isExpOperator(peekCh)) { + if (ch === 'e' && this.isExpOperator(peekCh)) { number += ch; } else if (this.isExpOperator(ch) && peekCh && this.isNumber(peekCh) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { number += ch; } else if (this.isExpOperator(ch) && (!peekCh || !this.isNumber(peekCh)) && - number.charAt(number.length - 1) == 'e') { + number.charAt(number.length - 1) === 'e') { this.throwError('Invalid exponent'); } else { break; @@ -13052,12 +14738,13 @@ Lexer.prototype = { readIdent: function() { var start = this.index; + this.index += this.peekMultichar().length; while (this.index < this.text.length) { - var ch = this.text.charAt(this.index); - if (!(this.isIdent(ch) || this.isNumber(ch))) { + var ch = this.peekMultichar(); + if (!this.isIdentifierContinue(ch)) { break; } - this.index++; + this.index += ch.length; } this.tokens.push({ index: start, @@ -13108,7 +14795,7 @@ Lexer.prototype = { } }; -var AST = function(lexer, options) { +var AST = function AST(lexer, options) { this.lexer = lexer; this.options = options; }; @@ -13128,6 +14815,7 @@ AST.ArrayExpression = 'ArrayExpression'; AST.Property = 'Property'; AST.ObjectExpression = 'ObjectExpression'; AST.ThisExpression = 'ThisExpression'; +AST.LocalsExpression = 'LocalsExpression'; // Internal use only AST.NGValueParameter = 'NGValueParameter'; @@ -13163,8 +14851,7 @@ AST.prototype = { filterChain: function() { var left = this.expression(); - var token; - while ((token = this.expect('|'))) { + while (this.expect('|')) { left = this.filter(left); } return left; @@ -13177,6 +14864,10 @@ AST.prototype = { assignment: function() { var result = this.ternary(); if (this.expect('=')) { + if (!isAssignable(result)) { + throw $parseMinErr('lval', 'Trying to assign a value to a non l-value'); + } + result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='}; } return result; @@ -13266,8 +14957,10 @@ AST.prototype = { primary = this.arrayDeclaration(); } else if (this.expect('{')) { primary = this.object(); - } else if (this.constants.hasOwnProperty(this.peek().text)) { - primary = copy(this.constants[this.consume().text]); + } else if (this.selfReferential.hasOwnProperty(this.peek().text)) { + primary = copy(this.selfReferential[this.consume().text]); + } else if (this.options.literals.hasOwnProperty(this.peek().text)) { + primary = { type: AST.Literal, value: this.options.literals[this.consume().text]}; } else if (this.peek().identifier) { primary = this.identifier(); } else if (this.peek().constant) { @@ -13308,7 +15001,7 @@ AST.prototype = { var args = []; if (this.peekToken().text !== ')') { do { - args.push(this.expression()); + args.push(this.filterChain()); } while (this.expect(',')); } return args; @@ -13354,13 +15047,28 @@ AST.prototype = { property = {type: AST.Property, kind: 'init'}; if (this.peek().constant) { property.key = this.constant(); + property.computed = false; + this.consume(':'); + property.value = this.expression(); } else if (this.peek().identifier) { property.key = this.identifier(); + property.computed = false; + if (this.peek(':')) { + this.consume(':'); + property.value = this.expression(); + } else { + property.value = property.key; + } + } else if (this.peek('[')) { + this.consume('['); + property.key = this.expression(); + this.consume(']'); + property.computed = true; + this.consume(':'); + property.value = this.expression(); } else { - this.throwError("invalid key", this.peek()); + this.throwError('invalid key', this.peek()); } - this.consume(':'); - property.value = this.expression(); properties.push(property); } while (this.expect(',')); } @@ -13419,16 +15127,9 @@ AST.prototype = { return false; }, - - /* `undefined` is not a constant, it is an identifier, - * but using it as an identifier is not supported - */ - constants: { - 'true': { type: AST.Literal, value: true }, - 'false': { type: AST.Literal, value: false }, - 'null': { type: AST.Literal, value: null }, - 'undefined': {type: AST.Literal, value: undefined }, - 'this': {type: AST.ThisExpression } + selfReferential: { + 'this': {type: AST.ThisExpression }, + '$locals': {type: AST.LocalsExpression } } }; @@ -13450,6 +15151,7 @@ function isStateless($filter, filterName) { function findConstantAndWatchExpressions(ast, $filter) { var allConstants; var argsToWatch; + var isStatelessFilter; switch (ast.type) { case AST.Program: allConstants = true; @@ -13500,7 +15202,8 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.toWatch = [ast]; break; case AST.CallExpression: - allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false; + isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false; + allConstants = isStatelessFilter; argsToWatch = []; forEach(ast.arguments, function(expr) { findConstantAndWatchExpressions(expr, $filter); @@ -13510,7 +15213,7 @@ function findConstantAndWatchExpressions(ast, $filter) { } }); ast.constant = allConstants; - ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast]; + ast.toWatch = isStatelessFilter ? argsToWatch : [ast]; break; case AST.AssignmentExpression: findConstantAndWatchExpressions(ast.left, $filter); @@ -13536,10 +15239,17 @@ function findConstantAndWatchExpressions(ast, $filter) { argsToWatch = []; forEach(ast.properties, function(property) { findConstantAndWatchExpressions(property.value, $filter); - allConstants = allConstants && property.value.constant; + allConstants = allConstants && property.value.constant && !property.computed; if (!property.value.constant) { argsToWatch.push.apply(argsToWatch, property.value.toWatch); } + if (property.computed) { + findConstantAndWatchExpressions(property.key, $filter); + if (!property.key.constant) { + argsToWatch.push.apply(argsToWatch, property.key.toWatch); + } + } + }); ast.constant = allConstants; ast.toWatch = argsToWatch; @@ -13548,11 +15258,15 @@ function findConstantAndWatchExpressions(ast, $filter) { ast.constant = false; ast.toWatch = []; break; + case AST.LocalsExpression: + ast.constant = false; + ast.toWatch = []; + break; } } function getInputs(body) { - if (body.length != 1) return; + if (body.length !== 1) return; var lastExpression = body[0].expression; var candidate = lastExpression.toWatch; if (candidate.length !== 1) return candidate; @@ -13587,13 +15301,12 @@ function ASTCompiler(astBuilder, $filter) { } ASTCompiler.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { 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: [] @@ -13634,27 +15347,16 @@ ASTCompiler.prototype = { this.watchFns() + 'return fn;'; - /* jshint -W054 */ + // eslint-disable-next-line no-new-func 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 */ + plusFn); this.state = this.stage = undefined; fn.literal = isLiteral(ast); fn.constant = isConstant(ast); @@ -13704,7 +15406,7 @@ ASTCompiler.prototype = { }, recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { - var left, right, self = this, args, expression; + var left, right, self = this, args, expression, computed; recursionFn = recursionFn || noop; if (!skipWatchIdCheck && isDefined(ast.watchId)) { intoId = intoId || this.nextId(); @@ -13728,7 +15430,7 @@ ASTCompiler.prototype = { case AST.Literal: expression = this.escape(ast.value); this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.UnaryExpression: this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; }); @@ -13768,22 +15470,18 @@ ASTCompiler.prototype = { 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.isNull(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: @@ -13795,25 +15493,20 @@ ASTCompiler.prototype = { 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)); + expression = 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), '{}')); + self.if_(self.isNull(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; @@ -13845,21 +15538,16 @@ ASTCompiler.prototype = { 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)); + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { + args.push(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'); @@ -13871,14 +15559,9 @@ ASTCompiler.prototype = { 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); @@ -13888,35 +15571,63 @@ ASTCompiler.prototype = { case AST.ArrayExpression: args = []; forEach(ast.elements, function(expr) { - self.recurse(expr, self.nextId(), undefined, function(argument) { + self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) { args.push(argument); }); }); expression = '[' + args.join(',') + ']'; this.assign(intoId, expression); - recursionFn(expression); + recursionFn(intoId || expression); break; case AST.ObjectExpression: args = []; + computed = false; forEach(ast.properties, function(property) { - self.recurse(property.value, self.nextId(), undefined, function(expr) { - args.push(self.escape( - property.key.type === AST.Identifier ? property.key.name : - ('' + property.key.value)) + - ':' + expr); - }); + if (property.computed) { + computed = true; + } }); - expression = '{' + args.join(',') + '}'; - this.assign(intoId, expression); - recursionFn(expression); + if (computed) { + intoId = intoId || this.nextId(); + this.assign(intoId, '{}'); + forEach(ast.properties, function(property) { + if (property.computed) { + left = self.nextId(); + self.recurse(property.key, left); + } else { + left = property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value); + } + right = self.nextId(); + self.recurse(property.value, right); + self.assign(self.member(intoId, left, property.computed), right); + }); + } else { + forEach(ast.properties, function(property) { + self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) { + args.push(self.escape( + property.key.type === AST.Identifier ? property.key.name : + ('' + property.key.value)) + + ':' + expr); + }); + }); + expression = '{' + args.join(',') + '}'; + this.assign(intoId, expression); + } + recursionFn(intoId || expression); break; case AST.ThisExpression: this.assign(intoId, 's'); - recursionFn('s'); + recursionFn(intoId || 's'); + break; + case AST.LocalsExpression: + this.assign(intoId, 'l'); + recursionFn(intoId || 'l'); break; case AST.NGValueParameter: this.assign(intoId, 'v'); - recursionFn('v'); + recursionFn(intoId || 'v'); break; } }, @@ -13975,12 +15686,22 @@ ASTCompiler.prototype = { return '!(' + expression + ')'; }, + isNull: function(expression) { + return expression + '==null'; + }, + notNull: function(expression) { return expression + '!=null'; }, nonComputedMember: function(left, right) { - return left + '.' + right; + var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/; + var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g; + if (SAFE_IDENTIFIER.test(right)) { + return left + '.' + right; + } else { + return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]'; + } }, computedMember: function(left, right) { @@ -13992,40 +15713,8 @@ ASTCompiler.prototype = { 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)'; + this.assign(item, 'getStringValue(' + item + ')'); }, lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) { @@ -14049,7 +15738,7 @@ ASTCompiler.prototype = { }, escape: function(value) { - if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'"; + 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'; @@ -14079,11 +15768,9 @@ function ASTInterpreter(astBuilder, $filter) { } ASTInterpreter.prototype = { - compile: function(expression, expensiveChecks) { + compile: function(expression) { var self = this; var ast = this.astBuilder.ast(expression); - this.expression = expression; - this.expensiveChecks = expensiveChecks; findConstantAndWatchExpressions(ast, self.$filter); var assignable; var assign; @@ -14105,7 +15792,7 @@ ASTInterpreter.prototype = { forEach(ast.body, function(expression) { expressions.push(self.recurse(expression.expression)); }); - var fn = ast.body.length === 0 ? function() {} : + var fn = ast.body.length === 0 ? noop : ast.body.length === 1 ? expressions[0] : function(scope, locals) { var lastValue; @@ -14128,7 +15815,7 @@ ASTInterpreter.prototype = { }, recurse: function(ast, context, create) { - var left, right, self = this, args, expression; + var left, right, self = this, args; if (ast.input) { return this.inputs(ast.input, ast.watchId); } @@ -14154,20 +15841,16 @@ ASTInterpreter.prototype = { context ); case AST.Identifier: - ensureSafeMemberName(ast.name, self.expression); - return self.identifier(ast.name, - self.expensiveChecks || isPossiblyDangerousMemberName(ast.name), - context, create, self.expression); + return self.identifier(ast.name, context, create); 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); + this.computedMember(left, right, context, create) : + this.nonComputedMember(left, right, context, create); case AST.CallExpression: args = []; forEach(ast.arguments, function(expr) { @@ -14188,13 +15871,11 @@ ASTInterpreter.prototype = { 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)); + values.push(args[i](scope, locals, assign, inputs)); } - value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression); + value = rhs.value.apply(rhs.context, values); } return context ? {value: value} : value; }; @@ -14204,8 +15885,6 @@ ASTInterpreter.prototype = { 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; }; @@ -14224,16 +15903,28 @@ ASTInterpreter.prototype = { case AST.ObjectExpression: args = []; forEach(ast.properties, function(property) { - args.push({key: property.key.type === AST.Identifier ? - property.key.name : - ('' + property.key.value), - value: self.recurse(property.value) - }); + if (property.computed) { + args.push({key: self.recurse(property.key), + computed: true, + value: self.recurse(property.value) + }); + } else { + args.push({key: property.key.type === AST.Identifier ? + property.key.name : + ('' + property.key.value), + computed: false, + value: self.recurse(property.value) + }); + } }); return function(scope, locals, assign, inputs) { var value = {}; for (var i = 0; i < args.length; ++i) { - value[args[i].key] = args[i].value(scope, locals, assign, inputs); + if (args[i].computed) { + value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs); + } else { + value[args[i].key] = args[i].value(scope, locals, assign, inputs); + } } return context ? {value: value} : value; }; @@ -14241,8 +15932,12 @@ ASTInterpreter.prototype = { return function(scope) { return context ? {value: scope} : scope; }; + case AST.LocalsExpression: + return function(scope, locals) { + return context ? {value: locals} : locals; + }; case AST.NGValueParameter: - return function(scope, locals, assign, inputs) { + return function(scope, locals, assign) { return context ? {value: assign} : assign; }; } @@ -14265,7 +15960,7 @@ ASTInterpreter.prototype = { if (isDefined(arg)) { arg = -arg; } else { - arg = 0; + arg = -0; } return context ? {value: arg} : arg; }; @@ -14324,12 +16019,14 @@ ASTInterpreter.prototype = { }, 'binary==': function(left, right, context) { return function(scope, locals, assign, inputs) { + // eslint-disable-next-line eqeqeq 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) { + // eslint-disable-next-line eqeqeq var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs); return context ? {value: arg} : arg; }; @@ -14379,16 +16076,13 @@ ASTInterpreter.prototype = { value: function(value, context) { return function() { return context ? {context: undefined, name: undefined, value: value} : value; }; }, - identifier: function(name, expensiveChecks, context, create, expression) { + identifier: function(name, context, create) { return function(scope, locals, assign, inputs) { var base = locals && (name in locals) ? locals : scope; - if (create && create !== 1 && base && !(base[name])) { + if (create && create !== 1 && base && base[name] == null) { base[name] = {}; } var value = base ? base[name] : undefined; - if (expensiveChecks) { - ensureSafeObject(value, expression); - } if (context) { return {context: base, name: name, value: value}; } else { @@ -14396,7 +16090,7 @@ ASTInterpreter.prototype = { } }; }, - computedMember: function(left, right, context, create, expression) { + computedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); var rhs; @@ -14404,12 +16098,12 @@ ASTInterpreter.prototype = { if (lhs != null) { rhs = right(scope, locals, assign, inputs); rhs = getStringValue(rhs); - ensureSafeMemberName(rhs, expression); - if (create && create !== 1 && lhs && !(lhs[rhs])) { - lhs[rhs] = {}; + if (create && create !== 1) { + if (lhs && !(lhs[rhs])) { + lhs[rhs] = {}; + } } value = lhs[rhs]; - ensureSafeObject(value, expression); } if (context) { return {context: lhs, name: rhs, value: value}; @@ -14418,16 +16112,15 @@ ASTInterpreter.prototype = { } }; }, - nonComputedMember: function(left, right, expensiveChecks, context, create, expression) { + nonComputedMember: function(left, right, context, create) { return function(scope, locals, assign, inputs) { var lhs = left(scope, locals, assign, inputs); - if (create && create !== 1 && lhs && !(lhs[right])) { - lhs[right] = {}; + if (create && create !== 1) { + if (lhs && lhs[right] == null) { + 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 { @@ -14446,11 +16139,11 @@ ASTInterpreter.prototype = { /** * @constructor */ -var Parser = function(lexer, $filter, options) { +var Parser = function Parser(lexer, $filter, options) { this.lexer = lexer; this.$filter = $filter; this.options = options; - this.ast = new AST(this.lexer); + this.ast = new AST(lexer, options); this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) : new ASTCompiler(this.ast, $filter); }; @@ -14459,19 +16152,10 @@ Parser.prototype = { constructor: Parser, parse: function(text) { - return this.astCompiler.compile(text, this.options.expensiveChecks); + return this.astCompiler.compile(text); } }; -var getterFnCacheDefault = createMap(); -var getterFnCacheExpensive = createMap(); - -function isPossiblyDangerousMemberName(name) { - return name == 'constructor'; -} - -var objectValueOf = Object.prototype.valueOf; - function getValueOf(value) { return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value); } @@ -14522,27 +16206,78 @@ function getValueOf(value) { /** * @ngdoc provider * @name $parseProvider + * @this * * @description * `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse} * service. */ function $ParseProvider() { - var cacheDefault = createMap(); - var cacheExpensive = createMap(); + var cache = createMap(); + var literals = { + 'true': true, + 'false': false, + 'null': null, + 'undefined': undefined + }; + var identStart, identContinue; + + /** + * @ngdoc method + * @name $parseProvider#addLiteral + * @description + * + * Configure $parse service to add literal values that will be present as literal at expressions. + * + * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name. + * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`. + * + **/ + this.addLiteral = function(literalName, literalValue) { + literals[literalName] = literalValue; + }; + + /** + * @ngdoc method + * @name $parseProvider#setIdentifierFns + * + * @description + * + * Allows defining the set of characters that are allowed in Angular expressions. The function + * `identifierStart` will get called to know if a given character is a valid character to be the + * first character for an identifier. The function `identifierContinue` will get called to know if + * a given character is a valid character to be a follow-up identifier character. The functions + * `identifierStart` and `identifierContinue` will receive as arguments the single character to be + * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in + * mind that the `string` parameter can be two characters long depending on the character + * representation. It is expected for the function to return `true` or `false`, whether that + * character is allowed or not. + * + * Since this function will be called extensively, keep the implementation of these functions fast, + * as the performance of these functions have a direct impact on the expressions parsing speed. + * + * @param {function=} identifierStart The function that will decide whether the given character is + * a valid identifier start character. + * @param {function=} identifierContinue The function that will decide whether the given character is + * a valid identifier continue character. + */ + this.setIdentifierFns = function(identifierStart, identifierContinue) { + identStart = identifierStart; + identContinue = identifierContinue; + return this; + }; this.$get = ['$filter', function($filter) { var noUnsafeEval = csp().noUnsafeEval; var $parseOptions = { csp: noUnsafeEval, - expensiveChecks: false - }, - $parseOptionsExpensive = { - csp: noUnsafeEval, - expensiveChecks: true + literals: copy(literals), + isIdentifierStart: isFunction(identStart) && identStart, + isIdentifierContinue: isFunction(identContinue) && identContinue }; + return $parse; - return function $parse(exp, interceptorFn, expensiveChecks) { + function $parse(exp, interceptorFn) { var parsedExpression, oneTime, cacheKey; switch (typeof exp) { @@ -14550,7 +16285,6 @@ function $ParseProvider() { exp = exp.trim(); cacheKey = exp; - var cache = (expensiveChecks ? cacheExpensive : cacheDefault); parsedExpression = cache[cacheKey]; if (!parsedExpression) { @@ -14558,9 +16292,8 @@ 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); + var lexer = new Lexer($parseOptions); + var parser = new Parser(lexer, $filter, $parseOptions); parsedExpression = parser.parse(exp); if (parsedExpression.constant) { parsedExpression.$$watchDelegate = constantWatchDelegate; @@ -14578,17 +16311,17 @@ function $ParseProvider() { return addInterceptor(exp, interceptorFn); default: - return noop; + return addInterceptor(noop, interceptorFn); } - }; + } - function expressionInputDirtyCheck(newValue, oldValueOfValue) { + function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) { if (newValue == null || oldValueOfValue == null) { // null/undefined return newValue === oldValueOfValue; } - if (typeof newValue === 'object') { + if (typeof newValue === 'object' && !compareObjectIdentity) { // attempt to convert the value to a primitive type // TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can @@ -14604,6 +16337,7 @@ function $ParseProvider() { } //Primitive or NaN + // eslint-disable-next-line no-self-compare return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue); } @@ -14616,7 +16350,7 @@ function $ParseProvider() { inputExpressions = inputExpressions[0]; return scope.$watch(function expressionInputWatch(scope) { var newInputValue = inputExpressions(scope); - if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) { + if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) { lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]); oldInputValueOf = newInputValue && getValueOf(newInputValue); } @@ -14636,7 +16370,7 @@ function $ParseProvider() { for (var i = 0, ii = inputExpressions.length; i < ii; i++) { var newInputValue = inputExpressions[i](scope); - if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) { + if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) { oldInputValues[i] = newInputValue; oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue); } @@ -14650,14 +16384,22 @@ function $ParseProvider() { }, listener, objectEquality, prettyPrintExpression); } - function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) { + function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) { var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { + if (parsedExpression.inputs) { + unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression); + } else { + unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality); + } + return unwatch; + + function oneTimeWatch(scope) { return parsedExpression(scope); - }, function oneTimeListener(value, old, scope) { + } + function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.apply(this, arguments); + listener(value, old, scope); } if (isDefined(value)) { scope.$$postDigest(function() { @@ -14666,17 +16408,17 @@ function $ParseProvider() { } }); } - }, objectEquality); + } } function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) { var unwatch, lastValue; - return unwatch = scope.$watch(function oneTimeWatch(scope) { + unwatch = scope.$watch(function oneTimeWatch(scope) { return parsedExpression(scope); }, function oneTimeListener(value, old, scope) { lastValue = value; if (isFunction(listener)) { - listener.call(this, value, old, scope); + listener(value, old, scope); } if (isAllDefined(value)) { scope.$$postDigest(function() { @@ -14685,6 +16427,8 @@ function $ParseProvider() { } }, objectEquality); + return unwatch; + function isAllDefined(value) { var allDefined = true; forEach(value, function(val) { @@ -14695,11 +16439,11 @@ function $ParseProvider() { } function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) { - var unwatch; - return unwatch = scope.$watch(function constantWatch(scope) { + var unwatch = scope.$watch(function constantWatch(scope) { unwatch(); return parsedExpression(scope); }, listener, objectEquality); + return unwatch; } function addInterceptor(parsedExpression, interceptorFn) { @@ -14723,14 +16467,15 @@ function $ParseProvider() { }; // Propagate $$watchDelegates other then inputsWatchDelegate + useInputs = !parsedExpression.inputs; if (parsedExpression.$$watchDelegate && parsedExpression.$$watchDelegate !== inputsWatchDelegate) { fn.$$watchDelegate = parsedExpression.$$watchDelegate; + fn.inputs = parsedExpression.inputs; } else if (!interceptorFn.$stateful) { // If there is an interceptor, but no watchDelegate then treat the interceptor like // we treat filters - it is assumed to be a pure function unless flagged with $stateful fn.$$watchDelegate = inputsWatchDelegate; - useInputs = !parsedExpression.inputs; fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]; } @@ -14748,19 +16493,19 @@ function $ParseProvider() { * A service that helps you run functions asynchronously, and use their return values (or exceptions) * when they are done processing. * - * This is an implementation of promises/deferred objects inspired by - * [Kris Kowal's Q](https://github.com/kriskowal/q). + * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred + * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q). * * $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred - * implementations, and the other which resembles ES6 promises to some degree. + * implementations, and the other which resembles ES6 (ES2015) promises to some degree. * * # $q constructor * * The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver` - * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony, + * function as the first argument. This is similar to the native Promise implementation from ES6, * see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). * - * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are + * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are * available yet. * * It can be used like so: @@ -14792,7 +16537,7 @@ function $ParseProvider() { * * Note: progress/notify callbacks are not currently supported via the ES6-style interface. * - * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise. + * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise. * * However, the more traditional CommonJS-style usage is still available, and documented below. * @@ -14874,7 +16619,7 @@ function $ParseProvider() { * * **Methods** * - * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or + * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or * will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously * as soon as the result is available. The callbacks are called with a single argument: the result * or rejection reason. Additionally, the notify callback may be called zero or more times to @@ -14885,7 +16630,8 @@ function $ParseProvider() { * with the value which is resolved in that promise using * [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)). * It also notifies via the return value of the `notifyCallback` method. The promise cannot be - * resolved or rejected from the notifyCallback method. + * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback + * arguments are optional. * * - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)` * @@ -14925,7 +16671,7 @@ function $ParseProvider() { * - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains * all the important functionality needed for common async tasks. * - * # Testing + * # Testing * * ```js * it('should simulate promise', inject(function($q, $rootScope) { @@ -14955,21 +16701,61 @@ function $ParseProvider() { * * @returns {Promise} The newly created promise. */ +/** + * @ngdoc provider + * @name $qProvider + * @this + * + * @description + */ function $QProvider() { - + var errorOnUnhandledRejections = true; this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) { return qFactory(function(callback) { $rootScope.$evalAsync(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + /** + * @ngdoc method + * @name $qProvider#errorOnUnhandledRejections + * @kind function + * + * @description + * Retrieves or overrides whether to generate an error when a rejected promise is not handled. + * This feature is enabled by default. + * + * @param {boolean=} value Whether to generate an error when a rejected promise is not handled. + * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for + * chaining otherwise. + */ + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } +/** @this */ function $$QProvider() { + var errorOnUnhandledRejections = true; this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { return qFactory(function(callback) { $browser.defer(callback); - }, $exceptionHandler); + }, $exceptionHandler, errorOnUnhandledRejections); }]; + + this.errorOnUnhandledRejections = function(value) { + if (isDefined(value)) { + errorOnUnhandledRejections = value; + return this; + } else { + return errorOnUnhandledRejections; + } + }; } /** @@ -14978,10 +16764,14 @@ function $$QProvider() { * @param {function(function)} nextTick Function for executing functions in the next turn. * @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for * debugging purposes. + @ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled + * promises rejections. * @returns {object} Promise manager. */ -function qFactory(nextTick, exceptionHandler) { +function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) { var $qMinErr = minErr('$q', TypeError); + var queueSize = 0; + var checkQueue = []; /** * @ngdoc method @@ -14993,14 +16783,18 @@ function qFactory(nextTick, exceptionHandler) { * * @returns {Deferred} Returns a new instance of deferred. */ - var defer = function() { - var d = new Deferred(); - //Necessary to support unbound execution :/ - d.resolve = simpleBind(d, d.resolve); - d.reject = simpleBind(d, d.reject); - d.notify = simpleBind(d, d.notify); - return d; - }; + function defer() { + return new Deferred(); + } + + function Deferred() { + var promise = this.promise = new Promise(); + //Non prototype methods necessary to support unbound execution :/ + this.resolve = function(val) { resolvePromise(promise, val); }; + this.reject = function(reason) { rejectPromise(promise, reason); }; + this.notify = function(progress) { notifyPromise(promise, progress); }; + } + function Promise() { this.$$state = { status: 0 }; @@ -15011,144 +16805,162 @@ function qFactory(nextTick, exceptionHandler) { if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) { return this; } - var result = new Deferred(); + var result = new Promise(); this.$$state.pending = this.$$state.pending || []; this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]); if (this.$$state.status > 0) scheduleProcessQueue(this.$$state); - return result.promise; + return result; }, - "catch": function(callback) { + 'catch': function(callback) { return this.then(null, callback); }, - "finally": function(callback, progressBack) { + 'finally': function(callback, progressBack) { return this.then(function(value) { - return handleCallback(value, true, callback); + return handleCallback(value, resolve, callback); }, function(error) { - return handleCallback(error, false, callback); + return handleCallback(error, reject, callback); }, progressBack); } }); - //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native - function simpleBind(context, fn) { - return function(value) { - fn.call(context, value); - }; - } - function processQueue(state) { - var fn, deferred, pending; + var fn, promise, pending; pending = state.pending; state.processScheduled = false; state.pending = undefined; - for (var i = 0, ii = pending.length; i < ii; ++i) { - deferred = pending[i][0]; - fn = pending[i][state.status]; - try { - if (isFunction(fn)) { - deferred.resolve(fn(state.value)); - } else if (state.status === 1) { - deferred.resolve(state.value); + try { + for (var i = 0, ii = pending.length; i < ii; ++i) { + state.pur = true; + promise = pending[i][0]; + fn = pending[i][state.status]; + try { + if (isFunction(fn)) { + resolvePromise(promise, fn(state.value)); + } else if (state.status === 1) { + resolvePromise(promise, state.value); + } else { + rejectPromise(promise, state.value); + } + } catch (e) { + rejectPromise(promise, e); + } + } + } finally { + --queueSize; + if (errorOnUnhandledRejections && queueSize === 0) { + nextTick(processChecks); + } + } + } + + function processChecks() { + // eslint-disable-next-line no-unmodified-loop-condition + while (!queueSize && checkQueue.length) { + var toCheck = checkQueue.shift(); + if (!toCheck.pur) { + toCheck.pur = true; + var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value); + if (toCheck.value instanceof Error) { + exceptionHandler(toCheck.value, errorMessage); } else { - deferred.reject(state.value); + exceptionHandler(errorMessage); } - } catch (e) { - deferred.reject(e); - exceptionHandler(e); } } } function scheduleProcessQueue(state) { + if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) { + if (queueSize === 0 && checkQueue.length === 0) { + nextTick(processChecks); + } + checkQueue.push(state); + } if (state.processScheduled || !state.pending) return; state.processScheduled = true; + ++queueSize; nextTick(function() { processQueue(state); }); } - function Deferred() { - this.promise = new Promise(); + function resolvePromise(promise, val) { + if (promise.$$state.status) return; + if (val === promise) { + $$reject(promise, $qMinErr( + 'qcycle', + 'Expected promise to be resolved with value other than itself \'{0}\'', + val)); + } else { + $$resolve(promise, val); + } + } - extend(Deferred.prototype, { - resolve: function(val) { - if (this.promise.$$state.status) return; - if (val === this.promise) { - this.$$reject($qMinErr( - 'qcycle', - "Expected promise to be resolved with value other than itself '{0}'", - val)); + function $$resolve(promise, val) { + var then; + var done = false; + try { + if (isObject(val) || isFunction(val)) then = val.then; + if (isFunction(then)) { + promise.$$state.status = -1; + then.call(val, doResolve, doReject, doNotify); } else { - this.$$resolve(val); - } - - }, - - $$resolve: function(val) { - var then; - var that = this; - var done = false; - try { - if ((isObject(val) || isFunction(val))) then = val && val.then; - if (isFunction(then)) { - this.promise.$$state.status = -1; - then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify)); - } else { - this.promise.$$state.value = val; - this.promise.$$state.status = 1; - scheduleProcessQueue(this.promise.$$state); - } - } catch (e) { - rejectPromise(e); - exceptionHandler(e); + promise.$$state.value = val; + promise.$$state.status = 1; + scheduleProcessQueue(promise.$$state); } + } catch (e) { + doReject(e); + } - function resolvePromise(val) { - if (done) return; - done = true; - that.$$resolve(val); - } - function rejectPromise(val) { - if (done) return; - done = true; - that.$$reject(val); - } - }, + function doResolve(val) { + if (done) return; + done = true; + $$resolve(promise, val); + } + function doReject(val) { + if (done) return; + done = true; + $$reject(promise, val); + } + function doNotify(progress) { + notifyPromise(promise, progress); + } + } - reject: function(reason) { - if (this.promise.$$state.status) return; - this.$$reject(reason); - }, + function rejectPromise(promise, reason) { + if (promise.$$state.status) return; + $$reject(promise, reason); + } - $$reject: function(reason) { - this.promise.$$state.value = reason; - this.promise.$$state.status = 2; - scheduleProcessQueue(this.promise.$$state); - }, + function $$reject(promise, reason) { + promise.$$state.value = reason; + promise.$$state.status = 2; + scheduleProcessQueue(promise.$$state); + } - notify: function(progress) { - var callbacks = this.promise.$$state.pending; + function notifyPromise(promise, progress) { + var callbacks = promise.$$state.pending; - if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) { - nextTick(function() { - var callback, result; - for (var i = 0, ii = callbacks.length; i < ii; i++) { - result = callbacks[i][0]; - callback = callbacks[i][3]; - try { - result.notify(isFunction(callback) ? callback(progress) : progress); - } catch (e) { - exceptionHandler(e); - } + if ((promise.$$state.status <= 0) && callbacks && callbacks.length) { + nextTick(function() { + var callback, result; + for (var i = 0, ii = callbacks.length; i < ii; i++) { + result = callbacks[i][0]; + callback = callbacks[i][3]; + try { + notifyPromise(result, isFunction(callback) ? callback(progress) : progress); + } catch (e) { + exceptionHandler(e); } - }); - } + } + }); } - }); + } /** * @ngdoc method @@ -15186,39 +16998,27 @@ function qFactory(nextTick, exceptionHandler) { * @param {*} reason Constant, message, exception or an object representing the rejection reason. * @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. */ - var reject = function(reason) { - var result = new Deferred(); - result.reject(reason); - return result.promise; - }; - - var makePromise = function makePromise(value, resolved) { - var result = new Deferred(); - if (resolved) { - result.resolve(value); - } else { - result.reject(value); - } - return result.promise; - }; + function reject(reason) { + var result = new Promise(); + rejectPromise(result, reason); + return result; + } - var handleCallback = function handleCallback(value, isResolved, callback) { + function handleCallback(value, resolver, callback) { var callbackOutput = null; try { if (isFunction(callback)) callbackOutput = callback(); } catch (e) { - return makePromise(e, false); + return reject(e); } if (isPromiseLike(callbackOutput)) { return callbackOutput.then(function() { - return makePromise(value, isResolved); - }, function(error) { - return makePromise(error, false); - }); + return resolver(value); + }, reject); } else { - return makePromise(value, isResolved); + return resolver(value); } - }; + } /** * @ngdoc method @@ -15238,11 +17038,11 @@ function qFactory(nextTick, exceptionHandler) { */ - var when = function(value, callback, errback, progressBack) { - var result = new Deferred(); - result.resolve(value); - return result.promise.then(callback, errback, progressBack); - }; + function when(value, callback, errback, progressBack) { + var result = new Promise(); + resolvePromise(result, value); + return result.then(callback, errback, progressBack); + } /** * @ngdoc method @@ -15277,63 +17077,86 @@ function qFactory(nextTick, exceptionHandler) { */ function all(promises) { - var deferred = new Deferred(), + var result = new Promise(), counter = 0, results = isArray(promises) ? [] : {}; forEach(promises, function(promise, key) { counter++; when(promise).then(function(value) { - if (results.hasOwnProperty(key)) return; results[key] = value; - if (!(--counter)) deferred.resolve(results); + if (!(--counter)) resolvePromise(result, results); }, function(reason) { - if (results.hasOwnProperty(key)) return; - deferred.reject(reason); + rejectPromise(result, reason); }); }); if (counter === 0) { - deferred.resolve(results); + resolvePromise(result, results); } + return result; + } + + /** + * @ngdoc method + * @name $q#race + * @kind function + * + * @description + * Returns a promise that resolves or rejects as soon as one of those promises + * resolves or rejects, with the value or reason from that promise. + * + * @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises. + * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises` + * resolves or rejects, with the value or reason from that promise. + */ + + function race(promises) { + var deferred = defer(); + + forEach(promises, function(promise) { + when(promise).then(deferred.resolve, deferred.reject); + }); + return deferred.promise; } - var $Q = function Q(resolver) { + function $Q(resolver) { if (!isFunction(resolver)) { - throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver); - } - - if (!(this instanceof Q)) { - // More useful when $Q is the Promise itself. - return new Q(resolver); + throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver); } - var deferred = new Deferred(); + var promise = new Promise(); function resolveFn(value) { - deferred.resolve(value); + resolvePromise(promise, value); } function rejectFn(reason) { - deferred.reject(reason); + rejectPromise(promise, reason); } resolver(resolveFn, rejectFn); - return deferred.promise; - }; + return promise; + } + + // Let's make the instanceof operator work for promises, so that + // `new $q(fn) instanceof $q` would evaluate to true. + $Q.prototype = Promise.prototype; $Q.defer = defer; $Q.reject = reject; $Q.when = when; $Q.resolve = resolve; $Q.all = all; + $Q.race = race; return $Q; } +/** @this */ function $$RAFProvider() { //rAF this.$get = ['$window', '$timeout', function($window, $timeout) { var requestAnimationFrame = $window.requestAnimationFrame || @@ -15423,6 +17246,8 @@ function $$RAFProvider() { //rAF /** * @ngdoc service * @name $rootScope + * @this + * * @description * * Every application has a single root {@link ng.$rootScope.Scope scope}. @@ -15467,14 +17292,19 @@ function $RootScopeProvider() { function cleanUpScope($scope) { + // Support: IE 9 only 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); + if ($scope.$$childHead) { + cleanUpScope($scope.$$childHead); + } + if ($scope.$$nextSibling) { + cleanUpScope($scope.$$nextSibling); + } } // The code below works around IE9 and V8's memory leaks @@ -15626,7 +17456,7 @@ function $RootScopeProvider() { // prototypically. In all other cases, this property needs to be set // when the parent scope is destroyed. // The listener needs to be added after the parent is set - if (isolate || parent != this) child.$on('$destroy', destroyChildScope); + if (isolate || parent !== this) child.$on('$destroy', destroyChildScope); return child; }, @@ -15643,7 +17473,7 @@ function $RootScopeProvider() { * $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). + * [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, @@ -15654,6 +17484,8 @@ function $RootScopeProvider() { * according to the {@link angular.equals} function. To save the value of the object for * later comparison, the {@link angular.copy} function is used. This therefore means that * watching complex objects will have adverse memory and performance implications. + * - This should not be used to watch for changes in objects that are + * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}. * - The watch `listener` may change the model, which may trigger other `listener`s to fire. * This is achieved by rerunning the watchers until no changes are detected. The rerun * iteration limit is 10 to prevent an infinite loop deadlock. @@ -15771,15 +17603,21 @@ function $RootScopeProvider() { if (!array) { array = scope.$$watchers = []; + array.$$digestWatchIndex = -1; } // we use unshift since we use a while loop in $digest for speed. // the while loop reads in reverse order. array.unshift(watcher); + array.$$digestWatchIndex++; incrementWatchersCount(this, 1); return function deregisterWatch() { - if (arrayRemove(array, watcher) >= 0) { + var index = arrayRemove(array, watcher); + if (index >= 0) { incrementWatchersCount(scope, -1); + if (index < array.$$digestWatchIndex) { + array.$$digestWatchIndex--; + } } lastDirtyWatch = null; }; @@ -15794,8 +17632,8 @@ function $RootScopeProvider() { * A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`. * If any one expression in the collection changes the `listener` is executed. * - * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every - * call to $digest() to see if any items changes. + * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return + * values are examined for changes on every call to `$digest`. * - The `listener` is called whenever any expression in the `watchExpressions` array changes. * * @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually @@ -15976,6 +17814,7 @@ function $RootScopeProvider() { oldItem = oldValue[i]; newItem = newValue[i]; + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -15998,6 +17837,7 @@ function $RootScopeProvider() { oldItem = oldValue[key]; if (key in oldValue) { + // eslint-disable-next-line no-self-compare bothNaN = (oldItem !== oldItem) && (newItem !== newItem); if (!bothNaN && (oldItem !== newItem)) { changeDetected++; @@ -16108,13 +17948,12 @@ function $RootScopeProvider() { * */ $digest: function() { - var watch, value, last, + var watch, value, last, fn, get, watchers, - length, dirty, ttl = TTL, next, current, target = this, watchLog = [], - logIdx, logMsg, asyncTask; + logIdx, asyncTask; beginPhase('$digest'); // Check for changes to browser url that happened in sync before the call to $digest @@ -16133,36 +17972,42 @@ function $RootScopeProvider() { dirty = false; current = target; - while (asyncQueue.length) { + // It's safe for asyncQueuePosition to be a local variable here because this loop can't + // be reentered recursively. Calling $digest from a function passed to $evalAsync would + // lead to a '$digest already in progress' error. + for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) { try { - asyncTask = asyncQueue.shift(); - asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals); + asyncTask = asyncQueue[asyncQueuePosition]; + fn = asyncTask.fn; + fn(asyncTask.scope, asyncTask.locals); } catch (e) { $exceptionHandler(e); } lastDirtyWatch = null; } + asyncQueue.length = 0; traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches - length = watchers.length; - while (length--) { + watchers.$$digestWatchIndex = watchers.length; + while (watchers.$$digestWatchIndex--) { try { - watch = watchers[length]; + watch = watchers[watchers.$$digestWatchIndex]; // 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' - && isNaN(value) && isNaN(last)))) { + : (isNumberNaN(value) && isNumberNaN(last)))) { 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] = []; @@ -16210,13 +18055,19 @@ function $RootScopeProvider() { clearPhase(); - while (postDigestQueue.length) { + // postDigestQueuePosition isn't local here because this loop can be reentered recursively. + while (postDigestQueuePosition < postDigestQueue.length) { try { - postDigestQueue.shift()(); + postDigestQueue[postDigestQueuePosition++](); } catch (e) { $exceptionHandler(e); } } + postDigestQueue.length = postDigestQueuePosition = 0; + + // Check for changes to browser url that happened during the $digest + // (for which no event is fired; e.g. via `history.pushState()`) + $browser.$$checkUrlChange(); }, @@ -16274,8 +18125,8 @@ function $RootScopeProvider() { // 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 && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling; - if (parent && 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; @@ -16362,7 +18213,7 @@ function $RootScopeProvider() { }); } - asyncQueue.push({scope: this, expression: expr, locals: locals}); + asyncQueue.push({scope: this, fn: $parse(expr), locals: locals}); }, $$postDigest: function(fn) { @@ -16429,6 +18280,7 @@ function $RootScopeProvider() { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); + // eslint-disable-next-line no-unsafe-finally throw e; } } @@ -16453,7 +18305,10 @@ function $RootScopeProvider() { */ $applyAsync: function(expr) { var scope = this; - expr && applyAsyncQueue.push($applyAsyncExpression); + if (expr) { + applyAsyncQueue.push($applyAsyncExpression); + } + expr = $parse(expr); scheduleApplyAsync(); function $applyAsyncExpression() { @@ -16670,6 +18525,8 @@ function $RootScopeProvider() { var postDigestQueue = $rootScope.$$postDigestQueue = []; var applyAsyncQueue = $rootScope.$$applyAsyncQueue = []; + var postDigestQueuePosition = 0; + return $rootScope; @@ -16729,6 +18586,22 @@ 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 + +/** + * @this * @description * Private service to sanitize uris for links and images. Used by $compile and $sanitize. */ @@ -16809,6 +18682,8 @@ function $$SanitizeUriProvider() { * Or gives undesired access to variables likes document or window? * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ +/* exported $SceProvider, $SceDelegateProvider */ + var $sceMinErr = minErr('$sce'); var SCE_CONTEXTS = { @@ -16823,6 +18698,13 @@ var SCE_CONTEXTS = { // Helper functions follow. +var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g; + +function snakeToCamel(name) { + return name + .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace); +} + function adjustMatcher(matcher) { if (matcher === 'self') { return matcher; @@ -16836,8 +18718,8 @@ function adjustMatcher(matcher) { 'Illegal sequence *** in string matcher. String: {0}', matcher); } matcher = escapeForRegexp(matcher). - replace('\\*\\*', '.*'). - replace('\\*', '[^:/.?&;]*'); + replace(/\\\*\\\*/g, '.*'). + replace(/\\\*/g, '[^:/.?&;]*'); return new RegExp('^' + matcher + '$'); } else if (isRegExp(matcher)) { // The only other type of matcher allowed is a Regexp. @@ -16892,6 +18774,8 @@ function adjustMatchers(matchers) { /** * @ngdoc provider * @name $sceDelegateProvider + * @this + * * @description * * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate @@ -16907,7 +18791,7 @@ function adjustMatchers(matchers) { * * - your app is hosted at url `http://myapp.example.com/` * - but some of your templates are hosted on other domains you control such as - * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. + * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc. * - and you have an open redirect at `http://myapp.example.com/clickThru?...`. * * Here is what a secure configuration for this scenario might look like: @@ -16942,13 +18826,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. * @@ -16971,17 +18857,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. * @@ -17186,6 +19072,8 @@ function $SceDelegateProvider() { /** * @ngdoc provider * @name $sceProvider + * @this + * * @description * * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service. @@ -17195,8 +19083,6 @@ function $SceDelegateProvider() { * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}. */ -/* jshint maxlen: false*/ - /** * @ngdoc service * @name $sce @@ -17221,7 +19107,7 @@ function $SceDelegateProvider() { * You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>` * to the top of your HTML document. * - * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for + * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for * security vulnerabilities such as XSS, clickjacking, etc. a lot easier. * * Here's an example of a binding in a privileged context: @@ -17329,7 +19215,7 @@ function $SceDelegateProvider() { * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. | * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. | * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. | - * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | + * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG`, `VIDEO`, `AUDIO`, `SOURCE`, and `TRACK` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. | * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. | * * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a> @@ -17381,7 +19267,7 @@ function $SceDelegateProvider() { * * ## Show me an example using SCE. * - * <example module="mySceApp" deps="angular-sanitize.js"> + * <example module="mySceApp" deps="angular-sanitize.js" name="sce-service"> * <file name="index.html"> * <div ng-controller="AppController as myCtrl"> * <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br> @@ -17402,10 +19288,10 @@ function $SceDelegateProvider() { * <file name="script.js"> * angular.module('mySceApp', ['ngSanitize']) * .controller('AppController', ['$http', '$templateCache', '$sce', - * function($http, $templateCache, $sce) { + * function AppController($http, $templateCache, $sce) { * var self = this; - * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) { - * self.userComments = userComments; + * $http.get('test_data.json', {cache: $templateCache}).then(function(response) { + * self.userComments = response.data; * }); * self.explicitlyTrustedHtml = $sce.trustAsHtml( * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + @@ -17428,12 +19314,12 @@ function $SceDelegateProvider() { * <file name="protractor.js" type="protractor"> * describe('SCE doc demo', function() { * it('should sanitize untrusted values', function() { - * expect(element.all(by.css('.htmlComment')).first().getInnerHtml()) + * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML')) * .toBe('<span>Is <i>anyone</i> reading this?</span>'); * }); * * it('should NOT sanitize explicitly trusted values', function() { - * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe( + * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe( * '<span onmouseover="this.textContent="Explicitly trusted HTML bypasses ' + * 'sanitization."">Hover over this text.</span>'); * }); @@ -17462,7 +19348,6 @@ function $SceDelegateProvider() { * ``` * */ -/* jshint maxlen: 100 */ function $SceProvider() { var enabled = true; @@ -17534,6 +19419,7 @@ function $SceProvider() { this.$get = ['$parse', '$sceDelegate', function( $parse, $sceDelegate) { + // Support: IE 9-11 only // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow // the "expression(javascript expression)" syntax which is insecure. if (enabled && msie < 8) { @@ -17847,13 +19733,13 @@ function $SceProvider() { forEach(SCE_CONTEXTS, function(enumValue, name) { var lName = lowercase(name); - sce[camelCase("parse_as_" + lName)] = function(expr) { + sce[snakeToCamel('parse_as_' + lName)] = function(expr) { return parse(enumValue, expr); }; - sce[camelCase("get_trusted_" + lName)] = function(value) { + sce[snakeToCamel('get_trusted_' + lName)] = function(value) { return getTrusted(enumValue, value); }; - sce[camelCase("trust_as_" + lName)] = function(value) { + sce[snakeToCamel('trust_as_' + lName)] = function(value) { return trustAs(enumValue, value); }; }); @@ -17862,12 +19748,15 @@ function $SceProvider() { }]; } +/* exported $SnifferProvider */ + /** * !!! This is an undocumented "private" service !!! * * @name $sniffer * @requires $window * @requires $document + * @this * * @property {boolean} history Does the browser support html5 history api ? * @property {boolean} transitions Does the browser support CSS transition events ? @@ -17879,37 +19768,32 @@ function $SceProvider() { function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. + // If not sandboxed, they can be detected by the presence of `chrome.app.runtime` + // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by + // the presence of an extension runtime ID and the absence of other Chrome runtime APIs + // (see https://developer.chrome.com/apps/manifest/sandbox). + // (NW.js apps have access to Chrome APIs, but do support `history`.) + isNw = $window.nw && $window.nw.process, + isChromePackagedApp = + !isNw && + $window.chrome && + ($window.chrome.app && $window.chrome.app.runtime || + !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id), + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), document = $document[0] || {}, - vendorPrefix, - vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/, bodyStyle = document.body && document.body.style, transitions = false, - animations = false, - match; + animations = false; if (bodyStyle) { - for (var prop in bodyStyle) { - if (match = vendorRegex.exec(prop)) { - vendorPrefix = match[0]; - vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1); - break; - } - } - - if (!vendorPrefix) { - vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit'; - } - - transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle)); - animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle)); - - if (android && (!transitions || !animations)) { - transitions = isString(bodyStyle.webkitTransition); - animations = isString(bodyStyle.webkitAnimation); - } + // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x + // Mentioned browsers need a -webkit- prefix for transitions & animations. + transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle); + animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle); } @@ -17922,16 +19806,15 @@ function $SnifferProvider() { // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined - // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), - // jshint +W018 + history: !!(hasHistoryPushState && !(android < 4) && !boxee), hasEvent: function(event) { + // Support: IE 9-11 only // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have // it. In particular the event is not fired when backspace or delete key are pressed or // when cut operation is performed. // IE10+ implements 'input' event but it erroneously fires under various situations, // e.g. when placeholder changes, or a form is focused. - if (event === 'input' && msie <= 11) return false; + if (event === 'input' && msie) return false; if (isUndefined(eventSupport[event])) { var divElm = document.createElement('div'); @@ -17941,7 +19824,6 @@ function $SnifferProvider() { return eventSupport[event]; }, csp: csp(), - vendorPrefix: vendorPrefix, transitions: transitions, animations: animations, android: android @@ -17949,11 +19831,13 @@ function $SnifferProvider() { }]; } -var $compileMinErr = minErr('$compile'); +var $templateRequestMinErr = minErr('$compile'); /** * @ngdoc provider * @name $templateRequestProvider + * @this + * * @description * Used to configure the options passed to the {@link $http} service when making a template request. * @@ -18007,57 +19891,64 @@ function $TemplateRequestProvider() { * * @property {number} totalPendingRequests total amount of pending template requests being downloaded. */ - this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) { - - function handleRequestFn(tpl, ignoreRequestError) { - handleRequestFn.totalPendingRequests++; - - // We consider the template cache holds only trusted templates, so - // there's no need to go through whitelisting again for keys that already - // are included in there. This also makes Angular accept any script - // directive, no matter its name. However, we still need to unwrap trusted - // types. - if (!isString(tpl) || !$templateCache.get(tpl)) { - tpl = $sce.getTrustedResourceUrl(tpl); - } + this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce', + function($exceptionHandler, $templateCache, $http, $q, $sce) { + + function handleRequestFn(tpl, ignoreRequestError) { + handleRequestFn.totalPendingRequests++; + + // We consider the template cache holds only trusted templates, so + // there's no need to go through whitelisting again for keys that already + // are included in there. This also makes Angular accept any script + // directive, no matter its name. However, we still need to unwrap trusted + // types. + if (!isString(tpl) || isUndefined($templateCache.get(tpl))) { + tpl = $sce.getTrustedResourceUrl(tpl); + } - var transformResponse = $http.defaults && $http.defaults.transformResponse; + var transformResponse = $http.defaults && $http.defaults.transformResponse; - if (isArray(transformResponse)) { - transformResponse = transformResponse.filter(function(transformer) { - return transformer !== defaultHttpResponseTransform; - }); - } else if (transformResponse === defaultHttpResponseTransform) { - transformResponse = null; - } + if (isArray(transformResponse)) { + transformResponse = transformResponse.filter(function(transformer) { + return transformer !== defaultHttpResponseTransform; + }); + } else if (transformResponse === defaultHttpResponseTransform) { + transformResponse = null; + } - return $http.get(tpl, extend({ - cache: $templateCache, - transformResponse: transformResponse - }, httpOptions)) - ['finally'](function() { - handleRequestFn.totalPendingRequests--; - }) - .then(function(response) { - $templateCache.put(tpl, response.data); - return response.data; - }, handleError); + return $http.get(tpl, extend({ + cache: $templateCache, + transformResponse: transformResponse + }, httpOptions)) + .finally(function() { + handleRequestFn.totalPendingRequests--; + }) + .then(function(response) { + $templateCache.put(tpl, response.data); + return response.data; + }, handleError); + + function handleError(resp) { + if (!ignoreRequestError) { + resp = $templateRequestMinErr('tpload', + 'Failed to load template: {0} (HTTP status: {1} {2})', + tpl, resp.status, resp.statusText); + + $exceptionHandler(resp); + } - function handleError(resp) { - if (!ignoreRequestError) { - throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})', - tpl, resp.status, resp.statusText); + return $q.reject(resp); } - return $q.reject(resp); } - } - handleRequestFn.totalPendingRequests = 0; + handleRequestFn.totalPendingRequests = 0; - return handleRequestFn; - }]; + return handleRequestFn; + } + ]; } +/** @this */ function $$TestabilityProvider() { this.$get = ['$rootScope', '$browser', '$location', function($rootScope, $browser, $location) { @@ -18096,7 +19987,7 @@ function $$TestabilityProvider() { matches.push(binding); } } else { - if (bindingName.indexOf(expression) != -1) { + if (bindingName.indexOf(expression) !== -1) { matches.push(binding); } } @@ -18173,6 +20064,7 @@ function $$TestabilityProvider() { }]; } +/** @this */ function $TimeoutProvider() { this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler', function($rootScope, $browser, $q, $$q, $exceptionHandler) { @@ -18205,8 +20097,8 @@ function $TimeoutProvider() { * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. * @param {...*=} Pass additional parameters to the executed function. - * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this - * promise will be resolved with is the return value of the `fn` function. + * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise + * will be resolved with the return value of the `fn` function. * */ function timeout(fn, delay, invokeApply) { @@ -18228,8 +20120,7 @@ function $TimeoutProvider() { } catch (e) { deferred.reject(e); $exceptionHandler(e); - } - finally { + } finally { delete deferreds[promise.$$timeoutId]; } @@ -18257,6 +20148,8 @@ function $TimeoutProvider() { */ timeout.cancel = function(promise) { if (promise && promise.$$timeoutId in deferreds) { + // Timeout cancels should not report an unhandled promise. + deferreds[promise.$$timeoutId].promise.catch(noop); deferreds[promise.$$timeoutId].reject('canceled'); delete deferreds[promise.$$timeoutId]; return $browser.defer.cancel(promise.$$timeoutId); @@ -18275,7 +20168,7 @@ function $TimeoutProvider() { // doesn't know about mocked locations and resolves URLs to the real document - which is // exactly the behavior needed here. There is little value is mocking these out for this // service. -var urlParsingNode = document.createElement("a"); +var urlParsingNode = window.document.createElement('a'); var originUrl = urlResolve(window.location.href); @@ -18288,7 +20181,7 @@ var originUrl = urlResolve(window.location.href); * URL will be resolved into an absolute URL in the context of the application document. * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related * properties are all populated to reflect the normalized URL. This approach has wide - * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * compatibility - Safari 1+, Mozilla 1+ etc. See * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html * * Implementation Notes for IE @@ -18327,10 +20220,11 @@ var originUrl = urlResolve(window.location.href); function urlResolve(url) { var href = url; + // Support: IE 9-11 only if (msie) { // Normalize before parse. Refer Implementation Notes on why this is // done in two steps on IE. - urlParsingNode.setAttribute("href", href); + urlParsingNode.setAttribute('href', href); href = urlParsingNode.href; } @@ -18367,6 +20261,7 @@ function urlIsSameOrigin(requestUrl) { /** * @ngdoc service * @name $window + * @this * * @description * A reference to the browser's `window` object. While `window` @@ -18380,7 +20275,7 @@ function urlIsSameOrigin(requestUrl) { * expression. * * @example - <example module="windowExample"> + <example module="windowExample" name="window-service"> <file name="index.html"> <script> angular.module('windowExample', []) @@ -18423,6 +20318,14 @@ function $$CookieReader($document) { var lastCookies = {}; var lastCookieString = ''; + function safeGetCookie(rawDocument) { + try { + return rawDocument.cookie || ''; + } catch (e) { + return ''; + } + } + function safeDecodeURIComponent(str) { try { return decodeURIComponent(str); @@ -18433,7 +20336,7 @@ function $$CookieReader($document) { return function() { var cookieArray, cookie, i, index, name; - var currentCookieString = rawDocument.cookie || ''; + var currentCookieString = safeGetCookie(rawDocument); if (currentCookieString !== lastCookieString) { lastCookieString = currentCookieString; @@ -18460,6 +20363,7 @@ function $$CookieReader($document) { $$CookieReader.$inject = ['$document']; +/** @this */ function $$CookieReaderProvider() { this.$get = $$CookieReader; } @@ -18539,9 +20443,15 @@ function $$CookieReaderProvider() { * @description * Filters are used for formatting data displayed to the user. * + * They can be used in view templates, controllers or services.Angular comes + * with a collection of [built-in filters](api/ng/filter), but it is easy to + * define your own as well. + * * The general syntax in templates is as follows: * - * {{ expression [| filter_name[:parameter_value] ... ] }} + * ```html + * {{ expression [| filter_name[:parameter_value] ... ] }} + * ``` * * @param {String} name Name of the filter function to retrieve * @return {Function} the filter function @@ -18564,6 +20474,7 @@ function $$CookieReaderProvider() { </example> */ $FilterProvider.$inject = ['$provide']; +/** @this */ function $FilterProvider($provide) { var suffix = 'Filter'; @@ -18613,7 +20524,7 @@ function $FilterProvider($provide) { lowercaseFilter: false, numberFilter: false, orderByFilter: false, - uppercaseFilter: false, + uppercaseFilter: false */ register('currency', currencyFilter); @@ -18636,6 +20547,9 @@ function $FilterProvider($provide) { * Selects a subset of items from `array` and returns it as a new array. * * @param {Array} array The source array. + * <div class="alert alert-info"> + * **Note**: If the array contains objects that reference themselves, filtering is not possible. + * </div> * @param {string|Object|function()} expression The predicate to be used for selecting items from * `array`. * @@ -18649,10 +20563,11 @@ function $FilterProvider($provide) { * - `Object`: A pattern object can be used to filter specific properties on objects contained * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items * which have property `name` containing "M" and property `phone` containing "1". A special - * property name `$` can be used (as in `{$:"text"}`) to accept a match against any - * property of the object or its nested object properties. That's equivalent to the simple - * substring match with a `string` as described above. The predicate can be negated by prefixing - * the string with `!`. + * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match + * against any property of the object or its nested object properties. That's equivalent to the + * simple substring match with a `string` as described above. The special property name can be + * overwritten, using the `anyPropertyKey` parameter. + * The predicate can be negated by prefixing the string with `!`. * For example `{name: "!M"}` predicate will return an array of items which have property `name` * not containing "M". * @@ -18667,7 +20582,7 @@ function $FilterProvider($provide) { * * 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 + * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in * determining if the expected value (from the filter expression) and actual value (from * the object in the array) should be considered a match. * @@ -18680,14 +20595,18 @@ function $FilterProvider($provide) { * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`. * This is essentially strict comparison of expected and actual. * - * - `false|undefined`: A short hand for a function which will look for a substring match in case - * insensitive way. + * - `false`: A short hand for a function which will look for a substring match in a 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). + * + * + * Defaults to `false`. * - * Primitive values are converted to strings. Objects are not compared against primitives, - * unless they have a custom `toString` method (e.g. `Date` objects). + * @param {string} [anyPropertyKey] The special property name that matches against any property. + * By default `$`. * * @example - <example> + <example name="filter-filter"> <file name="index.html"> <div ng-init="friends = [{name:'John', phone:'555-1276'}, {name:'Mary', phone:'800-BIG-MARY'}, @@ -18754,8 +20673,9 @@ function $FilterProvider($provide) { </file> </example> */ + function filterFilter() { - return function(array, expression, comparator) { + return function(array, expression, comparator, anyPropertyKey) { if (!isArrayLike(array)) { if (array == null) { return array; @@ -18764,6 +20684,7 @@ function filterFilter() { } } + anyPropertyKey = anyPropertyKey || '$'; var expressionType = getTypeForFilter(expression); var predicateFn; var matchAgainstAnyProp; @@ -18777,10 +20698,9 @@ function filterFilter() { case 'number': case 'string': matchAgainstAnyProp = true; - //jshint -W086 + // falls through case 'object': - //jshint +W086 - predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp); + predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp); break; default: return array; @@ -18791,8 +20711,8 @@ function filterFilter() { } // Helper functions for `filterFilter` -function createPredicateFn(expression, comparator, matchAgainstAnyProp) { - var shouldMatchPrimitives = isObject(expression) && ('$' in expression); +function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) { + var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression); var predicateFn; if (comparator === true) { @@ -18820,25 +20740,25 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) { predicateFn = function(item) { if (shouldMatchPrimitives && !isObject(item)) { - return deepCompare(item, expression.$, comparator, false); + return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false); } - return deepCompare(item, expression, comparator, matchAgainstAnyProp); + return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp); }; return predicateFn; } -function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) { +function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) { var actualType = getTypeForFilter(actual); var expectedType = getTypeForFilter(expected); if ((expectedType === 'string') && (expected.charAt(0) === '!')) { - return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp); + return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp); } else if (isArray(actual)) { // In case `actual` is an array, consider it a match // if ANY of it's items matches `expected` return actual.some(function(item) { - return deepCompare(item, expected, comparator, matchAgainstAnyProp); + return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp); }); } @@ -18847,11 +20767,14 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc var key; if (matchAgainstAnyProp) { for (key in actual) { - if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) { + // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined + // See: https://github.com/angular/angular.js/issues/15644 + if (key.charAt && (key.charAt(0) !== '$') && + deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) { return true; } } - return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false); + return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false); } else if (expectedType === 'object') { for (key in expected) { var expectedVal = expected[key]; @@ -18859,9 +20782,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc continue; } - var matchAnyProperty = key === '$'; + var matchAnyProperty = key === anyPropertyKey; var actualVal = matchAnyProperty ? actual : actual[key]; - if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) { + if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) { return false; } } @@ -18869,7 +20792,6 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc } else { return comparator(actual, expected); } - break; case 'function': return false; default: @@ -18882,6 +20804,10 @@ function getTypeForFilter(val) { return (val === null) ? 'null' : typeof val; } +var MAX_DIGITS = 22; +var DECIMAL_SEP = '.'; +var ZERO_CHAR = '0'; + /** * @ngdoc filter * @name currency @@ -18898,7 +20824,7 @@ function getTypeForFilter(val) { * * * @example - <example module="currencyExample"> + <example module="currencyExample" name="currency-filter"> <file name="index.html"> <script> angular.module('currencyExample', []) @@ -18909,7 +20835,7 @@ function getTypeForFilter(val) { <div ng-controller="ExampleController"> <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> + custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span><br> no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span> </div> </file> @@ -18920,7 +20846,7 @@ function getTypeForFilter(val) { expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235'); }); it('should update', function() { - if (browser.params.browser == 'safari') { + if (browser.params.browser === 'safari') { // Safari does not understand the minus key. See // https://github.com/angular/protractor/issues/481 return; @@ -18963,7 +20889,7 @@ function currencyFilter($locale) { * Formats a number as text. * * If the input is null or undefined, it will just be returned. - * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned. + * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively. * If the input is not a number an empty string is returned. * * @@ -18971,10 +20897,12 @@ function currencyFilter($locale) { * @param {(number|string)=} fractionSize Number of decimal places to round the number to. * If this is not provided then the fraction size is computed from the current locale's number * formatting pattern. In the case of the default locale, it will be 3. - * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit. + * @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current + * locale (e.g., in the en_US locale it will have "." as the decimal separator and + * include "," group separators after each third digit). * * @example - <example module="numberFilterExample"> + <example module="numberFilterExample" name="number-filter"> <file name="index.html"> <script> angular.module('numberFilterExample', []) @@ -19006,8 +20934,6 @@ function currencyFilter($locale) { </file> </example> */ - - numberFilter.$inject = ['$locale']; function numberFilter($locale) { var formats = $locale.NUMBER_FORMATS; @@ -19021,103 +20947,227 @@ function numberFilter($locale) { }; } -var DECIMAL_SEP = '.'; -function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { - if (isObject(number)) return ''; +/** + * Parse a number (as a string) into three components that can be used + * for formatting the number. + * + * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/) + * + * @param {string} numStr The number to parse + * @return {object} An object describing this number, containing the following keys: + * - d : an array of digits containing leading zeros as necessary + * - i : the number of the digits in `d` that are to the left of the decimal point + * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d` + * + */ +function parse(numStr) { + var exponent = 0, digits, numberOfIntegerDigits; + var i, j, zeros; - var isNegative = number < 0; - number = Math.abs(number); + // Decimal point? + if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) { + numStr = numStr.replace(DECIMAL_SEP, ''); + } - var isInfinity = number === Infinity; - if (!isInfinity && !isFinite(number)) return ''; + // Exponential form? + if ((i = numStr.search(/e/i)) > 0) { + // Work out the exponent. + if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i; + numberOfIntegerDigits += +numStr.slice(i + 1); + numStr = numStr.substring(0, i); + } else if (numberOfIntegerDigits < 0) { + // There was no decimal point or exponent so it is an integer. + numberOfIntegerDigits = numStr.length; + } - var numStr = number + '', - formatedText = '', - hasExponent = false, - parts = []; + // Count the number of leading zeros. + for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ } - if (isInfinity) formatedText = '\u221e'; + if (i === (zeros = numStr.length)) { + // The digits are all zero. + digits = [0]; + numberOfIntegerDigits = 1; + } else { + // Count the number of trailing zeros + zeros--; + while (numStr.charAt(zeros) === ZERO_CHAR) zeros--; - if (!isInfinity && numStr.indexOf('e') !== -1) { - var match = numStr.match(/([\d\.]+)e(-?)(\d+)/); - if (match && match[2] == '-' && match[3] > fractionSize + 1) { - number = 0; - } else { - formatedText = numStr; - hasExponent = true; + // Trailing zeros are insignificant so ignore them + numberOfIntegerDigits -= i; + digits = []; + // Convert string to array of digits without leading/trailing zeros. + for (j = 0; i <= zeros; i++, j++) { + digits[j] = +numStr.charAt(i); } } - if (!isInfinity && !hasExponent) { - var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length; + // If the number overflows the maximum allowed digits then use an exponent. + if (numberOfIntegerDigits > MAX_DIGITS) { + digits = digits.splice(0, MAX_DIGITS - 1); + exponent = numberOfIntegerDigits - 1; + numberOfIntegerDigits = 1; + } - // determine fractionSize if it is not specified - if (isUndefined(fractionSize)) { - fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac); - } + return { d: digits, e: exponent, i: numberOfIntegerDigits }; +} + +/** + * Round the parsed number to the specified number of decimal places + * This function changed the parsedNumber in-place + */ +function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) { + var digits = parsedNumber.d; + var fractionLen = digits.length - parsedNumber.i; - // safely round numbers in JS without hitting imprecisions of floating-point arithmetics - // inspired by: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round - number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize); + // determine fractionSize if it is not specified; `+fractionSize` converts it to a number + fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize; - var fraction = ('' + number).split(DECIMAL_SEP); - var whole = fraction[0]; - fraction = fraction[1] || ''; + // The index of the digit to where rounding is to occur + var roundAt = fractionSize + parsedNumber.i; + var digit = digits[roundAt]; - var i, pos = 0, - lgroup = pattern.lgSize, - group = pattern.gSize; + if (roundAt > 0) { + // Drop fractional digits beyond `roundAt` + digits.splice(Math.max(parsedNumber.i, roundAt)); - if (whole.length >= (lgroup + group)) { - pos = whole.length - lgroup; - for (i = 0; i < pos; i++) { - if ((pos - i) % group === 0 && i !== 0) { - formatedText += groupSep; + // Set non-fractional digits beyond `roundAt` to 0 + for (var j = roundAt; j < digits.length; j++) { + digits[j] = 0; + } + } else { + // We rounded to zero so reset the parsedNumber + fractionLen = Math.max(0, fractionLen); + parsedNumber.i = 1; + digits.length = Math.max(1, roundAt = fractionSize + 1); + digits[0] = 0; + for (var i = 1; i < roundAt; i++) digits[i] = 0; + } + + if (digit >= 5) { + if (roundAt - 1 < 0) { + for (var k = 0; k > roundAt; k--) { + digits.unshift(0); + parsedNumber.i++; } - formatedText += whole.charAt(i); + digits.unshift(1); + parsedNumber.i++; + } else { + digits[roundAt - 1]++; } } - for (i = pos; i < whole.length; i++) { - if ((whole.length - i) % lgroup === 0 && i !== 0) { - formatedText += groupSep; - } - formatedText += whole.charAt(i); - } + // Pad out with zeros to get the required fraction length + for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0); + - // format fraction part. - while (fraction.length < fractionSize) { - fraction += '0'; + // Do any carrying, e.g. a digit was rounded up to 10 + var carry = digits.reduceRight(function(carry, d, i, digits) { + d = d + carry; + digits[i] = d % 10; + return Math.floor(d / 10); + }, 0); + if (carry) { + digits.unshift(carry); + parsedNumber.i++; } +} + +/** + * Format a number into a string + * @param {number} number The number to format + * @param {{ + * minFrac, // the minimum number of digits required in the fraction part of the number + * maxFrac, // the maximum number of digits required in the fraction part of the number + * gSize, // number of digits in each group of separated digits + * lgSize, // number of digits in the last group of digits before the decimal separator + * negPre, // the string to go in front of a negative number (e.g. `-` or `(`)) + * posPre, // the string to go in front of a positive number + * negSuf, // the string to go after a negative number (e.g. `)`) + * posSuf // the string to go after a positive number + * }} pattern + * @param {string} groupSep The string to separate groups of number (e.g. `,`) + * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`) + * @param {[type]} fractionSize The size of the fractional part of the number + * @return {string} The number formatted as a string + */ +function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) { + + if (!(isString(number) || isNumber(number)) || isNaN(number)) return ''; - if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize); + var isInfinity = !isFinite(number); + var isZero = false; + var numStr = Math.abs(number) + '', + formattedText = '', + parsedNumber; + + if (isInfinity) { + formattedText = '\u221e'; } else { - if (fractionSize > 0 && number < 1) { - formatedText = number.toFixed(fractionSize); - number = parseFloat(formatedText); - formatedText = formatedText.replace(DECIMAL_SEP, decimalSep); + parsedNumber = parse(numStr); + + roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac); + + var digits = parsedNumber.d; + var integerLen = parsedNumber.i; + var exponent = parsedNumber.e; + var decimals = []; + isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true); + + // pad zeros for small numbers + while (integerLen < 0) { + digits.unshift(0); + integerLen++; } - } - if (number === 0) { - isNegative = false; - } + // extract decimals digits + if (integerLen > 0) { + decimals = digits.splice(integerLen, digits.length); + } else { + decimals = digits; + digits = [0]; + } + + // format the integer digits with grouping separators + var groups = []; + if (digits.length >= pattern.lgSize) { + groups.unshift(digits.splice(-pattern.lgSize, digits.length).join('')); + } + while (digits.length > pattern.gSize) { + groups.unshift(digits.splice(-pattern.gSize, digits.length).join('')); + } + if (digits.length) { + groups.unshift(digits.join('')); + } + formattedText = groups.join(groupSep); + + // append the decimal digits + if (decimals.length) { + formattedText += decimalSep + decimals.join(''); + } - parts.push(isNegative ? pattern.negPre : pattern.posPre, - formatedText, - isNegative ? pattern.negSuf : pattern.posSuf); - return parts.join(''); + if (exponent) { + formattedText += 'e+' + exponent; + } + } + if (number < 0 && !isZero) { + return pattern.negPre + formattedText + pattern.negSuf; + } else { + return pattern.posPre + formattedText + pattern.posSuf; + } } -function padNumber(num, digits, trim) { +function padNumber(num, digits, trim, negWrap) { var neg = ''; - if (num < 0) { - neg = '-'; - num = -num; + if (num < 0 || (negWrap && num <= 0)) { + if (negWrap) { + num = -num + 1; + } else { + num = -num; + neg = '-'; + } } num = '' + num; - while (num.length < digits) num = '0' + num; + while (num.length < digits) num = ZERO_CHAR + num; if (trim) { num = num.substr(num.length - digits); } @@ -19125,22 +21175,23 @@ function padNumber(num, digits, trim) { } -function dateGetter(name, size, offset, trim) { +function dateGetter(name, size, offset, trim, negWrap) { offset = offset || 0; return function(date) { var value = date['get' + name](); if (offset > 0 || value > -offset) { value += offset; } - if (value === 0 && offset == -12) value = 12; - return padNumber(value, size, trim); + if (value === 0 && offset === -12) value = 12; + return padNumber(value, size, trim, negWrap); }; } -function dateStrGetter(name, shortForm) { +function dateStrGetter(name, shortForm, standAlone) { return function(date, formats) { var value = date['get' + name](); - var get = uppercase(shortForm ? ('SHORT' + name) : name); + var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : ''); + var get = uppercase(propPrefix + name); return formats[get][value]; }; @@ -19148,7 +21199,7 @@ function dateStrGetter(name, shortForm) { function timeZoneGetter(date, formats, offset) { var zone = -1 * offset; - var paddedZone = (zone >= 0) ? "+" : ""; + var paddedZone = (zone >= 0) ? '+' : ''; paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) + padNumber(Math.abs(zone % 60), 2); @@ -19195,13 +21246,14 @@ function longEraGetter(date, formats) { } var DATE_FORMATS = { - yyyy: dateGetter('FullYear', 4), - yy: dateGetter('FullYear', 2, 0, true), - y: dateGetter('FullYear', 1), + yyyy: dateGetter('FullYear', 4, 0, false, true), + yy: dateGetter('FullYear', 2, 0, true, true), + y: dateGetter('FullYear', 1, 0, false, true), MMMM: dateStrGetter('Month'), MMM: dateStrGetter('Month', true), MM: dateGetter('Month', 2, 1), M: dateGetter('Month', 1, 1), + LLLL: dateStrGetter('Month', false, true), dd: dateGetter('Date', 2), d: dateGetter('Date', 1), HH: dateGetter('Hours', 2), @@ -19227,8 +21279,8 @@ var DATE_FORMATS = { GGGG: longEraGetter }; -var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, - NUMBER_STRING = /^\-?\d+$/; +var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/, + NUMBER_STRING = /^-?\d+$/; /** * @ngdoc filter @@ -19247,6 +21299,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| * * `'MMM'`: Month in year (Jan-Dec) * * `'MM'`: Month in year, padded (01-12) * * `'M'`: Month in year (1-12) + * * `'LLLL'`: Stand-alone month in year (January-December) * * `'dd'`: Day in month, padded (01-31) * * `'d'`: Day in month (1-31) * * `'EEEE'`: Day in Week,(Sunday-Saturday) @@ -19298,7 +21351,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| * @returns {string} Formatted string or the input if input is not recognized as date/millis. * * @example - <example> + <example name="filter-date"> <file name="index.html"> <span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>: <span>{{1288323623006 | date:'medium'}}</span><br> @@ -19314,7 +21367,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+| expect(element(by.binding("1288323623006 | date:'medium'")).getText()). toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/); expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()). - toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/); + toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/); expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()). toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/); expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()). @@ -19331,7 +21384,7 @@ function dateFilter($locale) { // 1 2 3 4 5 6 7 8 9 10 11 function jsonStringToDate(string) { var match; - if (match = string.match(R_ISO8601_STR)) { + if ((match = string.match(R_ISO8601_STR))) { var date = new Date(0), tzHour = 0, tzMin = 0, @@ -19386,13 +21439,13 @@ function dateFilter($locale) { var dateTimezoneOffset = date.getTimezoneOffset(); if (timezone) { - dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset()); + dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset); date = convertTimezoneToLocal(date, timezone, true); } forEach(parts, function(value) { fn = DATE_FORMATS[value]; text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset) - : value.replace(/(^'|'$)/g, '').replace(/''/g, "'"); + : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\''); }); return text; @@ -19417,15 +21470,15 @@ function dateFilter($locale) { * * * @example - <example> + <example name="filter-json"> <file name="index.html"> <pre id="default-spacing">{{ {'name':'value'} | json }}</pre> <pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre> </file> <file name="protractor.js" type="protractor"> it('should jsonify filtered objects', function() { - expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); - expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/); + expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/); + expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/); }); </file> </example> @@ -19468,24 +21521,25 @@ var uppercaseFilter = valueFn(uppercase); * @kind function * * @description - * Creates a new array or string containing only a specified number of elements. The elements - * are taken from either the beginning or the end of the source array, string or number, as specified by - * the value and sign (positive or negative) of `limit`. If a number is used as input, it is - * converted to a string. - * - * @param {Array|string|number} input Source array, string or number to be limited. - * @param {string|number} limit The length of the returned array or string. If the `limit` number + * Creates a new array or string containing only a specified number of elements. The elements are + * taken from either the beginning or the end of the source array, string or number, as specified by + * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported + * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input, + * it is converted to a string. + * + * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited. + * @param {string|number} limit - The length of the returned array or string. If the `limit` number * is positive, `limit` number of items from the beginning of the source array/string are copied. * If the number is negative, `limit` number of items from the end of the source array/string * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined, * the input will be returned unchanged. - * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin` - * indicates an offset from the end of `input`. Defaults to `0`. - * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array - * had less than `limit` elements. + * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index, + * `begin` indicates an offset from the end of `input`. Defaults to `0`. + * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had + * less than `limit` elements. * * @example - <example module="limitToExample"> + <example module="limitToExample" name="limit-to-filter"> <file name="index.html"> <script> angular.module('limitToExample', []) @@ -19567,81 +21621,163 @@ function limitToFilter() { } else { limit = toInt(limit); } - if (isNaN(limit)) return input; + if (isNumberNaN(limit)) return input; if (isNumber(input)) input = input.toString(); - if (!isArray(input) && !isString(input)) return input; + if (!isArrayLike(input)) return input; begin = (!begin || isNaN(begin)) ? 0 : toInt(begin); begin = (begin < 0) ? Math.max(0, input.length + begin) : begin; if (limit >= 0) { - return input.slice(begin, begin + limit); + return sliceFn(input, begin, begin + limit); } else { if (begin === 0) { - return input.slice(limit, input.length); + return sliceFn(input, limit, input.length); } else { - return input.slice(Math.max(0, begin + limit), begin); + return sliceFn(input, Math.max(0, begin + limit), begin); } } }; } +function sliceFn(input, begin, end) { + if (isString(input)) return input.slice(begin, end); + + return slice.call(input, begin, end); +} + /** * @ngdoc filter * @name orderBy * @kind function * * @description - * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically - * for strings and numerically for numbers. Note: if you notice numbers are not being sorted - * as expected, make sure they are actually being saved as numbers and not strings. - * Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported. + * Returns an array containing the items from the specified `collection`, ordered by a `comparator` + * function based on the values computed using the `expression` predicate. + * + * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in + * `[{id: 'bar'}, {id: 'foo'}]`. + * + * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray, + * String, etc). + * + * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker + * for the preceding one. The `expression` is evaluated against each item and the output is used + * for comparing with other items. + * + * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in + * ascending order. * - * @param {Array} array The array (or array-like object) to sort. - * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be - * used by the comparator to determine the order of elements. + * The comparison is done using the `comparator` function. If none is specified, a default, built-in + * comparator is used (see below for details - in a nutshell, it compares numbers numerically and + * strings alphabetically). + * + * ### Under the hood + * + * Ordering the specified `collection` happens in two phases: + * + * 1. All items are passed through the predicate (or predicates), and the returned values are saved + * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed + * through a predicate that extracts the value of the `label` property, would be transformed to: + * ``` + * { + * value: 'foo', + * type: 'string', + * index: ... + * } + * ``` + * 2. The comparator function is used to sort the items, based on the derived values, types and + * indices. + * + * If you use a custom comparator, it will be called with pairs of objects of the form + * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal + * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the + * second, or `1` otherwise. + * + * In order to ensure that the sorting will be deterministic across platforms, if none of the + * specified predicates can distinguish between two items, `orderBy` will automatically introduce a + * dummy predicate that returns the item's index as `value`. + * (If you are using a custom comparator, make sure it can handle this predicate as well.) + * + * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted + * value for an item, `orderBy` will try to convert that object to a primitive value, before passing + * it to the comparator. The following rules govern the conversion: + * + * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be + * used instead.<br /> + * (If the object has a `valueOf()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that + * returns a primitive, its return value will be used instead.<br /> + * (If the object has a `toString()` method that returns another object, then the returned object + * will be used in subsequent steps.) + * 3. No conversion; the object itself is used. + * + * ### The default comparator + * + * The default, built-in comparator should be sufficient for most usecases. In short, it compares + * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to + * using their index in the original collection, and sorts values of different types by type. + * + * More specifically, it follows these steps to determine the relative order of items: + * + * 1. If the compared values are of different types, compare the types themselves alphabetically. + * 2. If both values are of type `string`, compare them alphabetically in a case- and + * locale-insensitive way. + * 3. If both values are objects, compare their indices instead. + * 4. Otherwise, return: + * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`). + * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator). + * - `1`, otherwise. + * + * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being + * saved as numbers and not strings. + * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e. + * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to + * other values. + * + * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort. + * @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of + * predicates) to be used by the comparator to determine the order of elements. * * Can be one of: * - * - `function`: Getter function. The result of this function will be sorted using the - * `<`, `===`, `>` operator. - * - `string`: An Angular expression. The result of this expression is used to compare elements - * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by - * 3 first characters of a property called `name`). The result of a constant expression - * is interpreted as a property name to be used in comparisons (for example `"special name"` - * to sort object by the value of their `special name` property). An expression can be - * optionally prefixed with `+` or `-` to control ascending or descending sort order - * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array - * element itself is used to compare where sorting. - * - `Array`: An array of function or string predicates. The first predicate in the array - * is used for sorting, but when two items are equivalent, the next predicate is used. + * - `Function`: A getter function. This function will be called with each item as argument and + * the return value will be used for sorting. + * - `string`: An Angular expression. This expression will be evaluated against each item and the + * result will be used for sorting. For example, use `'label'` to sort by a property called + * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label` + * property.<br /> + * (The result of a constant expression is interpreted as a property name to be used for + * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a + * property called `special name`.)<br /> + * An expression can be optionally prefixed with `+` or `-` to control the sorting direction, + * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided, + * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons. + * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the + * relative order of two items, the next predicate is used as a tie-breaker. + * + * **Note:** If the predicate is missing or empty then it defaults to `'+'`. * - * If the predicate is missing or empty then it defaults to `'+'`. + * @param {boolean=} reverse - If `true`, reverse the sorting order. + * @param {(Function)=} comparator - The comparator function used to determine the relative order of + * value pairs. If omitted, the built-in comparator will be used. * - * @param {boolean=} reverse Reverse the order of the array. - * @returns {Array} Sorted copy of the source array. + * @returns {Array} - The sorted array. * * * @example - * The example below demonstrates a simple ngRepeat, where the data is sorted - * by age in descending order (predicate is set to `'-age'`). - * `reverse` is not set, which means it defaults to `false`. - <example module="orderByExample"> + * ### Ordering a table with `ngRepeat` + * + * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by + * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means + * it defaults to the built-in comparator. + * + <example name="orderBy-static" module="orderByExample1"> <file name="index.html"> - <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"> + <table class="friends"> <tr> <th>Name</th> <th>Phone Number</th> @@ -19655,58 +21791,78 @@ function limitToFilter() { </table> </div> </file> + <file name="script.js"> + angular.module('orderByExample1', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + }]); + </file> + <file name="style.css"> + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + </file> + <file name="protractor.js" type="protractor"> + // Element locators + var names = element.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by age in reverse order', function() { + expect(names.get(0).getText()).toBe('Adam'); + expect(names.get(1).getText()).toBe('Julie'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('John'); + }); + </file> </example> + * <hr /> * - * The predicate and reverse parameters can be controlled dynamically through scope properties, - * as shown in the next example. * @example - <example module="orderByExample"> + * ### Changing parameters dynamically + * + * All parameters can be changed dynamically. The next example shows how you can make the columns of + * a table sortable, by binding the `expression` and `reverse` parameters to scope properties. + * + <example name="orderBy-dynamic" module="orderByExample2"> <file name="index.html"> - <script> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.friends = - [{name:'John', phone:'555-1212', age:10}, - {name:'Mary', phone:'555-9876', age:19}, - {name:'Mike', phone:'555-4321', age:21}, - {name:'Adam', phone:'555-5678', age:35}, - {name:'Julie', phone:'555-8765', age:29}]; - $scope.predicate = 'age'; - $scope.reverse = true; - $scope.order = function(predicate) { - $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false; - $scope.predicate = predicate; - }; - }]); - </script> - <style type="text/css"> - .sortorder:after { - content: '\25b2'; - } - .sortorder.reverse:after { - content: '\25bc'; - } - </style> <div ng-controller="ExampleController"> - <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre> + <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre> + <hr/> + <button ng-click="propertyName = null; reverse = false">Set to unsorted</button> <hr/> - [ <a href="" ng-click="predicate=''">unsorted</a> ] - <table class="friend"> + <table class="friends"> <tr> <th> - <a href="" ng-click="order('name')">Name</a> - <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span> + <button ng-click="sortBy('name')">Name</button> + <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span> </th> <th> - <a href="" ng-click="order('phone')">Phone Number</a> - <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span> + <button ng-click="sortBy('phone')">Phone Number</button> + <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span> </th> <th> - <a href="" ng-click="order('age')">Age</a> - <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span> + <button ng-click="sortBy('age')">Age</button> + <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span> </th> </tr> - <tr ng-repeat="friend in friends | orderBy:predicate:reverse"> + <tr ng-repeat="friend in friends | orderBy:propertyName:reverse"> <td>{{friend.name}}</td> <td>{{friend.phone}}</td> <td>{{friend.age}}</td> @@ -19714,56 +21870,336 @@ function limitToFilter() { </table> </div> </file> + <file name="script.js"> + angular.module('orderByExample2', []) + .controller('ExampleController', ['$scope', function($scope) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = friends; + + $scope.sortBy = function(propertyName) { + $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false; + $scope.propertyName = propertyName; + }; + }]); + </file> + <file name="style.css"> + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + </file> + <file name="protractor.js" type="protractor"> + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + </file> </example> + * <hr /> * - * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the - * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the - * desired parameters. + * @example + * ### Using `orderBy` inside a controller * - * Example: + * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and + * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory + * and retrieve the `orderBy` filter with `$filter('orderBy')`.) + * + <example name="orderBy-call-manually" module="orderByExample3"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre> + <hr/> + <button ng-click="sortBy(null)">Set to unsorted</button> + <hr/> + <table class="friends"> + <tr> + <th> + <button ng-click="sortBy('name')">Name</button> + <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span> + </th> + <th> + <button ng-click="sortBy('phone')">Phone Number</button> + <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span> + </th> + <th> + <button ng-click="sortBy('age')">Age</button> + <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span> + </th> + </tr> + <tr ng-repeat="friend in friends"> + <td>{{friend.name}}</td> + <td>{{friend.phone}}</td> + <td>{{friend.age}}</td> + </tr> + </table> + </div> + </file> + <file name="script.js"> + angular.module('orderByExample3', []) + .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) { + var friends = [ + {name: 'John', phone: '555-1212', age: 10}, + {name: 'Mary', phone: '555-9876', age: 19}, + {name: 'Mike', phone: '555-4321', age: 21}, + {name: 'Adam', phone: '555-5678', age: 35}, + {name: 'Julie', phone: '555-8765', age: 29} + ]; + + $scope.propertyName = 'age'; + $scope.reverse = true; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + + $scope.sortBy = function(propertyName) { + $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName) + ? !$scope.reverse : false; + $scope.propertyName = propertyName; + $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse); + }; + }]); + </file> + <file name="style.css"> + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + + .sortorder:after { + content: '\25b2'; // BLACK UP-POINTING TRIANGLE + } + .sortorder.reverse:after { + content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE + } + </file> + <file name="protractor.js" type="protractor"> + // Element locators + var unsortButton = element(by.partialButtonText('unsorted')); + var nameHeader = element(by.partialButtonText('Name')); + var phoneHeader = element(by.partialButtonText('Phone')); + var ageHeader = element(by.partialButtonText('Age')); + var firstName = element(by.repeater('friends').column('friend.name').row(0)); + var lastName = element(by.repeater('friends').column('friend.name').row(4)); + + it('should sort friends by some property, when clicking on the column header', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + phoneHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Mary'); + + nameHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('Mike'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + }); + + it('should sort friends in reverse order, when clicking on the same column', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + ageHeader.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Adam'); + + ageHeader.click(); + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + }); + + it('should restore the original order, when clicking "Set to unsorted"', function() { + expect(firstName.getText()).toBe('Adam'); + expect(lastName.getText()).toBe('John'); + + unsortButton.click(); + expect(firstName.getText()).toBe('John'); + expect(lastName.getText()).toBe('Julie'); + }); + </file> + </example> + * <hr /> * * @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> - </file> + * ### Using a custom comparator + * + * If you have very specific requirements about the way items are sorted, you can pass your own + * comparator function. For example, you might need to compare some strings in a locale-sensitive + * way. (When specifying a custom comparator, you also need to pass a value for the `reverse` + * argument - passing `false` retains the default sorting order, i.e. ascending.) + * + <example name="orderBy-custom-comparator" module="orderByExample4"> + <file name="index.html"> + <div ng-controller="ExampleController"> + <div class="friends-container custom-comparator"> + <h3>Locale-sensitive Comparator</h3> + <table class="friends"> + <tr> + <th>Name</th> + <th>Favorite Letter</th> + </tr> + <tr ng-repeat="friend in friends | orderBy:'favoriteLetter':false:localeSensitiveComparator"> + <td>{{friend.name}}</td> + <td>{{friend.favoriteLetter}}</td> + </tr> + </table> + </div> + <div class="friends-container default-comparator"> + <h3>Default Comparator</h3> + <table class="friends"> + <tr> + <th>Name</th> + <th>Favorite Letter</th> + </tr> + <tr ng-repeat="friend in friends | orderBy:'favoriteLetter'"> + <td>{{friend.name}}</td> + <td>{{friend.favoriteLetter}}</td> + </tr> + </table> + </div> + </div> + </file> + <file name="script.js"> + angular.module('orderByExample4', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.friends = [ + {name: 'John', favoriteLetter: 'Ä'}, + {name: 'Mary', favoriteLetter: 'Ü'}, + {name: 'Mike', favoriteLetter: 'Ö'}, + {name: 'Adam', favoriteLetter: 'H'}, + {name: 'Julie', favoriteLetter: 'Z'} + ]; + + $scope.localeSensitiveComparator = function(v1, v2) { + // If we don't get strings, just compare by index + if (v1.type !== 'string' || v2.type !== 'string') { + return (v1.index < v2.index) ? -1 : 1; + } - <file name="script.js"> - angular.module('orderByExample', []) - .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) { - var orderBy = $filter('orderBy'); - $scope.friends = [ - { name: 'John', phone: '555-1212', age: 10 }, - { name: 'Mary', phone: '555-9876', age: 19 }, - { name: 'Mike', phone: '555-4321', age: 21 }, - { name: 'Adam', phone: '555-5678', age: 35 }, - { name: 'Julie', phone: '555-8765', age: 29 } - ]; - $scope.order = function(predicate, reverse) { - $scope.friends = orderBy($scope.friends, predicate, reverse); - }; - $scope.order('-age',false); - }]); - </file> -</example> + // Compare strings alphabetically, taking locale into account + return v1.value.localeCompare(v2.value); + }; + }]); + </file> + <file name="style.css"> + .friends-container { + display: inline-block; + margin: 0 30px; + } + + .friends { + border-collapse: collapse; + } + + .friends th { + border-bottom: 1px solid; + } + .friends td, .friends th { + border-left: 1px solid; + padding: 5px 10px; + } + .friends td:first-child, .friends th:first-child { + border-left: none; + } + </file> + <file name="protractor.js" type="protractor"> + // Element locators + var container = element(by.css('.custom-comparator')); + var names = container.all(by.repeater('friends').column('friend.name')); + + it('should sort friends by favorite letter (in correct alphabetical order)', function() { + expect(names.get(0).getText()).toBe('John'); + expect(names.get(1).getText()).toBe('Adam'); + expect(names.get(2).getText()).toBe('Mike'); + expect(names.get(3).getText()).toBe('Mary'); + expect(names.get(4).getText()).toBe('Julie'); + }); + </file> + </example> + * */ orderByFilter.$inject = ['$parse']; function orderByFilter($parse) { - return function(array, sortPredicate, reverseOrder) { + return function(array, sortPredicate, reverseOrder, compareFn) { if (array == null) return array; if (!isArrayLike(array)) { @@ -19773,11 +22209,12 @@ function orderByFilter($parse) { if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; } if (sortPredicate.length === 0) { sortPredicate = ['+']; } - var predicates = processPredicates(sortPredicate, reverseOrder); - // Add a predicate at the end that evaluates to the element index. This makes the - // sort stable as it works as a tie-breaker when all the input predicates cannot - // distinguish between two elements. - predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1}); + var predicates = processPredicates(sortPredicate); + + var descending = reverseOrder ? -1 : 1; + + // Define the `compare()` function. Use a default comparator if none is specified. + var compare = isFunction(compareFn) ? compareFn : defaultCompare; // The next three lines are a version of a Swartzian Transform idiom from Perl // (sometimes called the Decorate-Sort-Undecorate idiom) @@ -19789,8 +22226,12 @@ function orderByFilter($parse) { return array; function getComparisonObject(value, index) { + // NOTE: We are adding an extra `tieBreaker` value based on the element's index. + // This will be used to keep the sort stable when none of the input predicates can + // distinguish between two elements. return { value: value, + tieBreaker: {value: index, type: 'number', index: index}, predicateValues: predicates.map(function(predicate) { return getPredicateValue(predicate.get(value), index); }) @@ -19798,25 +22239,26 @@ function orderByFilter($parse) { } function doComparison(v1, v2) { - var result = 0; - for (var index=0, length = predicates.length; index < length; ++index) { - result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending; - if (result) break; + for (var i = 0, ii = predicates.length; i < ii; i++) { + var result = compare(v1.predicateValues[i], v2.predicateValues[i]); + if (result) { + return result * predicates[i].descending * descending; + } } - return result; + + return compare(v1.tieBreaker, v2.tieBreaker) * descending; } }; - function processPredicates(sortPredicate, reverseOrder) { - reverseOrder = reverseOrder ? -1 : 1; - return sortPredicate.map(function(predicate) { + function processPredicates(sortPredicates) { + return sortPredicates.map(function(predicate) { var descending = 1, get = identity; if (isFunction(predicate)) { get = predicate; } else if (isString(predicate)) { - if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) { - descending = predicate.charAt(0) == '-' ? -1 : 1; + if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) { + descending = predicate.charAt(0) === '-' ? -1 : 1; predicate = predicate.substring(1); } if (predicate !== '') { @@ -19827,7 +22269,7 @@ function orderByFilter($parse) { } } } - return { get: get, descending: descending * reverseOrder }; + return {get: get, descending: descending}; }); } @@ -19842,9 +22284,9 @@ function orderByFilter($parse) { } } - function objectValue(value, index) { + function objectValue(value) { // If `valueOf` is a valid function use that - if (typeof value.valueOf === 'function') { + if (isFunction(value.valueOf)) { value = value.valueOf(); if (isPrimitive(value)) return value; } @@ -19853,8 +22295,8 @@ function orderByFilter($parse) { value = value.toString(); if (isPrimitive(value)) return value; } - // We have a basic object so we use the position of the object in the collection - return index; + + return value; } function getPredicateValue(value, index) { @@ -19862,23 +22304,39 @@ function orderByFilter($parse) { if (value === null) { type = 'string'; value = 'null'; - } else if (type === 'string') { - value = value.toLowerCase(); } else if (type === 'object') { - value = objectValue(value, index); + value = objectValue(value); } - return { value: value, type: type }; + return {value: value, type: type, index: index}; } - function compare(v1, v2) { + function defaultCompare(v1, v2) { var result = 0; - if (v1.type === v2.type) { - if (v1.value !== v2.value) { - result = v1.value < v2.value ? -1 : 1; + var type1 = v1.type; + var type2 = v2.type; + + if (type1 === type2) { + var value1 = v1.value; + var value2 = v2.value; + + if (type1 === 'string') { + // Compare strings case-insensitively + value1 = value1.toLowerCase(); + value2 = value2.toLowerCase(); + } else if (type1 === 'object') { + // For basic objects, use the position of the object + // in the collection instead of the value + if (isObject(value1)) value1 = v1.index; + if (isObject(value2)) value2 = v2.index; + } + + if (value1 !== value2) { + result = value1 < value2 ? -1 : 1; } } else { - result = v1.type < v2.type ? -1 : 1; + result = type1 < type2 ? -1 : 1; } + return result; } } @@ -19899,12 +22357,10 @@ function ngDirective(directive) { * @restrict E * * @description - * Modifies the default behavior of the html A tag so that the default action is prevented when + * Modifies the default behavior of the html a tag so that the default action is prevented when * the href attribute is empty. * - * This change permits the easy creation of action links with the `ngClick` directive - * without changing the location or causing page reloads, e.g.: - * `<a href="" ng-click="list.addItem()">Add Item</a>` + * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive. */ var htmlAnchorDirective = valueFn({ restrict: 'E', @@ -19958,7 +22414,7 @@ var htmlAnchorDirective = valueFn({ * @example * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes * in links and their different behaviors: - <example> + <example name="ng-href"> <file name="index.html"> <input ng-model="value" /><br /> <a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br /> @@ -20087,27 +22543,15 @@ var htmlAnchorDirective = valueFn({ * * @description * - * This directive sets the `disabled` attribute on the element if the + * This directive sets the `disabled` attribute on the element (typically a form control, + * e.g. `input`, `button`, `select` etc.) if the * {@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> + <example name="ng-disabled"> <file name="index.html"> <label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/> <button ng-model="button" ng-disabled="checked">Button</button> @@ -20139,17 +22583,11 @@ var htmlAnchorDirective = valueFn({ * Note that this directive should not be used together with {@link ngModel `ngModel`}, * as this can lead to unexpected behavior. * - * ### Why do we need `ngChecked`? + * A special directive is necessary because we cannot use interpolation inside the `checked` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. * - * The HTML specification does not require browsers to preserve the values of boolean attributes - * such as checked. (Their presence means true and their absence means false.) - * If we put an Angular interpolation expression into such an attribute then the - * binding information would be lost when the browser removes the attribute. - * The `ngChecked` directive solves this problem for the `checked` attribute. - * This complementary directive is not removed by the browser and so provides - * a permanent reliable place to store the binding information. * @example - <example> + <example name="ng-checked"> <file name="index.html"> <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"> @@ -20176,15 +22614,16 @@ 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. + * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on + * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information. + * + * A special directive is necessary because we cannot use interpolation inside the `readonly` + * attribute. See the {@link guide/interpolation interpolation guide} for more info. + * * @example - <example> + <example name="ng-readonly"> <file name="index.html"> <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" /> @@ -20211,16 +22650,21 @@ 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. + * + * <div class="alert alert-warning"> + * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only + * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you + * should not use `ngSelected` on the options, as `ngModel` will set the select value and + * selected options. + * </div> * * @example - <example> + <example name="ng-selected"> <file name="index.html"> <label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/> <select aria-label="ngSelected demo"> @@ -20249,15 +22693,19 @@ 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. + * + * ## A note about browser compatibility + * + * Edge, Firefox, and Internet Explorer do not support the `details` element, it is + * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead. + * * @example - <example> + <example name="ng-open"> <file name="index.html"> <label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/> <details id="details" ng-open="open"> @@ -20283,7 +22731,7 @@ var ngAttributeAliasDirectives = {}; // boolean attrs are evaluated forEach(BOOLEAN_ATTR, function(propName, attrName) { // binding to multiple is not supported - if (propName == "multiple") return; + if (propName === 'multiple') return; function defaultLinkFn(scope, element, attr) { scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) { @@ -20320,10 +22768,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) { link: function(scope, element, attr) { //special case ngPattern when a literal regular expression value //is used as the expression (this way we don't have to watch anything). - if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") { + if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') { var match = attr.ngPattern.match(REGEX_STRING_REGEXP); if (match) { - attr.$set("ngPattern", new RegExp(match[1], match[2])); + attr.$set('ngPattern', new RegExp(match[1], match[2])); return; } } @@ -20363,10 +22811,11 @@ forEach(['src', 'srcset', 'href'], function(attrName) { attr.$set(name, value); - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist + // Support: IE 9-11 only + // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. + // We use attr[attrName] value since $set can sanitize the url. if (msie && propName) element.prop(propName, attr[name]); }); } @@ -20374,7 +22823,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) { }; }); -/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true +/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS */ var nullFormCtrl = { $addControl: noop, @@ -20385,6 +22834,7 @@ var nullFormCtrl = { $setPristine: noop, $setSubmitted: noop }, +PENDING_CLASS = 'ng-pending', SUBMITTED_CLASS = 'ng-submitted'; function nullFormRenameControl(control, name) { @@ -20435,22 +22885,28 @@ function nullFormRenameControl(control, name) { */ //asks for $scope to fool the BC controller module FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate']; -function FormController(element, attrs, $scope, $animate, $interpolate) { - var form = this, - controls = []; +function FormController($element, $attrs, $scope, $animate, $interpolate) { + this.$$controls = []; // init state - form.$error = {}; - form.$$success = {}; - form.$pending = undefined; - form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope); - form.$dirty = false; - form.$pristine = true; - form.$valid = true; - form.$invalid = false; - form.$submitted = false; - form.$$parentForm = nullFormCtrl; + this.$error = {}; + this.$$success = {}; + this.$pending = undefined; + this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope); + this.$dirty = false; + this.$pristine = true; + this.$valid = true; + this.$invalid = false; + this.$submitted = false; + this.$$parentForm = nullFormCtrl; + + this.$$element = $element; + this.$$animate = $animate; + setupValidity(this); +} + +FormController.prototype = { /** * @ngdoc method * @name form.FormController#$rollbackViewValue @@ -20462,11 +22918,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is typically needed by the reset button of * a form that uses `ng-model-options` to pend updates. */ - form.$rollbackViewValue = function() { - forEach(controls, function(control) { + $rollbackViewValue: function() { + forEach(this.$$controls, function(control) { control.$rollbackViewValue(); }); - }; + }, /** * @ngdoc method @@ -20479,11 +22935,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * event defined in `ng-model-options`. This method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - form.$commitViewValue = function() { - forEach(controls, function(control) { + $commitViewValue: function() { + forEach(this.$$controls, function(control) { control.$commitViewValue(); }); - }; + }, /** * @ngdoc method @@ -20501,34 +22957,34 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * * However, if the method is used programmatically, for example by adding dynamically created controls, * or controls that have been previously removed without destroying their corresponding DOM element, - * it's the developers responsiblity to make sure the current state propagates to the parent form. + * it's the developers responsibility to make sure the current state propagates to the parent form. * * For example, if an input control is added that is already `$dirty` and has `$error` properties, * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form. */ - form.$addControl = function(control) { + $addControl: function(control) { // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored // and not added to the scope. Now we throw an error. assertNotHasOwnProperty(control.$name, 'input'); - controls.push(control); + this.$$controls.push(control); if (control.$name) { - form[control.$name] = control; + this[control.$name] = control; } - control.$$parentForm = form; - }; + control.$$parentForm = this; + }, // Private API: rename a form control - form.$$renameControl = function(control, newName) { + $$renameControl: function(control, newName) { var oldName = control.$name; - if (form[oldName] === control) { - delete form[oldName]; + if (this[oldName] === control) { + delete this[oldName]; } - form[newName] = control; + this[newName] = control; control.$name = newName; - }; + }, /** * @ngdoc method @@ -20546,60 +23002,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * different from case to case. For example, removing the only `$dirty` control from a form may or * may not mean that the form is still `$dirty`. */ - form.$removeControl = function(control) { - if (control.$name && form[control.$name] === control) { - delete form[control.$name]; - } - forEach(form.$pending, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$error, function(value, name) { - form.$setValidity(name, null, control); - }); - forEach(form.$$success, function(value, name) { - form.$setValidity(name, null, control); - }); - - arrayRemove(controls, control); + $removeControl: function(control) { + if (control.$name && this[control.$name] === control) { + delete this[control.$name]; + } + forEach(this.$pending, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$error, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + forEach(this.$$success, function(value, name) { + // eslint-disable-next-line no-invalid-this + this.$setValidity(name, null, control); + }, this); + + arrayRemove(this.$$controls, control); control.$$parentForm = nullFormCtrl; - }; - - - /** - * @ngdoc method - * @name form.FormController#$setValidity - * - * @description - * Sets the validity of a form control. - * - * This method will also propagate to parent forms. - */ - addSetValidityMethod({ - ctrl: this, - $element: element, - set: function(object, property, controller) { - var list = object[property]; - if (!list) { - object[property] = [controller]; - } else { - var index = list.indexOf(controller); - if (index === -1) { - list.push(controller); - } - } - }, - unset: function(object, property, controller) { - var list = object[property]; - if (!list) { - return; - } - arrayRemove(list, controller); - if (list.length === 0) { - delete object[property]; - } - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -20611,13 +23033,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * This method can be called to add the 'ng-dirty' class and set the form to a dirty * state (ng-dirty class). This method will also propagate to parent forms. */ - form.$setDirty = function() { - $animate.removeClass(element, PRISTINE_CLASS); - $animate.addClass(element, DIRTY_CLASS); - form.$dirty = true; - form.$pristine = false; - form.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$dirty = true; + this.$pristine = false; + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -20626,22 +23048,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its pristine state. * - * This method can be called to remove the 'ng-dirty' class and set the form to its pristine - * state (ng-pristine class). This method will also propagate to all the controls contained - * in this form. + * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes + * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted` + * state to false. + * + * This method will also propagate to all the controls contained in this form. * * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after * saving or resetting it. */ - form.$setPristine = function() { - $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); - form.$dirty = false; - form.$pristine = true; - form.$submitted = false; - forEach(controls, function(control) { + $setPristine: function() { + this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS); + this.$dirty = false; + this.$pristine = true; + this.$submitted = false; + forEach(this.$$controls, function(control) { control.$setPristine(); }); - }; + }, /** * @ngdoc method @@ -20656,11 +23080,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * Setting a form controls back to their untouched state is often useful when setting the form * back to its pristine state. */ - form.$setUntouched = function() { - forEach(controls, function(control) { + $setUntouched: function() { + forEach(this.$$controls, function(control) { control.$setUntouched(); }); - }; + }, /** * @ngdoc method @@ -20669,12 +23093,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * @description * Sets the form to its submitted state. */ - form.$setSubmitted = function() { - $animate.addClass(element, SUBMITTED_CLASS); - form.$submitted = true; - form.$$parentForm.$setSubmitted(); - }; -} + $setSubmitted: function() { + this.$$animate.addClass(this.$$element, SUBMITTED_CLASS); + this.$submitted = true; + this.$$parentForm.$setSubmitted(); + } +}; + +/** + * @ngdoc method + * @name form.FormController#$setValidity + * + * @description + * Sets the validity of a form control. + * + * This method will also propagate to parent forms. + */ +addSetValidityMethod({ + clazz: FormController, + set: function(object, property, controller) { + var list = object[property]; + if (!list) { + object[property] = [controller]; + } else { + var index = list.indexOf(controller); + if (index === -1) { + list.push(controller); + } + } + }, + unset: function(object, property, controller) { + var list = object[property]; + if (!list) { + return; + } + arrayRemove(list, controller); + if (list.length === 0) { + delete object[property]; + } + } +}); /** * @ngdoc directive @@ -20711,13 +23169,9 @@ 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. @@ -20788,7 +23242,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) { * </pre> * * @example - <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> + <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample"> <file name="index.html"> <script> angular.module('formExample', []) @@ -20875,13 +23329,13 @@ var formDirectiveFactory = function(isNgForm) { event.preventDefault(); }; - addEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].addEventListener('submit', handleFormSubmission); // unregister the preventDefault listener so that we don't not leak memory but in a // way that will achieve the prevention of the default action. formElement.on('$destroy', function() { $timeout(function() { - removeEventListenerFn(formElement[0], 'submit', handleFormSubmission); + formElement[0].removeEventListener('submit', handleFormSubmission); }, 0, false); }); } @@ -20926,27 +23380,149 @@ var formDirectiveFactory = function(isNgForm) { var formDirective = formDirectiveFactory(); var ngFormDirective = formDirectiveFactory(true); -/* global VALID_CLASS: false, + + +// helper methods +function setupValidity(instance) { + instance.$$classCache = {}; + instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS)); +} +function addSetValidityMethod(context) { + var clazz = context.clazz, + set = context.set, + unset = context.unset; + + clazz.prototype.$setValidity = function(validationErrorKey, state, controller) { + if (isUndefined(state)) { + createAndSet(this, '$pending', validationErrorKey, controller); + } else { + unsetAndCleanup(this, '$pending', validationErrorKey, controller); + } + if (!isBoolean(state)) { + unset(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } else { + if (state) { + unset(this.$error, validationErrorKey, controller); + set(this.$$success, validationErrorKey, controller); + } else { + set(this.$error, validationErrorKey, controller); + unset(this.$$success, validationErrorKey, controller); + } + } + if (this.$pending) { + cachedToggleClass(this, PENDING_CLASS, true); + this.$valid = this.$invalid = undefined; + toggleValidationCss(this, '', null); + } else { + cachedToggleClass(this, PENDING_CLASS, false); + this.$valid = isObjectEmpty(this.$error); + this.$invalid = !this.$valid; + toggleValidationCss(this, '', this.$valid); + } + + // re-read the state as the set/unset methods could have + // combined state in this.$error[validationError] (used for forms), + // where setting/unsetting only increments/decrements the value, + // and does not replace it. + var combinedState; + if (this.$pending && this.$pending[validationErrorKey]) { + combinedState = undefined; + } else if (this.$error[validationErrorKey]) { + combinedState = false; + } else if (this.$$success[validationErrorKey]) { + combinedState = true; + } else { + combinedState = null; + } + + toggleValidationCss(this, validationErrorKey, combinedState); + this.$$parentForm.$setValidity(validationErrorKey, combinedState, this); + }; + + function createAndSet(ctrl, name, value, controller) { + if (!ctrl[name]) { + ctrl[name] = {}; + } + set(ctrl[name], value, controller); + } + + function unsetAndCleanup(ctrl, name, value, controller) { + if (ctrl[name]) { + unset(ctrl[name], value, controller); + } + if (isObjectEmpty(ctrl[name])) { + ctrl[name] = undefined; + } + } + + function cachedToggleClass(ctrl, className, switchValue) { + if (switchValue && !ctrl.$$classCache[className]) { + ctrl.$$animate.addClass(ctrl.$$element, className); + ctrl.$$classCache[className] = true; + } else if (!switchValue && ctrl.$$classCache[className]) { + ctrl.$$animate.removeClass(ctrl.$$element, className); + ctrl.$$classCache[className] = false; + } + } + + function toggleValidationCss(ctrl, validationErrorKey, isValid) { + validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + + cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true); + cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false); + } +} + +function isObjectEmpty(obj) { + if (obj) { + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + return false; + } + } + } + return true; +} + +/* global + VALID_CLASS: false, INVALID_CLASS: false, PRISTINE_CLASS: false, DIRTY_CLASS: false, - UNTOUCHED_CLASS: false, - TOUCHED_CLASS: false, - ngModelMinErr: false, + ngModelMinErr: false */ -// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 -var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/; +// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231 +var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/; // See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987) -var URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/; -var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; -var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/; -var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; -var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; -var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; -var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; +// 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 55555555555555555555555 666 77777777 8888888 999 +var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i; +// eslint-disable-next-line max-len +var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/; +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 = { /** @@ -20971,8 +23547,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21009,11 +23585,11 @@ var inputType = { <span class="error" ng-show="myForm.input.$error.pattern"> Single word only!</span> </div> - <tt>text = {{example.text}}</tt><br/> - <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/> - <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/> - <tt>myForm.$valid = {{myForm.$valid}}</tt><br/> - <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/> + <code>text = {{example.text}}</code><br/> + <code>myForm.input.$valid = {{myForm.input.$valid}}</code><br/> + <code>myForm.input.$error = {{myForm.input.$error}}</code><br/> + <code>myForm.$valid = {{myForm.$valid}}</code><br/> + <code>myForm.$error.required = {{!!myForm.$error.required}}</code><br/> </form> </file> <file name="protractor.js" type="protractor"> @@ -21114,7 +23690,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM-dd"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21217,7 +23792,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21302,7 +23876,7 @@ var inputType = { }]); </script> <form name="myForm" ng-controller="DateController as dateCtrl"> - <label for="exampleInput">Pick a between 8am and 5pm:</label> + <label for="exampleInput">Pick a time between 8am and 5pm:</label> <input type="time" id="exampleInput" name="input" ng-model="example.value" placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required /> <div role="alert"> @@ -21321,7 +23895,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "HH:mm:ss"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21426,7 +23999,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-Www"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21530,7 +24102,6 @@ var inputType = { <file name="protractor.js" type="protractor"> var value = element(by.binding('example.value | date: "yyyy-MM"')); var valid = element(by.binding('myForm.input.$valid')); - var input = element(by.model('example.value')); // currently protractor/webdriver does not support // sending keys to all known HTML5 input controls @@ -21593,7 +24164,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`. + * Can be interpolated. * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. + * Can be interpolated. + * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`, + * but does not trigger HTML5 native validation. Takes an expression. + * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint. + * Can be interpolated. + * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint, + * but does not trigger HTML5 native validation. Takes an expression. * @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 @@ -21606,8 +24187,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21704,8 +24285,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21803,8 +24384,8 @@ var inputType = { * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string * that contains the regular expression body that will be converted to a regular expression * as in the ngPattern directive. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -21925,19 +24506,140 @@ var inputType = { </file> <file name="protractor.js" type="protractor"> it('should change state', function() { + var inputs = element.all(by.model('color.name')); var color = element(by.binding('color.name')); expect(color.getText()).toContain('blue'); - element.all(by.model('color.name')).get(0).click(); - + inputs.get(0).click(); expect(color.getText()).toContain('red'); + + inputs.get(1).click(); + expect(color.getText()).toContain('green'); }); </file> </example> */ 'radio': radioInputType, + /** + * @ngdoc input + * @name input[range] + * + * @description + * Native range input with validation and transformation. + * + * The model for the range input must always be a `Number`. + * + * IE9 and other browsers that do not support the `range` type fall back + * to a text input without any default values for `min`, `max` and `step`. Model binding, + * validation and number parsing are nevertheless supported. + * + * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]` + * in a way that never allows the input to hold an invalid value. That means: + * - any non-numerical value is set to `(max + min) / 2`. + * - any numerical value that is less than the current min val, or greater than the current max val + * is set to the min / max val respectively. + * - additionally, the current `step` is respected, so the nearest value that satisfies a step + * is used. + * + * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range)) + * for more info. + * + * This has the following consequences for Angular: + * + * Since the element value should always reflect the current model value, a range input + * will set the bound ngModel expression to the value that the browser has set for the + * input element. For example, in the following input `<input type="range" ng-model="model.value">`, + * if the application sets `model.value = null`, the browser will set the input to `'50'`. + * Angular will then set the model to `50`, to prevent input and model value being out of sync. + * + * That means the model for range will immediately be set to `50` after `ngModel` has been + * initialized. It also means a range input can never have the required error. + * + * This does not only affect changes to the model value, but also to the values of the `min`, + * `max`, and `step` attributes. When these change in a way that will cause the browser to modify + * the input value, Angular will also update the model value. + * + * Automatic value adjustment also means that a range input element can never have the `required`, + * `min`, or `max` errors. + * + * However, `step` is currently only fully implemented by Firefox. Other browsers have problems + * when the step value changes dynamically - they do not adjust the element value correctly, but + * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step` + * error on the input, and set the model to `undefined`. + * + * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do + * not set the `min` and `max` attributes, which means that the browser won't automatically adjust + * the input value based on their values, and will always assume min = 0, max = 100, and step = 1. + * + * @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 to ensure that the value entered is greater + * than `min`. Can be interpolated. + * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`. + * Can be interpolated. + * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step` + * Can be interpolated. + * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due + * to user interaction with the input element. + * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the + * element. **Note** : `ngChecked` should not be used alongside `ngModel`. + * Checkout {@link ng.directive:ngChecked ngChecked} for usage. + * + * @example + <example name="range-input-directive" module="rangeExample"> + <file name="index.html"> + <script> + angular.module('rangeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 75; + $scope.min = 10; + $scope.max = 90; + }]); + </script> + <form name="myForm" ng-controller="ExampleController"> + + Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}"> + <hr> + Model as number: <input type="number" ng-model="value"><br> + Min: <input type="number" ng-model="min"><br> + Max: <input type="number" ng-model="max"><br> + value = <code>{{value}}</code><br/> + myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> + myForm.range.$error = <code>{{myForm.range.$error}}</code> + </form> + </file> + </example> + + * ## Range Input with ngMin & ngMax attributes + + * @example + <example name="range-input-directive-ng" module="rangeExample"> + <file name="index.html"> + <script> + angular.module('rangeExample', []) + .controller('ExampleController', ['$scope', function($scope) { + $scope.value = 75; + $scope.min = 10; + $scope.max = 90; + }]); + </script> + <form name="myForm" ng-controller="ExampleController"> + Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max"> + <hr> + Model as number: <input type="number" ng-model="value"><br> + Min: <input type="number" ng-model="min"><br> + Max: <input type="number" ng-model="max"><br> + value = <code>{{value}}</code><br/> + myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/> + myForm.range.$error = <code>{{myForm.range.$error}}</code> + </form> + </file> + </example> + + */ + 'range': rangeInputType, /** * @ngdoc input @@ -22017,13 +24719,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) { function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { var type = lowercase(element[0].type); - // In composition mode, users are still inputing intermediate text buffer, + // In composition mode, users are still inputting intermediate text buffer, // hold the listener until composition is done. // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent if (!$sniffer.android) { var composing = false; - element.on('compositionstart', function(data) { + element.on('compositionstart', function() { composing = true; }); @@ -22033,6 +24735,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { }); } + var timeout; + var listener = function(ev) { if (timeout) { $browser.defer.cancel(timeout); @@ -22062,8 +24766,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() { @@ -22075,7 +24777,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) { } }; - element.on('keydown', function(event) { + element.on('keydown', /** @this */ function(event) { var key = event.keyCode; // ignore @@ -22095,6 +24797,26 @@ 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, /** @this */ 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() { // Workaround for Firefox validation #12102. var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue; @@ -22148,7 +24870,7 @@ function createDateParser(regexp, mapping) { // When a date is JSON'ified to wraps itself inside of an extra // set of double quotes. This makes the date parsing code unable // to match the date string and parse it as a date. - if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') { + if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') { iso = iso.substring(1, iso.length - 1); } if (ISO_DATE_REGEXP.test(iso)) { @@ -22190,7 +24912,7 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options && ctrl.$options.timezone; + var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; ctrl.$$parserName = type; @@ -22269,10 +24991,7 @@ function badInputChecker(scope, element, attr, ctrl) { } } -function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { - badInputChecker(scope, element, attr, ctrl); - baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - +function numberFormatterParser(ctrl) { ctrl.$$parserName = 'number'; ctrl.$parsers.push(function(value) { if (ctrl.$isEmpty(value)) return null; @@ -22289,40 +25008,255 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { } return value; }); +} + +function parseNumberAttrVal(val) { + if (isDefined(val) && !isNumber(val)) { + val = parseFloat(val); + } + return !isNumberNaN(val) ? val : undefined; +} + +function isNumberInteger(num) { + // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066 + // (minus the assumption that `num` is a number) + + // eslint-disable-next-line no-bitwise + return (num | 0) === num; +} + +function countDecimals(num) { + var numString = num.toString(); + var decimalSymbolIndex = numString.indexOf('.'); + + if (decimalSymbolIndex === -1) { + if (-1 < num && num < 1) { + // It may be in the exponential notation format (`1e-X`) + var match = /e-(\d+)$/.exec(numString); + + if (match) { + return Number(match[1]); + } + } + + return 0; + } + + return numString.length - decimalSymbolIndex - 1; +} + +function isValidForStep(viewValue, stepBase, step) { + // At this point `stepBase` and `step` are expected to be non-NaN values + // and `viewValue` is expected to be a valid stringified number. + var value = Number(viewValue); + + var isNonIntegerValue = !isNumberInteger(value); + var isNonIntegerStepBase = !isNumberInteger(stepBase); + var isNonIntegerStep = !isNumberInteger(step); + + // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or + // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers. + if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) { + var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0; + var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0; + var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0; + + var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals); + var multiplier = Math.pow(10, decimalCount); + + value = value * multiplier; + stepBase = stepBase * multiplier; + step = step * multiplier; + + if (isNonIntegerValue) value = Math.round(value); + if (isNonIntegerStepBase) stepBase = Math.round(stepBase); + if (isNonIntegerStep) step = Math.round(step); + } + + return (value - stepBase) % step === 0; +} + +function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var minVal; + var maxVal; if (isDefined(attr.min) || attr.ngMin) { - var minVal; ctrl.$validators.min = function(value) { return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal; }; attr.$observe('min', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); - } - minVal = isNumber(val) && !isNaN(val) ? val : undefined; + minVal = parseNumberAttrVal(val); // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } if (isDefined(attr.max) || attr.ngMax) { - var maxVal; ctrl.$validators.max = function(value) { return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal; }; attr.$observe('max', function(val) { - if (isDefined(val) && !isNumber(val)) { - val = parseFloat(val, 10); - } - maxVal = isNumber(val) && !isNaN(val) ? val : undefined; + maxVal = parseNumberAttrVal(val); + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + }); + } + + if (isDefined(attr.step) || attr.ngStep) { + var stepVal; + ctrl.$validators.step = function(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + attr.$observe('step', function(val) { + stepVal = parseNumberAttrVal(val); // TODO(matsko): implement validateLater to reduce number of validations ctrl.$validate(); }); } } +function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) { + badInputChecker(scope, element, attr, ctrl); + numberFormatterParser(ctrl); + baseInputType(scope, element, attr, ctrl, $sniffer, $browser); + + var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range', + minVal = supportsRange ? 0 : undefined, + maxVal = supportsRange ? 100 : undefined, + stepVal = supportsRange ? 1 : undefined, + validity = element[0].validity, + hasMinAttr = isDefined(attr.min), + hasMaxAttr = isDefined(attr.max), + hasStepAttr = isDefined(attr.step); + + var originalRender = ctrl.$render; + + ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ? + //Browsers that implement range will set these values automatically, but reading the adjusted values after + //$render would cause the min / max validators to be applied with the wrong value + function rangeRender() { + originalRender(); + ctrl.$setViewValue(element.val()); + } : + originalRender; + + if (hasMinAttr) { + ctrl.$validators.min = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMinValidator() { return true; } : + // non-support browsers validate the min val + function minValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal; + }; + + setInitialValueAndObserver('min', minChange); + } + + if (hasMaxAttr) { + ctrl.$validators.max = supportsRange ? + // Since all browsers set the input to a valid value, we don't need to check validity + function noopMaxValidator() { return true; } : + // non-support browsers validate the max val + function maxValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal; + }; + + setInitialValueAndObserver('max', maxChange); + } + + if (hasStepAttr) { + ctrl.$validators.step = supportsRange ? + function nativeStepValidator() { + // Currently, only FF implements the spec on step change correctly (i.e. adjusting the + // input element value to a valid value). It's possible that other browsers set the stepMismatch + // validity error instead, so we can at least report an error in that case. + return !validity.stepMismatch; + } : + // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would + function stepValidator(modelValue, viewValue) { + return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) || + isValidForStep(viewValue, minVal || 0, stepVal); + }; + + setInitialValueAndObserver('step', stepChange); + } + + function setInitialValueAndObserver(htmlAttrName, changeFn) { + // interpolated attributes set the attribute value only after a digest, but we need the + // attribute value when the input is first rendered, so that the browser can adjust the + // input value based on the min/max value + element.attr(htmlAttrName, attr[htmlAttrName]); + attr.$observe(htmlAttrName, changeFn); + } + + function minChange(val) { + minVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the minVal is greater than the element value + if (minVal > elVal) { + elVal = minVal; + element.val(elVal); + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function maxChange(val) { + maxVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + if (supportsRange) { + var elVal = element.val(); + // IE11 doesn't set the el val correctly if the maxVal is less than the element value + if (maxVal < elVal) { + element.val(maxVal); + // IE11 and Chrome don't set the value to the minVal when max < min + elVal = maxVal < minVal ? minVal : maxVal; + } + ctrl.$setViewValue(elVal); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } + + function stepChange(val) { + stepVal = parseNumberAttrVal(val); + // ignore changes before model is initialized + if (isNumberNaN(ctrl.$modelValue)) { + return; + } + + // Some browsers don't adjust the input value correctly, but set the stepMismatch error + if (supportsRange && ctrl.$viewValue !== element.val()) { + ctrl.$setViewValue(element.val()); + } else { + // TODO(matsko): implement validateLater to reduce number of validations + ctrl.$validate(); + } + } +} + function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) { // Note: no badInputChecker here by purpose as `url` is only a validation // in browsers, i.e. we can always read out input.value even if it is not valid! @@ -22350,14 +25284,20 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) { } function radioInputType(scope, element, attr, ctrl) { + var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false'; // make the name unique, if not defined if (isUndefined(attr.name)) { element.attr('name', nextUid()); } var listener = function(ev) { + var value; if (element[0].checked) { - ctrl.$setViewValue(attr.value, ev && ev.type); + value = attr.value; + if (doTrim) { + value = trim(value); + } + ctrl.$setViewValue(value, ev && ev.type); } }; @@ -22365,7 +25305,10 @@ function radioInputType(scope, element, attr, ctrl) { ctrl.$render = function() { var value = attr.value; - element[0].checked = (value == ctrl.$viewValue); + if (doTrim) { + value = trim(value); + } + element[0].checked = (value === ctrl.$viewValue); }; attr.$observe('value', ctrl.$render); @@ -22436,8 +25379,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -22448,6 +25391,20 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @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. + * + * @knownIssue + * + * When specifying the `placeholder` attribute of `<textarea>`, Internet Explorer will temporarily + * insert the placeholder value as the textarea's content. If the placeholder value contains + * interpolation (`{{ ... }}`), an error will be logged in the console when Angular tries to update + * the value of the by-then-removed text node. This doesn't affect the functionality of the + * textarea, but can be undesirable. + * + * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of + * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can + * find more details on `ngAttr` in the + * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the + * Developer Guide. */ @@ -22475,8 +25432,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt * @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any * length. - * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match - * a RegExp found by evaluating the Angular expression given in the attribute value. + * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue} + * value does not match a RegExp found by evaluating the Angular expression given in the attribute value. * If the expression evaluates to a RegExp object, then this is used directly. * If the expression evaluates to a string, then it will be converted to a RegExp * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to @@ -22612,21 +25569,19 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; * @name ngValue * * @description - * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`}, - * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to - * the bound value. + * Binds the given expression to the value of the element. * - * `ngValue` is useful when dynamically generating lists of radio buttons using - * {@link ngRepeat `ngRepeat`}, as shown below. + * It is mainly used on {@link input[radio] `input[radio]`} and option elements, + * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its + * {@link select `select`} parent element) is set to the bound value. It is especially useful + * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below. * - * Likewise, `ngValue` can be used to generate `<option>` elements for - * the {@link select `select`} element. In that case however, only strings are supported - * for the `value `attribute, so the resulting `ngModel` will always be a string. - * Support for `select` models with non-string values is available via `ngOptions`. + * It can also be used to achieve one-way binding of a given expression to an input element + * such as an `input[text]` or a `textarea`, when that element does not use ngModel. * * @element input * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute - * of the `input` element + * and `value` property of the element. * * @example <example name="ngValue-directive" module="valueExample"> @@ -22665,18 +25620,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; </example> */ var ngValueDirective = function() { + /** + * inputs use the value attribute as their default value if the value property is not set. + * Once the value property has been set (by adding input), it will not react to changes to + * the value attribute anymore. Setting both attribute and property fixes this behavior, and + * makes it possible to use ngValue as a sort of one-way bind. + */ + function updateElementValue(element, attr, value) { + // Support: IE9 only + // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`). + var propValue = isDefined(value) ? value : (msie === 9) ? '' : null; + element.prop('value', propValue); + attr.$set('value', value); + } + return { restrict: 'A', priority: 100, compile: function(tpl, tplAttr) { if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) { return function ngValueConstantLink(scope, elm, attr) { - attr.$set('value', scope.$eval(attr.ngValue)); + var value = scope.$eval(attr.ngValue); + updateElementValue(elm, attr, value); }; } else { return function ngValueLink(scope, elm, attr) { scope.$watch(attr.ngValue, function valueWatchAction(value) { - attr.$set('value', value); + updateElementValue(elm, attr, value); }); }; } @@ -22710,7 +25680,7 @@ var ngValueDirective = function() { * * @example * Enter a name in the Live Preview text box; the greeting below the text box changes instantly. - <example module="bindExample"> + <example module="bindExample" name="ng-bind"> <file name="index.html"> <script> angular.module('bindExample', []) @@ -22744,7 +25714,7 @@ var ngBindDirective = ['$compile', function($compile) { $compile.$$addBindingInfo(element, attr.ngBind); element = element[0]; scope.$watch(attr.ngBind, function ngBindWatchAction(value) { - element.textContent = isUndefined(value) ? '' : value; + element.textContent = stringify(value); }); }; } @@ -22770,7 +25740,7 @@ var ngBindDirective = ['$compile', function($compile) { * * @example * Try it here: enter text in text box and watch the greeting change. - <example module="bindExample"> + <example module="bindExample" name="ng-bind-template"> <file name="index.html"> <script> angular.module('bindExample', []) @@ -22843,7 +25813,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate * * @example - <example module="bindHtmlExample" deps="angular-sanitize.js"> + <example module="bindHtmlExample" deps="angular-sanitize.js" name="ng-bind-html"> <file name="index.html"> <div ng-controller="ExampleController"> <p ng-bind-html="myHTML"></p> @@ -22872,8 +25842,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, restrict: 'A', compile: function ngBindHtmlCompile(tElement, tAttrs) { var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml); - var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) { - return (value || '').toString(); + var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) { + // Unwrap the value to compare the actual inner safe value, not the wrapper object. + return $sce.valueOf(val); }); $compile.$$addBindingClass(tElement); @@ -22881,9 +25852,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile.$$addBindingInfo(element, attr.ngBindHtml); scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() { - // we re-evaluate the expr because we want a TrustedValueHolderType - // for $sce, not a string - element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || ''); + // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter. + var value = ngBindHtmlGetter(scope); + element.html($sce.getTrustedHtml(value) || ''); }); }; } @@ -22967,50 +25938,79 @@ var ngChangeDirective = valueFn({ } }); +/* exported + ngClassDirective, + ngClassEvenDirective, + ngClassOddDirective +*/ + function classDirective(name, selector) { name = 'ngClass' + name; - return ['$animate', function($animate) { + var indexWatchExpression; + + return ['$parse', function($parse) { return { restrict: 'AC', link: function(scope, element, attr) { - var oldVal; + var expression = attr[name].trim(); + var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':'); - scope.$watch(attr[name], ngClassWatchAction, true); + var watchInterceptor = isOneTime ? toFlatValue : toClassString; + var watchExpression = $parse(expression, watchInterceptor); + var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction; - attr.$observe('class', function(value) { - ngClassWatchAction(scope.$eval(attr[name])); - }); + var classCounts = element.data('$classCounts'); + var oldModulo = true; + var oldClassString; + if (!classCounts) { + // Use createMap() to prevent class assumptions involving property + // names in Object.prototype + classCounts = createMap(); + element.data('$classCounts', classCounts); + } if (name !== 'ngClass') { - scope.$watch('$index', function($index, old$index) { - // jshint bitwise: false - var mod = $index & 1; - if (mod !== (old$index & 1)) { - var classes = arrayClasses(scope.$eval(attr[name])); - mod === selector ? - addClasses(classes) : - removeClasses(classes); - } - }); + if (!indexWatchExpression) { + indexWatchExpression = $parse('$index', function moduloTwo($index) { + // eslint-disable-next-line no-bitwise + return $index & 1; + }); + } + + scope.$watch(indexWatchExpression, ngClassIndexWatchAction); } - function addClasses(classes) { - var newClasses = digestClassCounts(classes, 1); - attr.$addClass(newClasses); + scope.$watch(watchExpression, watchAction, isOneTime); + + function addClasses(classString) { + classString = digestClassCounts(split(classString), 1); + attr.$addClass(classString); } - function removeClasses(classes) { - var newClasses = digestClassCounts(classes, -1); - attr.$removeClass(newClasses); + function removeClasses(classString) { + classString = digestClassCounts(split(classString), -1); + attr.$removeClass(classString); } - function digestClassCounts(classes, count) { - // Use createMap() to prevent class assumptions involving property - // names in Object.prototype - var classCounts = element.data('$classCounts') || createMap(); + function updateClasses(oldClassString, newClassString) { + var oldClassArray = split(oldClassString); + var newClassArray = split(newClassString); + + var toRemoveArray = arrayDifference(oldClassArray, newClassArray); + var toAddArray = arrayDifference(newClassArray, oldClassArray); + + var toRemoveString = digestClassCounts(toRemoveArray, -1); + var toAddString = digestClassCounts(toAddArray, 1); + + attr.$addClass(toAddString); + attr.$removeClass(toRemoveString); + } + + function digestClassCounts(classArray, count) { var classesToUpdate = []; - forEach(classes, function(className) { + + forEach(classArray, function(className) { if (count > 0 || classCounts[className]) { classCounts[className] = (classCounts[className] || 0) + count; if (classCounts[className] === +(count > 0)) { @@ -23018,72 +26018,106 @@ function classDirective(name, selector) { } } }); - element.data('$classCounts', classCounts); + return classesToUpdate.join(' '); } - function updateClasses(oldClasses, newClasses) { - var toAdd = arrayDifference(newClasses, oldClasses); - var toRemove = arrayDifference(oldClasses, newClasses); - toAdd = digestClassCounts(toAdd, 1); - toRemove = digestClassCounts(toRemove, -1); - if (toAdd && toAdd.length) { - $animate.addClass(element, toAdd); + function ngClassIndexWatchAction(newModulo) { + // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it + // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the + // `ngClass[OneTime]WatchAction()` will update the classes. + if (newModulo === selector) { + addClasses(oldClassString); + } else { + removeClasses(oldClassString); } - if (toRemove && toRemove.length) { - $animate.removeClass(element, toRemove); + + oldModulo = newModulo; + } + + function ngClassOneTimeWatchAction(newClassValue) { + var newClassString = toClassString(newClassValue); + + if (newClassString !== oldClassString) { + ngClassWatchAction(newClassString); } } - function ngClassWatchAction(newVal) { - if (selector === true || scope.$index % 2 === selector) { - var newClasses = arrayClasses(newVal || []); - if (!oldVal) { - addClasses(newClasses); - } else if (!equals(newVal,oldVal)) { - var oldClasses = arrayClasses(oldVal); - updateClasses(oldClasses, newClasses); - } + function ngClassWatchAction(newClassString) { + if (oldModulo === selector) { + updateClasses(oldClassString, newClassString); } - oldVal = shallowCopy(newVal); + + oldClassString = newClassString; } } }; + }]; - function arrayDifference(tokens1, tokens2) { - var values = []; + // Helpers + function arrayDifference(tokens1, tokens2) { + if (!tokens1 || !tokens1.length) return []; + if (!tokens2 || !tokens2.length) return tokens1; - outer: - for (var i = 0; i < tokens1.length; i++) { - var token = tokens1[i]; - for (var j = 0; j < tokens2.length; j++) { - if (token == tokens2[j]) continue outer; - } - values.push(token); + var values = []; + + outer: + for (var i = 0; i < tokens1.length; i++) { + var token = tokens1[i]; + for (var j = 0; j < tokens2.length; j++) { + if (token === tokens2[j]) continue outer; } - return values; + values.push(token); } - function arrayClasses(classVal) { - var classes = []; - if (isArray(classVal)) { - forEach(classVal, function(v) { - classes = classes.concat(arrayClasses(v)); - }); - return classes; - } else if (isString(classVal)) { - return classVal.split(' '); - } else if (isObject(classVal)) { - forEach(classVal, function(v, k) { - if (v) { - classes = classes.concat(k.split(' ')); - } - }); - return classes; + return values; + } + + function split(classString) { + return classString && classString.split(' '); + } + + function toClassString(classValue) { + var classString = classValue; + + if (isArray(classValue)) { + classString = classValue.map(toClassString).join(' '); + } else if (isObject(classValue)) { + classString = Object.keys(classValue). + filter(function(key) { return classValue[key]; }). + join(' '); + } + + return classString; + } + + function toFlatValue(classValue) { + var flatValue = classValue; + + if (isArray(classValue)) { + flatValue = classValue.map(toFlatValue); + } else if (isObject(classValue)) { + var hasUndefined = false; + + flatValue = Object.keys(classValue).filter(function(key) { + var value = classValue[key]; + + if (!hasUndefined && isUndefined(value)) { + hasUndefined = true; + } + + return value; + }); + + if (hasUndefined) { + // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering + // the watcher, by including at least one `undefined` value. + flatValue.push(undefined); } - return classVal; } - }]; + + return flatValue; + } } /** @@ -23114,10 +26148,16 @@ function classDirective(name, selector) { * When the expression changes, the previously added classes are removed and only then are the * new classes added. * - * @animations - * **add** - happens just before the class is applied to the elements + * @knownIssue + * You should not use {@link guide/interpolation interpolation} in the value of the `class` + * attribute, when using the `ngClass` directive on the same element. + * See {@link guide/interpolation#known-issues here} for more info. * - * **remove** - happens just before the class is removed from the element + * @animations + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element | + * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element | * * @element ANY * @param {expression} ngClass {@link guide/expression Expression} to eval. The result @@ -23127,7 +26167,7 @@ function classDirective(name, selector) { * element. * * @example Example that demonstrates basic bindings via ngClass directive. - <example> + <example name="ng-class"> <file name="index.html"> <p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p> <label> @@ -23220,7 +26260,7 @@ function classDirective(name, selector) { The example below demonstrates how to perform animations using ngClass. - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-class"> <file name="index.html"> <input id="setbtn" type="button" value="set" ng-click="myVar='my-class'"> <input id="clearbtn" type="button" value="clear" ng-click="myVar=''"> @@ -23283,7 +26323,7 @@ var ngClassDirective = classDirective('', true); * of the evaluation can be a string representing space delimited class names or an array. * * @example - <example> + <example name="ng-class-odd"> <file name="index.html"> <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> <li ng-repeat="name in names"> @@ -23331,7 +26371,7 @@ var ngClassOddDirective = classDirective('Odd', 0); * result of the evaluation can be a string representing space delimited class names or an array. * * @example - <example> + <example name="ng-class-even"> <file name="index.html"> <ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']"> <li ng-repeat="name in names"> @@ -23397,7 +26437,7 @@ var ngClassEvenDirective = classDirective('Even', 1); * @element ANY * * @example - <example> + <example name="ng-cloak"> <file name="index.html"> <div id="template1" ng-cloak>{{ 'hello' }}</div> <div id="template2" class="ng-cloak">{{ 'world' }}</div> @@ -23453,7 +26493,7 @@ var ngCloakDirective = ngDirective({ * * If the current `$controllerProvider` is configured to use globals (via * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may - * also be the name of a globally accessible constructor function (not recommended). + * also be the name of a globally accessible constructor function (deprecated, not recommended). * * @example * Here is a simple form for editing user contact information. Adding, removing, clearing, and @@ -23506,10 +26546,11 @@ var ngCloakDirective = ngDirective({ * .controller('SettingsController1', SettingsController1); * * function SettingsController1() { - * this.name = "John Smith"; + * this.name = 'John Smith'; * this.contacts = [ * {type: 'phone', value: '408 555 1212'}, - * {type: 'email', value: 'john.smith@example.org'} ]; + * {type: 'email', value: 'john.smith@example.org'} + * ]; * } * * SettingsController1.prototype.greet = function() { @@ -23589,10 +26630,11 @@ var ngCloakDirective = ngDirective({ * .controller('SettingsController2', ['$scope', SettingsController2]); * * function SettingsController2($scope) { - * $scope.name = "John Smith"; + * $scope.name = 'John Smith'; * $scope.contacts = [ * {type:'phone', value:'408 555 1212'}, - * {type:'email', value:'john.smith@example.org'} ]; + * {type:'email', value:'john.smith@example.org'} + * ]; * * $scope.greet = function() { * alert($scope.name); @@ -23659,31 +26701,38 @@ var ngControllerDirective = [function() { * @ngdoc directive * @name ngCsp * - * @element html + * @restrict A + * @element ANY * @description * - * Angular has some features that can break certain + * Angular has some features that can conflict with certain restrictions that are applied when using * [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. + * If you intend to implement CSP with 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. * * - * The following rules affect Angular: + * The following default rules in CSP affect Angular: * - * * `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. + * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute + * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to + * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be + * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would + * weaken the protections offered by CSP.) * - * * `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. + * * The use of inline resources, such as inline `<script>` and `<style>` elements, are forbidden. + * This prevents apps from injecting custom styles directly 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. (This CSP rule can be disabled with the CSP keyword `unsafe-inline`, but + * it is generally not recommended as it would weaken the protections offered by CSP.) * - * 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: + * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking dynamic code + * creation from strings (e.g., `unsafe-eval` not specified in CSP header) 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 @@ -23702,24 +26751,24 @@ var ngControllerDirective = [function() { * * * no-inline-style: this stops Angular from injecting CSS styles into the DOM * - * * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings + * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings * * You can use these values in the following combinations: * * * * 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 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. + * 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-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">` + * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">` * * * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject * styles nor use eval, which is the same as an empty: ng-csp. @@ -23735,8 +26784,7 @@ var ngControllerDirective = [function() { </html> ``` * @example - // Note: the suffix `.csp` in the example name triggers - // csp mode in our http server! + <!-- Note: the `.csp` suffix in the example name triggers CSP mode in our http server! --> <example name="example.csp" module="cspExample" ng-csp="true"> <file name="index.html"> <div ng-controller="MainController as ctrl"> @@ -23757,15 +26805,14 @@ var ngControllerDirective = [function() { </file> <file name="script.js"> angular.module('cspExample', []) - .controller('MainController', function() { + .controller('MainController', function MainController() { this.counter = 0; this.inc = function() { this.counter++; }; this.evil = function() { - // jshint evil:true try { - eval('1+2'); + eval('1+2'); // eslint-disable-line no-eval } catch (e) { this.evilError = e.message; } @@ -23817,7 +26864,7 @@ var ngControllerDirective = [function() { beforeEach(function() { util = require('util'); - webdriver = require('protractor/node_modules/selenium-webdriver'); + webdriver = require('selenium-webdriver'); }); // For now, we only test on Chrome, @@ -23854,9 +26901,9 @@ var ngControllerDirective = [function() { </example> */ -// 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() fn that looks for the `ng-csp` attribute anywhere in the current doc +// `ngCsp` is not implemented as a proper directive any more, because we need it be processed while +// we bootstrap the app (before `$parse` is instantiated). For this reason, we just have the `csp()` +// fn that looks for the `ng-csp` attribute anywhere in the current doc. /** * @ngdoc directive @@ -23872,7 +26919,7 @@ var ngControllerDirective = [function() { * click. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-click"> <file name="index.html"> <button ng-click="count = count + 1" ng-init="count=0"> Increment @@ -23911,15 +26958,15 @@ forEach( return { restrict: 'A', compile: function($element, attr) { - // We expose the powerful $event object on the scope that provides access to the Window, - // etc. that isn't protected by the fast paths in $parse. We explicitly request better - // checks at the cost of speed since event handler expressions are not executed as - // frequently as regular change detection. - var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); + // NOTE: + // We expose the powerful `$event` object on the scope that provides access to the Window, + // etc. This is OK, because expressions are not sandboxed any more (and the expression + // sandbox was never meant to be a security feature anyway). + var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { element.on(eventName, function(event) { var callback = function() { - fn(scope, {$event:event}); + fn(scope, {$event: event}); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); @@ -23947,7 +26994,7 @@ forEach( * a dblclick. (The Event object is available as `$event`) * * @example - <example> + <example name="ng-dblclick"> <file name="index.html"> <button ng-dblclick="count = count + 1" ng-init="count=0"> Increment (on double click) @@ -23971,7 +27018,7 @@ forEach( * mousedown. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mousedown"> <file name="index.html"> <button ng-mousedown="count = count + 1" ng-init="count=0"> Increment (on mouse down) @@ -23995,7 +27042,7 @@ forEach( * mouseup. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseup"> <file name="index.html"> <button ng-mouseup="count = count + 1" ng-init="count=0"> Increment (on mouse up) @@ -24018,7 +27065,7 @@ forEach( * mouseover. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseover"> <file name="index.html"> <button ng-mouseover="count = count + 1" ng-init="count=0"> Increment (when mouse is over) @@ -24042,7 +27089,7 @@ forEach( * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseenter"> <file name="index.html"> <button ng-mouseenter="count = count + 1" ng-init="count=0"> Increment (when mouse enters) @@ -24066,7 +27113,7 @@ forEach( * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mouseleave"> <file name="index.html"> <button ng-mouseleave="count = count + 1" ng-init="count=0"> Increment (when mouse leaves) @@ -24090,7 +27137,7 @@ forEach( * mousemove. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-mousemove"> <file name="index.html"> <button ng-mousemove="count = count + 1" ng-init="count=0"> Increment (when mouse moves) @@ -24114,7 +27161,7 @@ forEach( * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keydown"> <file name="index.html"> <input ng-keydown="count = count + 1" ng-init="count=0"> key down count: {{count}} @@ -24136,7 +27183,7 @@ forEach( * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keyup"> <file name="index.html"> <p>Typing in the input box below updates the key count</p> <input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}} @@ -24163,7 +27210,7 @@ forEach( * and can be interrogated for keyCode, altKey, etc.) * * @example - <example> + <example name="ng-keypress"> <file name="index.html"> <input ng-keypress="count = count + 1" ng-init="count=0"> key press count: {{count}} @@ -24196,7 +27243,7 @@ forEach( * ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example module="submitExample"> + <example module="submitExample" name="ng-submit"> <file name="index.html"> <script> angular.module('submitExample', []) @@ -24292,7 +27339,7 @@ forEach( * copy. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-copy"> <file name="index.html"> <input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value"> copied: {{copied}} @@ -24313,7 +27360,7 @@ forEach( * cut. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-cut"> <file name="index.html"> <input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value"> cut: {{cut}} @@ -24334,7 +27381,7 @@ forEach( * paste. ({@link guide/expression#-event- Event object is available as `$event`}) * * @example - <example> + <example name="ng-paste"> <file name="index.html"> <input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'> pasted: {{paste}} @@ -24376,8 +27423,10 @@ forEach( * and `leave` effects. * * @animations - * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container - * leave - happens just before the `ngIf` contents are removed from the DOM + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container | + * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM | * * @element ANY * @scope @@ -24387,7 +27436,7 @@ forEach( * element is added to the DOM tree. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-if"> <file name="index.html"> <label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/> Show when checked: @@ -24418,7 +27467,7 @@ forEach( </file> </example> */ -var ngIfDirective = ['$animate', function($animate) { +var ngIfDirective = ['$animate', '$compile', function($animate, $compile) { return { multiElement: true, transclude: 'element', @@ -24434,7 +27483,7 @@ var ngIfDirective = ['$animate', function($animate) { if (!childScope) { $transclude(function(clone, newScope) { childScope = newScope; - clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' '); + clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf); // Note: We only need the first/last node of the cloned nodes. // However, we need to keep the reference to the jqlite wrapper as it might be changed later // by a directive with templateUrl when its template arrives. @@ -24455,8 +27504,8 @@ var ngIfDirective = ['$animate', function($animate) { } if (block) { previousElements = getBlockNodes(block.clone); - $animate.leave(previousElements).then(function() { - previousElements = null; + $animate.leave(previousElements).done(function(response) { + if (response !== false) previousElements = null; }); block = null; } @@ -24489,8 +27538,10 @@ var ngIfDirective = ['$animate', function($animate) { * access on some browsers. * * @animations - * enter - animation is used to bring new content into the browser. - * leave - animation is used to animate existing content away. + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when the expression changes, on the new include | + * | {@link ng.$animate#leave leave} | when the expression changes, on the old include | * * The enter and leave animation occur concurrently. * @@ -24515,7 +27566,7 @@ var ngIfDirective = ['$animate', function($animate) { * - Otherwise enable scrolling only if the expression evaluates to truthy value. * * @example - <example module="includeExample" deps="angular-animate.js" animations="true"> + <example module="includeExample" deps="angular-animate.js" animations="true" name="ng-include"> <file name="index.html"> <div ng-controller="ExampleController"> <select ng-model="template" ng-options="t.name for t in templates"> @@ -24532,8 +27583,8 @@ var ngIfDirective = ['$animate', function($animate) { angular.module('includeExample', ['ngAnimate']) .controller('ExampleController', ['$scope', function($scope) { $scope.templates = - [ { name: 'template1.html', url: 'template1.html'}, - { name: 'template2.html', url: 'template2.html'} ]; + [{ name: 'template1.html', url: 'template1.html'}, + { name: 'template2.html', url: 'template2.html'}]; $scope.template = $scope.templates[0]; }]); </file> @@ -24591,7 +27642,7 @@ var ngIfDirective = ['$animate', function($animate) { }); it('should load template2.html', function() { - if (browser.params.browser == 'firefox') { + if (browser.params.browser === 'firefox') { // Firefox can't handle using selects // See https://github.com/angular/protractor/issues/480 return; @@ -24602,7 +27653,7 @@ var ngIfDirective = ['$animate', function($animate) { }); it('should change to blank', function() { - if (browser.params.browser == 'firefox') { + if (browser.params.browser === 'firefox') { // Firefox can't handle using selects return; } @@ -24678,8 +27729,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', currentScope = null; } if (currentElement) { - $animate.leave(currentElement).then(function() { - previousElement = null; + $animate.leave(currentElement).done(function(response) { + if (response !== false) previousElement = null; }); previousElement = currentElement; currentElement = null; @@ -24687,9 +27738,10 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', }; scope.$watch(srcExp, function ngIncludeWatchAction(src) { - var afterAnimation = function() { - if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); + var afterAnimation = function(response) { + if (response !== false && isDefined(autoScrollExp) && + (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); } }; var thisChangeId = ++changeCounter; @@ -24698,6 +27750,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; @@ -24710,7 +27764,7 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', // directives to non existing elements. var clone = $transclude(newScope, function(clone) { cleanupLastIncludeContent(); - $animate.enter(clone, null, $element).then(afterAnimation); + $animate.enter(clone, null, $element).done(afterAnimation); }); currentScope = newScope; @@ -24719,6 +27773,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); @@ -24747,12 +27803,12 @@ var ngIncludeFillContentDirective = ['$compile', priority: -400, require: 'ngInclude', link: function(scope, $element, $attr, ctrl) { - if (/SVG/.test($element[0].toString())) { + if (toString.call($element[0]).match(/SVG/)) { // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not // support innerHTML, so detect this here and try to generate the contents // specially. $element.empty(); - $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope, + $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope, function namespaceAdaptedClone(clone) { $element.append(clone); }, {futureParentElement: $element}); @@ -24796,7 +27852,7 @@ var ngIncludeFillContentDirective = ['$compile', * @param {expression} ngInit {@link guide/expression Expression} to eval. * * @example - <example module="initExample"> + <example module="initExample" name="ng-init"> <file name="index.html"> <script> angular.module('initExample', []) @@ -24924,9 +27980,7 @@ var ngListDirective = function() { priority: 100, require: 'ngModel', link: function(scope, element, attr, ctrl) { - // We want to control whitespace trimming so we use this convoluted approach - // to access the ngList attribute, which doesn't pre-trim the attribute - var ngList = element.attr(attr.$attr.ngList) || ', '; + var ngList = attr.ngList || ', '; var trimValues = attr.ngTrim !== 'false'; var separator = trimValues ? trim(ngList) : ngList; @@ -24968,15 +28022,19 @@ var ngListDirective = function() { DIRTY_CLASS: true, UNTOUCHED_CLASS: true, TOUCHED_CLASS: true, + PENDING_CLASS: true, + addSetValidityMethod: true, + setupValidity: true, + defaultModelOptions: false */ + var VALID_CLASS = 'ng-valid', INVALID_CLASS = 'ng-invalid', PRISTINE_CLASS = 'ng-pristine', DIRTY_CLASS = 'ng-dirty', UNTOUCHED_CLASS = 'ng-untouched', TOUCHED_CLASS = 'ng-touched', - PENDING_CLASS = 'ng-pending', EMPTY_CLASS = 'ng-empty', NOT_EMPTY_CLASS = 'ng-not-empty'; @@ -24989,32 +28047,57 @@ var ngModelMinErr = minErr('ngModel'); * @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 - its return value through to the next. The last return value is forwarded to the - {@link ngModel.NgModelController#$validators `$validators`} collection. + * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue + `$viewValue`} from the DOM, usually via user input. + See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation. + Note that the `$parsers` are not called when the bound ngModel expression changes programmatically. -Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue -`$viewValue`}. + The functions are called in array order, each passing + its return value through to the next. The last return value is forwarded to the + {@link ngModel.NgModelController#$validators `$validators`} collection. -Returning `undefined` from a parser means a parse error occurred. In that case, -no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` -will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} -is set to `true`. The parse error is stored in `ngModel.$error.parse`. + Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue + `$viewValue`}. + + Returning `undefined` from a parser means a parse error occurred. In that case, + no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel` + will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`} + is set to `true`. The parse error is stored in `ngModel.$error.parse`. + + This simple example shows a parser that would convert text input value to lowercase: + * ```js + * function parse(value) { + * if (value) { + * return value.toLowerCase(); + * } + * } + * ngModelController.$parsers.push(parse); + * ``` * * @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever - the model value changes. The functions are called in reverse array order, each passing the value through to the - next. The last return value is used as the actual DOM value. - Used to format / convert values for display in the control. + the bound ngModel expression changes programmatically. The `$formatters` are not called when the + value of the control is changed by user interaction. + + Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue + `$modelValue`} for display in the control. + + The functions are called in reverse array order, each passing the value through to the + next. The last return value is used as the actual DOM value. + + This simple example shows a formatter that would convert the model value to uppercase: + * ```js - * function formatter(value) { + * function format(value) { * if (value) { * return value.toUpperCase(); * } * } - * ngModel.$formatters.push(formatter); + * ngModel.$formatters.push(format); * ``` * * @property {Object.<string, function>} $validators A collection of validators that are applied @@ -25140,7 +28223,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. var html = element.html(); // When we clear the content editable the browser leaves a <br> behind // If strip-br attribute is provided then we strip this out - if ( attrs.stripBr && html == '<br>' ) { + if (attrs.stripBr && html === '<br>') { html = ''; } ngModel.$setViewValue(html); @@ -25162,7 +28245,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. </file> <file name="protractor.js" type="protractor"> it('should data-bind and become invalid', function() { - if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') { + if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') { // SafariDriver can't handle contenteditable // and Firefox driver can't clear contenteditables very well return; @@ -25182,8 +28265,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`. * * */ -var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate', - function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) { +NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate']; +function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) { this.$viewValue = Number.NaN; this.$modelValue = Number.NaN; this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity. @@ -25203,40 +28286,56 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ this.$pending = undefined; // keep pending keys here this.$name = $interpolate($attr.name || '', false)($scope); this.$$parentForm = nullFormCtrl; + this.$options = defaultModelOptions; + + this.$$parsedNgModel = $parse($attr.ngModel); + this.$$parsedNgModelAssign = this.$$parsedNgModel.assign; + this.$$ngModelGet = this.$$parsedNgModel; + this.$$ngModelSet = this.$$parsedNgModelAssign; + this.$$pendingDebounce = null; + this.$$parserValid = undefined; + + this.$$currentValidationRunId = 0; + + this.$$scope = $scope; + this.$$attr = $attr; + this.$$element = $element; + this.$$animate = $animate; + this.$$timeout = $timeout; + this.$$parse = $parse; + this.$$q = $q; + this.$$exceptionHandler = $exceptionHandler; + + setupValidity(this); + setupModelWatcher(this); +} - var parsedNgModel = $parse($attr.ngModel), - parsedNgModelAssign = parsedNgModel.assign, - ngModelGet = parsedNgModel, - ngModelSet = parsedNgModelAssign, - pendingDebounce = null, - parserValid, - ctrl = this; - - this.$$setOptions = function(options) { - ctrl.$options = options; - if (options && options.getterSetter) { - var invokeModelGetter = $parse($attr.ngModel + '()'), - invokeModelSetter = $parse($attr.ngModel + '($$$p)'); - - ngModelGet = function($scope) { - var modelValue = parsedNgModel($scope); +NgModelController.prototype = { + $$initGetterSetters: function() { + if (this.$options.getOption('getterSetter')) { + var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'), + invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)'); + + this.$$ngModelGet = function($scope) { + var modelValue = this.$$parsedNgModel($scope); if (isFunction(modelValue)) { modelValue = invokeModelGetter($scope); } return modelValue; }; - ngModelSet = function($scope, newValue) { - if (isFunction(parsedNgModel($scope))) { - invokeModelSetter($scope, {$$$p: ctrl.$modelValue}); + this.$$ngModelSet = function($scope, newValue) { + if (isFunction(this.$$parsedNgModel($scope))) { + invokeModelSetter($scope, {$$$p: newValue}); } else { - parsedNgModelAssign($scope, ctrl.$modelValue); + this.$$parsedNgModelAssign($scope, newValue); } }; - } else if (!parsedNgModel.assign) { - throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}", - $attr.ngModel, startingTag($element)); + } else if (!this.$$parsedNgModel.assign) { + throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}', + this.$$attr.ngModel, startingTag(this.$$element)); } - }; + }, + /** * @ngdoc method @@ -25254,11 +28353,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * the `$viewValue` are different from last time. * * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of - * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue` + * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue` * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be * invoked if you only change a property on the objects. */ - this.$render = noop; + $render: noop, /** * @ngdoc method @@ -25278,56 +28377,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value The value of the input to check for emptiness. * @returns {boolean} True if `value` is "empty". */ - this.$isEmpty = function(value) { + $isEmpty: function(value) { + // eslint-disable-next-line no-self-compare return isUndefined(value) || value === '' || value === null || value !== value; - }; + }, - this.$$updateEmptyClasses = function(value) { - if (ctrl.$isEmpty(value)) { - $animate.removeClass($element, NOT_EMPTY_CLASS); - $animate.addClass($element, EMPTY_CLASS); + $$updateEmptyClasses: function(value) { + if (this.$isEmpty(value)) { + this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS); + this.$$animate.addClass(this.$$element, EMPTY_CLASS); } else { - $animate.removeClass($element, EMPTY_CLASS); - $animate.addClass($element, NOT_EMPTY_CLASS); + this.$$animate.removeClass(this.$$element, EMPTY_CLASS); + this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS); } - }; - - - var currentValidationRunId = 0; - - /** - * @ngdoc method - * @name ngModel.NgModelController#$setValidity - * - * @description - * Change the validity state, and notify the form. - * - * This method can be called within $parsers/$formatters or a custom validation implementation. - * However, in most cases it should be sufficient to use the `ngModel.$validators` and - * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. - * - * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned - * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` - * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. - * The `validationErrorKey` should be in camelCase and will get converted into dash-case - * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` - * class and can be bound to as `{{someForm.someControl.$error.myError}}` . - * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), - * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. - * Skipped is used by Angular when validators do not run because of parse errors and - * when `$asyncValidators` do not run because any of the `$validators` failed. - */ - addSetValidityMethod({ - ctrl: this, - $element: $element, - set: function(object, property) { - object[property] = true; - }, - unset: function(object, property) { - delete object[property]; - }, - $animate: $animate - }); + }, /** * @ngdoc method @@ -25340,12 +28403,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-pristine` class). A model is considered to be pristine when the control * has not been changed from when first compiled. */ - this.$setPristine = function() { - ctrl.$dirty = false; - ctrl.$pristine = true; - $animate.removeClass($element, DIRTY_CLASS); - $animate.addClass($element, PRISTINE_CLASS); - }; + $setPristine: function() { + this.$dirty = false; + this.$pristine = true; + this.$$animate.removeClass(this.$$element, DIRTY_CLASS); + this.$$animate.addClass(this.$$element, PRISTINE_CLASS); + }, /** * @ngdoc method @@ -25358,13 +28421,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed * from when first compiled. */ - this.$setDirty = function() { - ctrl.$dirty = true; - ctrl.$pristine = false; - $animate.removeClass($element, PRISTINE_CLASS); - $animate.addClass($element, DIRTY_CLASS); - ctrl.$$parentForm.$setDirty(); - }; + $setDirty: function() { + this.$dirty = true; + this.$pristine = false; + this.$$animate.removeClass(this.$$element, PRISTINE_CLASS); + this.$$animate.addClass(this.$$element, DIRTY_CLASS); + this.$$parentForm.$setDirty(); + }, /** * @ngdoc method @@ -25378,11 +28441,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * by default, however this function can be used to restore that state if the model has * already been touched by the user. */ - this.$setUntouched = function() { - ctrl.$touched = false; - ctrl.$untouched = true; - $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS); - }; + $setUntouched: function() { + this.$touched = false; + this.$untouched = true; + this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS); + }, /** * @ngdoc method @@ -25395,11 +28458,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * touched state (`ng-touched` class). A model is considered to be touched when the user has * first focused the control element and then shifted focus away from the control (blur event). */ - this.$setTouched = function() { - ctrl.$touched = true; - ctrl.$untouched = false; - $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS); - }; + $setTouched: function() { + this.$touched = true; + this.$untouched = false; + this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS); + }, /** * @ngdoc method @@ -25407,14 +28470,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * @description * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`, - * which may be caused by a pending debounced event or because the input is waiting for a some + * which may be caused by a pending debounced event or because the input is waiting for 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`, there can be 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. * @@ -25427,46 +28493,70 @@ 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) { - * if (e.keyCode == 27) { - * $scope.myValue = ''; + * $scope.model = {value1: '', value2: ''}; + * + * $scope.setEmpty = function(e, value, rollback) { + * if (e.keyCode === 27) { + * e.preventDefault(); + * if (rollback) { + * $scope.myForm[value].$rollbackViewValue(); + * } + * $scope.model[value] = ''; * } * }; * }]); * </file> * <file name="index.html"> * <div ng-controller="CancelUpdateController"> - * <p>Try typing something in each input. See that the model only updates when you - * blur off the input. - * </p> - * <p>Now see what happens if you start typing then press the Escape key</p> + * <p>Both of these inputs are only updated if they are blurred. Hitting escape should + * empty them. Follow these steps and observe the difference:</p> + * <ol> + * <li>Type something in the input. You will see that the model is not yet updated</li> + * <li>Press the Escape key. + * <ol> + * <li> In the first example, nothing happens, because the model is already '', and no + * update is detected. If you blur the input, the model will be set to the current view. + * </li> + * <li> In the second example, the pending update is cancelled, and the input is set back + * to the last committed view value (''). Blurring the input does nothing. + * </li> + * </ol> + * </li> + * </ol> * * <form name="myForm" ng-model-options="{ updateOn: 'blur' }"> - * <p id="inputDescription1">With $rollbackViewValue()</p> - * <input name="myInput1" aria-describedby="inputDescription1" ng-model="myValue" - * ng-keydown="resetWithCancel($event)"><br/> - * myValue: "{{ myValue }}" - * - * <p id="inputDescription2">Without $rollbackViewValue()</p> - * <input name="myInput2" aria-describedby="inputDescription2" ng-model="myValue" - * ng-keydown="resetWithoutCancel($event)"><br/> - * myValue: "{{ myValue }}" + * <div> + * <p id="inputDescription1">Without $rollbackViewValue():</p> + * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1" + * ng-keydown="setEmpty($event, 'value1')"> + * value1: "{{ model.value1 }}" + * </div> + * + * <div> + * <p id="inputDescription2">With $rollbackViewValue():</p> + * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2" + * ng-keydown="setEmpty($event, 'value2', true)"> + * value2: "{{ model.value2 }}" + * </div> * </form> * </div> * </file> + <file name="style.css"> + div { + display: table-cell; + } + div:nth-child(1) { + padding-right: 30px; + } + + </file> * </example> */ - this.$rollbackViewValue = function() { - $timeout.cancel(pendingDebounce); - ctrl.$viewValue = ctrl.$$lastCommittedViewValue; - ctrl.$render(); - }; + $rollbackViewValue: function() { + this.$$timeout.cancel(this.$$pendingDebounce); + this.$viewValue = this.$$lastCommittedViewValue; + this.$render(); + }, /** * @ngdoc method @@ -25480,45 +28570,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * If the validity changes to valid, it will set the model to the last available valid * `$modelValue`, i.e. either the last parsed value or the last value set from the scope. */ - this.$validate = function() { + $validate: function() { // ignore $validate before model is initialized - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { + if (isNumberNaN(this.$modelValue)) { return; } - var viewValue = ctrl.$$lastCommittedViewValue; + var viewValue = this.$$lastCommittedViewValue; // Note: we use the $$rawModelValue as $modelValue might have been // set to undefined during a view -> model update that found validation // errors. We can't parse the view here, since that could change // the model although neither viewValue nor the model on the scope changed - var modelValue = ctrl.$$rawModelValue; + var modelValue = this.$$rawModelValue; - var prevValid = ctrl.$valid; - var prevModelValue = ctrl.$modelValue; + var prevValid = this.$valid; + var prevModelValue = this.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; + var allowInvalid = this.$options.getOption('allowInvalid'); - ctrl.$$runValidators(modelValue, viewValue, function(allValid) { + var that = this; + this.$$runValidators(modelValue, viewValue, function(allValid) { // If there was no change in validity, don't update the model // This prevents changing an invalid modelValue to undefined if (!allowInvalid && prevValid !== allValid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } }); + }, - }; - - this.$$runValidators = function(modelValue, viewValue, doneCallback) { - currentValidationRunId++; - var localValidationRunId = currentValidationRunId; + $$runValidators: function(modelValue, viewValue, doneCallback) { + this.$$currentValidationRunId++; + var localValidationRunId = this.$$currentValidationRunId; + var that = this; // check parser error if (!processParseErrors()) { @@ -25532,34 +28623,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ processAsyncValidators(); function processParseErrors() { - var errorKey = ctrl.$$parserName || 'parse'; - if (isUndefined(parserValid)) { + var errorKey = that.$$parserName || 'parse'; + if (isUndefined(that.$$parserValid)) { setValidity(errorKey, null); } else { - if (!parserValid) { - forEach(ctrl.$validators, function(v, name) { + if (!that.$$parserValid) { + forEach(that.$validators, function(v, name) { setValidity(name, null); }); - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); } // Set the parse error last, to prevent unsetting it, should a $validators key == parserName - setValidity(errorKey, parserValid); - return parserValid; + setValidity(errorKey, that.$$parserValid); + return that.$$parserValid; } return true; } function processSyncValidators() { var syncValidatorsValid = true; - forEach(ctrl.$validators, function(validator, name) { - var result = validator(modelValue, viewValue); + forEach(that.$validators, function(validator, name) { + var result = Boolean(validator(modelValue, viewValue)); syncValidatorsValid = syncValidatorsValid && result; setValidity(name, result); }); if (!syncValidatorsValid) { - forEach(ctrl.$asyncValidators, function(v, name) { + forEach(that.$asyncValidators, function(v, name) { setValidity(name, null); }); return false; @@ -25570,16 +28661,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ function processAsyncValidators() { var validatorPromises = []; var allValid = true; - forEach(ctrl.$asyncValidators, function(validator, name) { + forEach(that.$asyncValidators, function(validator, name) { var promise = validator(modelValue, viewValue); if (!isPromiseLike(promise)) { - throw ngModelMinErr("$asyncValidators", - "Expected asynchronous validator to return a promise but got '{0}' instead.", promise); + throw ngModelMinErr('nopromise', + 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise); } setValidity(name, undefined); validatorPromises.push(promise.then(function() { setValidity(name, true); - }, function(error) { + }, function() { allValid = false; setValidity(name, false); })); @@ -25587,25 +28678,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ if (!validatorPromises.length) { validationDone(true); } else { - $q.all(validatorPromises).then(function() { + that.$$q.all(validatorPromises).then(function() { validationDone(allValid); }, noop); } } function setValidity(name, isValid) { - if (localValidationRunId === currentValidationRunId) { - ctrl.$setValidity(name, isValid); + if (localValidationRunId === that.$$currentValidationRunId) { + that.$setValidity(name, isValid); } } function validationDone(allValid) { - if (localValidationRunId === currentValidationRunId) { + if (localValidationRunId === that.$$currentValidationRunId) { doneCallback(allValid); } } - }; + }, /** * @ngdoc method @@ -25618,84 +28709,87 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * event defined in `ng-model-options`. this method is rarely needed as `NgModelController` * usually handles calling this in response to input events. */ - this.$commitViewValue = function() { - var viewValue = ctrl.$viewValue; + $commitViewValue: function() { + var viewValue = this.$viewValue; - $timeout.cancel(pendingDebounce); + this.$$timeout.cancel(this.$$pendingDebounce); // If the view value has not changed then we should just exit, except in the case where there is // a native validator on the element. In this case the validation state may have changed even though // the viewValue has stayed empty. - if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) { + if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) { return; } - ctrl.$$updateEmptyClasses(viewValue); - ctrl.$$lastCommittedViewValue = viewValue; + this.$$updateEmptyClasses(viewValue); + this.$$lastCommittedViewValue = viewValue; // change to dirty - if (ctrl.$pristine) { + if (this.$pristine) { this.$setDirty(); } this.$$parseAndValidate(); - }; + }, - this.$$parseAndValidate = function() { - var viewValue = ctrl.$$lastCommittedViewValue; + $$parseAndValidate: function() { + var viewValue = this.$$lastCommittedViewValue; var modelValue = viewValue; - parserValid = isUndefined(modelValue) ? undefined : true; + var that = this; - if (parserValid) { - for (var i = 0; i < ctrl.$parsers.length; i++) { - modelValue = ctrl.$parsers[i](modelValue); + this.$$parserValid = isUndefined(modelValue) ? undefined : true; + + if (this.$$parserValid) { + for (var i = 0; i < this.$parsers.length; i++) { + modelValue = this.$parsers[i](modelValue); if (isUndefined(modelValue)) { - parserValid = false; + this.$$parserValid = false; break; } } } - if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) { - // ctrl.$modelValue has not been touched yet... - ctrl.$modelValue = ngModelGet($scope); + if (isNumberNaN(this.$modelValue)) { + // this.$modelValue has not been touched yet... + this.$modelValue = this.$$ngModelGet(this.$$scope); } - var prevModelValue = ctrl.$modelValue; - var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid; - ctrl.$$rawModelValue = modelValue; + var prevModelValue = this.$modelValue; + var allowInvalid = this.$options.getOption('allowInvalid'); + this.$$rawModelValue = modelValue; if (allowInvalid) { - ctrl.$modelValue = modelValue; + this.$modelValue = modelValue; writeToModelIfNeeded(); } // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date. // This can happen if e.g. $setViewValue is called from inside a parser - ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) { + this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) { if (!allowInvalid) { - // Note: Don't check ctrl.$valid here, as we could have + // Note: Don't check this.$valid here, as we could have // external validators (e.g. calculated on the server), // that just call $setValidity and need the model value // to calculate their validity. - ctrl.$modelValue = allValid ? modelValue : undefined; + that.$modelValue = allValid ? modelValue : undefined; writeToModelIfNeeded(); } }); function writeToModelIfNeeded() { - if (ctrl.$modelValue !== prevModelValue) { - ctrl.$$writeModelToScope(); + if (that.$modelValue !== prevModelValue) { + that.$$writeModelToScope(); } } - }; + }, - this.$$writeModelToScope = function() { - ngModelSet($scope, ctrl.$modelValue); - forEach(ctrl.$viewChangeListeners, function(listener) { + $$writeModelToScope: function() { + this.$$ngModelSet(this.$$scope, this.$modelValue); + forEach(this.$viewChangeListeners, function(listener) { try { listener(); } catch (e) { - $exceptionHandler(e); + // eslint-disable-next-line no-invalid-this + this.$$exceptionHandler(e); } - }); - }; + }, this); + }, /** * @ngdoc method @@ -25711,9 +28805,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * * 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. + * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and + * `$asyncValidators` are called and the value is applied to `$modelValue`. + * Finally, the value is set to the **expression** specified in the `ng-model` attribute and + * 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 @@ -25728,7 +28823,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * However, custom controls might also pass objects to this method. In this case, we should make * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not * perform a deep watch of objects, it only looks for a change of identity. If you only change - * the property of the object then ngModel will not realise that the object has changed and + * the property of the object then ngModel will not realize that the object has changed and * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should * not change properties of the copy once it has been passed to `$setViewValue`. * Otherwise you may cause the model value on the scope to change incorrectly. @@ -25747,43 +28842,62 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * @param {*} value value from the view. * @param {string} trigger Event that triggered the update. */ - this.$setViewValue = function(value, trigger) { - ctrl.$viewValue = value; - if (!ctrl.$options || ctrl.$options.updateOnDefault) { - ctrl.$$debounceViewValueCommit(trigger); + $setViewValue: function(value, trigger) { + this.$viewValue = value; + if (this.$options.getOption('updateOnDefault')) { + this.$$debounceViewValueCommit(trigger); } - }; + }, - this.$$debounceViewValueCommit = function(trigger) { - var debounceDelay = 0, - options = ctrl.$options, - debounce; + $$debounceViewValueCommit: function(trigger) { + var debounceDelay = this.$options.getOption('debounce'); - if (options && isDefined(options.debounce)) { - debounce = options.debounce; - if (isNumber(debounce)) { - debounceDelay = debounce; - } else if (isNumber(debounce[trigger])) { - debounceDelay = debounce[trigger]; - } else if (isNumber(debounce['default'])) { - debounceDelay = debounce['default']; - } + if (isNumber(debounceDelay[trigger])) { + debounceDelay = debounceDelay[trigger]; + } else if (isNumber(debounceDelay['default'])) { + debounceDelay = debounceDelay['default']; } - $timeout.cancel(pendingDebounce); - if (debounceDelay) { - pendingDebounce = $timeout(function() { - ctrl.$commitViewValue(); + this.$$timeout.cancel(this.$$pendingDebounce); + var that = this; + if (debounceDelay > 0) { // this fails if debounceDelay is an object + this.$$pendingDebounce = this.$$timeout(function() { + that.$commitViewValue(); }, debounceDelay); - } else if ($rootScope.$$phase) { - ctrl.$commitViewValue(); + } else if (this.$$scope.$root.$$phase) { + this.$commitViewValue(); } else { - $scope.$apply(function() { - ctrl.$commitViewValue(); + this.$$scope.$apply(function() { + that.$commitViewValue(); }); } - }; + }, + /** + * @ngdoc method + * + * @name ngModel.NgModelController#$overrideModelOptions + * + * @description + * + * Override the current model options settings programmatically. + * + * The previous `ModelOptions` value will not be modified. Instead, a + * new `ModelOptions` object will inherit from the previous one overriding + * or inheriting settings that are defined in the given parameter. + * + * See {@link ngModelOptions} for information about what options can be specified + * and how model option inheritance works. + * + * @param {Object} options a hash of settings to override the previous options + * + */ + $overrideModelOptions: function(options) { + this.$options = this.$options.createChild(options); + } +}; + +function setupModelWatcher(ctrl) { // model -> value // Note: we cannot use a normal scope.$watch as we want to detect the following: // 1. scope value is 'a' @@ -25792,17 +28906,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ // -> scope value did not change since the last digest as // ng-change executes in apply phase // 4. view should be changed back to 'a' - $scope.$watch(function ngModelWatch() { - var modelValue = ngModelGet($scope); + ctrl.$$scope.$watch(function ngModelWatch() { + var modelValue = ctrl.$$ngModelGet(ctrl.$$scope); // if scope model value and ngModel value are out of sync // TODO(perf): why not move this to the action fn? if (modelValue !== ctrl.$modelValue && // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator + // eslint-disable-next-line no-self-compare (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue) ) { ctrl.$modelValue = ctrl.$$rawModelValue = modelValue; - parserValid = undefined; + ctrl.$$parserValid = undefined; var formatters = ctrl.$formatters, idx = formatters.length; @@ -25816,13 +28931,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue; ctrl.$render(); - ctrl.$$runValidators(modelValue, viewValue, noop); + // It is possible that model and view value have been updated during render + ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop); } } return modelValue; }); -}]; +} + +/** + * @ngdoc method + * @name ngModel.NgModelController#$setValidity + * + * @description + * Change the validity state, and notify the form. + * + * This method can be called within $parsers/$formatters or a custom validation implementation. + * However, in most cases it should be sufficient to use the `ngModel.$validators` and + * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically. + * + * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned + * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` + * (for unfulfilled `$asyncValidators`), so that it is available for data-binding. + * The `validationErrorKey` should be in camelCase and will get converted into dash-case + * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error` + * class and can be bound to as `{{someForm.someControl.$error.myError}}` . + * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined), + * or skipped (null). Pending is used for unfulfilled `$asyncValidators`. + * Skipped is used by Angular when validators do not run because of parse errors and + * when `$asyncValidators` do not run because any of the `$validators` failed. + */ +addSetValidityMethod({ + clazz: NgModelController, + set: function(object, property) { + object[property] = true; + }, + unset: function(object, property) { + delete object[property]; + } +}); /** @@ -25872,6 +29020,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. @@ -25916,7 +29080,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$ * </pre> * * @example - * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample"> + * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample" name="ng-model"> <file name="index.html"> <script> angular.module('inputExample', []) @@ -26012,9 +29176,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) { return { pre: function ngModelPreLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0], - formCtrl = ctrls[1] || modelCtrl.$$parentForm; + formCtrl = ctrls[1] || modelCtrl.$$parentForm, + optionsCtrl = ctrls[2]; + + if (optionsCtrl) { + modelCtrl.$options = optionsCtrl.$options; + } - modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options); + modelCtrl.$$initGetterSetters(); // notify others, especially parent forms formCtrl.$addControl(modelCtrl); @@ -26031,19 +29200,23 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }, post: function ngModelPostLink(scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - if (modelCtrl.$options && modelCtrl.$options.updateOn) { - element.on(modelCtrl.$options.updateOn, function(ev) { + if (modelCtrl.$options.getOption('updateOn')) { + element.on(modelCtrl.$options.getOption('updateOn'), function(ev) { modelCtrl.$$debounceViewValueCommit(ev && ev.type); }); } - element.on('blur', function(ev) { + function setTouched() { + modelCtrl.$setTouched(); + } + + element.on('blur', function() { if (modelCtrl.$touched) return; if ($rootScope.$$phase) { - scope.$evalAsync(modelCtrl.$setTouched); + scope.$evalAsync(setTouched); } else { - scope.$apply(modelCtrl.$setTouched); + scope.$apply(setTouched); } }); } @@ -26052,23 +29225,173 @@ var ngModelDirective = ['$rootScope', function($rootScope) { }; }]; +/* exported defaultModelOptions */ +var defaultModelOptions; var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; /** + * @ngdoc type + * @name ModelOptions + * @description + * A container for the options set by the {@link ngModelOptions} directive + */ +function ModelOptions(options) { + this.$$options = options; +} + +ModelOptions.prototype = { + + /** + * @ngdoc method + * @name ModelOptions#getOption + * @param {string} name the name of the option to retrieve + * @returns {*} the value of the option + * @description + * Returns the value of the given option + */ + getOption: function(name) { + return this.$$options[name]; + }, + + /** + * @ngdoc method + * @name ModelOptions#createChild + * @param {Object} options a hash of options for the new child that will override the parent's options + * @return {ModelOptions} a new `ModelOptions` object initialized with the given options. + */ + createChild: function(options) { + var inheritAll = false; + + // make a shallow copy + options = extend({}, options); + + // Inherit options from the parent if specified by the value `"$inherit"` + forEach(options, /* @this */ function(option, key) { + if (option === '$inherit') { + if (key === '*') { + inheritAll = true; + } else { + options[key] = this.$$options[key]; + // `updateOn` is special so we must also inherit the `updateOnDefault` option + if (key === 'updateOn') { + options.updateOnDefault = this.$$options.updateOnDefault; + } + } + } else { + if (key === 'updateOn') { + // If the `updateOn` property contains the `default` event then we have to remove + // it from the event list and set the `updateOnDefault` flag. + options.updateOnDefault = false; + options[key] = trim(option.replace(DEFAULT_REGEXP, function() { + options.updateOnDefault = true; + return ' '; + })); + } + } + }, this); + + if (inheritAll) { + // We have a property of the form: `"*": "$inherit"` + delete options['*']; + defaults(options, this.$$options); + } + + // Finally add in any missing defaults + defaults(options, defaultModelOptions.$$options); + + return new ModelOptions(options); + } +}; + + +defaultModelOptions = new ModelOptions({ + updateOn: '', + updateOnDefault: true, + debounce: 0, + getterSetter: false, + allowInvalid: false, + timezone: null +}); + + +/** * @ngdoc directive * @name ngModelOptions * * @description - * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of - * events that will trigger a model update and/or a debouncing delay so that the actual update only - * takes place when a timer expires; this timer will be reset after another change takes place. + * This directive allows you to modify the behaviour of {@link ngModel} directives within your + * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel} + * directives will use the options of their nearest `ngModelOptions` ancestor. + * + * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as + * an Angular expression. This expression should evaluate to an object, whose properties contain + * the settings. For example: `<div "ng-model-options"="{ debounce: 100 }"`. + * + * ## Inheriting Options + * + * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions` + * directive by giving it the value of `"$inherit"`. + * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the + * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings + * will be used. + * + * For example given the following fragment of HTML + * + * + * ```html + * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> + * <form ng-model-options="{ updateOn: 'blur', allowInvalid: '$inherit' }"> + * <input ng-model-options="{ updateOn: 'default', allowInvalid: '$inherit' }" /> + * </form> + * </div> + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 0 } + * ``` + * + * Notice that the `debounce` setting was not inherited and used the default value instead. + * + * You can specify that all undefined settings are automatically inherited from an ancestor by + * including a property with key of `"*"` and value of `"$inherit"`. + * + * For example given the following fragment of HTML + * + * + * ```html + * <div ng-model-options="{ allowInvalid: true, debounce: 200 }"> + * <form ng-model-options="{ updateOn: 'blur', "*": '$inherit' }"> + * <input ng-model-options="{ updateOn: 'default', "*": '$inherit' }" /> + * </form> + * </div> + * ``` + * + * the `input` element will have the following settings + * + * ```js + * { allowInvalid: true, updateOn: 'default', debounce: 200 } + * ``` + * + * Notice that the `debounce` setting now inherits the value from the outer `<div>` element. + * + * If you are creating a reusable component then you should be careful when using `"*": "$inherit"` + * since you may inadvertently inherit a setting in the future that changes the behavior of your component. + * + * + * ## Triggering and debouncing model updates + * + * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will + * trigger a model update and/or a debouncing delay so that the actual update only 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 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 + * 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. * - * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`} + * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue} * method is by making sure the input is placed inside a form that has a `name` attribute. This is * important because `form` controllers are published to the related scope under the name in their * `name` attribute. @@ -26077,271 +29400,194 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit` * to have access to the updated model. * - * `ngModelOptions` has an effect on the element it's declared on and its descendants. + * The following example shows how to override immediate updates. Changes on the inputs within the + * form will update the model only when the control loses focus (blur event). If `escape` key is + * pressed while the input field is focused, the value is reset to the value in the current model. + * + * <example name="ngModelOptions-directive-blur" module="optionsExample"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="userForm"> + * <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> + * </div> + * </file> + * <file name="app.js"> + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say', data: '' }; + * + * $scope.cancel = function(e) { + * if (e.keyCode === 27) { + * $scope.userForm.userName.$rollbackViewValue(); + * } + * }; + * }]); + * </file> + * <file name="protractor.js" type="protractor"> + * var model = element(by.binding('user.name')); + * var input = element(by.model('user.name')); + * var other = element(by.model('user.data')); + * + * it('should allow custom events', function() { + * input.sendKeys(' hello'); + * input.click(); + * expect(model.getText()).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say hello'); + * }); * - * @param {Object} ngModelOptions options to apply to the current model. Valid keys are: + * it('should $rollbackViewValue when model changes', function() { + * input.sendKeys(' hello'); + * expect(input.getAttribute('value')).toEqual('say hello'); + * input.sendKeys(protractor.Key.ESCAPE); + * expect(input.getAttribute('value')).toEqual('say'); + * other.click(); + * expect(model.getText()).toEqual('say'); + * }); + * </file> + * </example> + * + * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change. + * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. + * + * <example name="ngModelOptions-directive-debounce" module="optionsExample"> + * <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 /> + * </form> + * <pre>user.name = <span ng-bind="user.name"></span></pre> + * </div> + * </file> + * <file name="app.js"> + * angular.module('optionsExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.user = { name: 'say' }; + * }]); + * </file> + * </example> + * + * ## Model updates and validation + * + * The default behaviour in `ngModel` is that the model value is set to `undefined` when the + * validation determines that the value is invalid. By setting the `allowInvalid` property to true, + * the model will still be updated even if the value is invalid. + * + * + * ## Connecting to the scope + * + * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression + * on the scope refers to a "getter/setter" function rather than the value itself. + * + * The following example shows how to bind to getter/setters: + * + * <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="userForm"> + * <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> + * </file> + * <file name="app.js"> + * angular.module('getterSetterExample', []) + * .controller('ExampleController', ['$scope', function($scope) { + * var _name = 'Brian'; + * $scope.user = { + * name: function(newName) { + * return angular.isDefined(newName) ? (_name = newName) : _name; + * } + * }; + * }]); + * </file> + * </example> + * + * + * ## Specifying timezones + * + * You can specify the timezone that date/time input directives expect by providing its name in the + * `timezone` property. + * + * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and + * and its descendents. Valid keys are: * - `updateOn`: string specifying which event should the input be bound to. You can set several * events using an space delimited list. There is a special event called `default` that - * matches the default events belonging of the control. + * matches the default events belonging to the control. * - `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. + * `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">`, ... . It understands UTC/GMT and the + * `<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 - - The following example shows how to override immediate updates. Changes on the inputs within the - form will update the model only when the control loses focus (blur event). If `escape` key is - pressed while the input field is focused, the value is reset to the value in the current model. - - <example name="ngModelOptions-directive-blur" module="optionsExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <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: 'John', data: '' }; - - $scope.cancel = function(e) { - if (e.keyCode == 27) { - $scope.userForm.userName.$rollbackViewValue(); - } - }; - }]); - </file> - <file name="protractor.js" type="protractor"> - var model = element(by.binding('user.name')); - var input = element(by.model('user.name')); - var other = element(by.model('user.data')); - - it('should allow custom events', function() { - input.sendKeys(' Doe'); - input.click(); - expect(model.getText()).toEqual('John'); - other.click(); - expect(model.getText()).toEqual('John Doe'); - }); - - it('should $rollbackViewValue when model changes', function() { - input.sendKeys(' Doe'); - expect(input.getAttribute('value')).toEqual('John Doe'); - input.sendKeys(protractor.Key.ESCAPE); - expect(input.getAttribute('value')).toEqual('John'); - other.click(); - expect(model.getText()).toEqual('John'); - }); - </file> - </example> - - This one shows how to debounce model changes. Model will be updated only 1 sec after last change. - If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty. - - <example name="ngModelOptions-directive-debounce" module="optionsExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <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> - </file> - <file name="app.js"> - angular.module('optionsExample', []) - .controller('ExampleController', ['$scope', function($scope) { - $scope.user = { name: 'Igor' }; - }]); - </file> - </example> - - This one shows how to bind to getter/setters: - - <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <form name="userForm"> - <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> - </file> - <file name="app.js"> - angular.module('getterSetterExample', []) - .controller('ExampleController', ['$scope', function($scope) { - var _name = 'Brian'; - $scope.user = { - name: function(newName) { - // Note that newName can be undefined for two reasons: - // 1. Because it is called as a getter and thus called with no arguments - // 2. Because the property should actually be set to undefined. This happens e.g. if the - // input is invalid - return arguments.length ? (_name = newName) : _name; - } - }; - }]); - </file> - </example> */ var ngModelOptionsDirective = function() { - return { - restrict: 'A', - controller: ['$scope', '$attrs', function($scope, $attrs) { - var that = this; - this.$options = copy($scope.$eval($attrs.ngModelOptions)); - // Allow adding/overriding bound events - 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() { - that.$options.updateOnDefault = true; - return ' '; - })); - } else { - this.$options.updateOnDefault = true; - } - }] - }; -}; - - - -// helper methods -function addSetValidityMethod(context) { - var ctrl = context.ctrl, - $element = context.$element, - classCache = {}, - set = context.set, - unset = context.unset, - $animate = context.$animate; - - classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS)); - - ctrl.$setValidity = setValidity; - - function setValidity(validationErrorKey, state, controller) { - if (isUndefined(state)) { - createAndSet('$pending', validationErrorKey, controller); - } else { - unsetAndCleanup('$pending', validationErrorKey, controller); - } - if (!isBoolean(state)) { - unset(ctrl.$error, validationErrorKey, controller); - unset(ctrl.$$success, validationErrorKey, controller); - } else { - if (state) { - unset(ctrl.$error, validationErrorKey, controller); - set(ctrl.$$success, validationErrorKey, controller); - } else { - set(ctrl.$error, validationErrorKey, controller); - unset(ctrl.$$success, validationErrorKey, controller); - } - } - if (ctrl.$pending) { - cachedToggleClass(PENDING_CLASS, true); - ctrl.$valid = ctrl.$invalid = undefined; - toggleValidationCss('', null); - } else { - cachedToggleClass(PENDING_CLASS, false); - ctrl.$valid = isObjectEmpty(ctrl.$error); - ctrl.$invalid = !ctrl.$valid; - toggleValidationCss('', ctrl.$valid); - } - - // re-read the state as the set/unset methods could have - // combined state in ctrl.$error[validationError] (used for forms), - // where setting/unsetting only increments/decrements the value, - // and does not replace it. - var combinedState; - if (ctrl.$pending && ctrl.$pending[validationErrorKey]) { - combinedState = undefined; - } else if (ctrl.$error[validationErrorKey]) { - combinedState = false; - } else if (ctrl.$$success[validationErrorKey]) { - combinedState = true; - } else { - combinedState = null; - } - - toggleValidationCss(validationErrorKey, combinedState); - ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl); + NgModelOptionsController.$inject = ['$attrs', '$scope']; + function NgModelOptionsController($attrs, $scope) { + this.$$attrs = $attrs; + this.$$scope = $scope; } + NgModelOptionsController.prototype = { + $onInit: function() { + var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions; + var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions); - function createAndSet(name, value, controller) { - if (!ctrl[name]) { - ctrl[name] = {}; + this.$options = parentOptions.createChild(modelOptionsDefinition); } - set(ctrl[name], value, controller); - } - - function unsetAndCleanup(name, value, controller) { - if (ctrl[name]) { - unset(ctrl[name], value, controller); - } - if (isObjectEmpty(ctrl[name])) { - ctrl[name] = undefined; - } - } - - function cachedToggleClass(className, switchValue) { - if (switchValue && !classCache[className]) { - $animate.addClass($element, className); - classCache[className] = true; - } else if (!switchValue && classCache[className]) { - $animate.removeClass($element, className); - classCache[className] = false; - } - } + }; - function toggleValidationCss(validationErrorKey, isValid) { - validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : ''; + return { + restrict: 'A', + // ngModelOptions needs to run before ngModel and input directives + priority: 10, + require: {parentCtrl: '?^^ngModelOptions'}, + bindToController: true, + controller: NgModelOptionsController + }; +}; - cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true); - cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false); - } -} -function isObjectEmpty(obj) { - if (obj) { - for (var prop in obj) { - if (obj.hasOwnProperty(prop)) { - return false; - } +// shallow copy over values from `src` that are not already specified on `dst` +function defaults(dst, src) { + forEach(src, function(value, key) { + if (!isDefined(dst[key])) { + dst[key] = value; } - } - return true; + }); } /** @@ -26363,7 +29609,7 @@ function isObjectEmpty(obj) { * but the one wrapped in `ngNonBindable` is left alone. * * @example - <example> + <example name="ng-non-bindable"> <file name="index.html"> <div>Normal: {{1 + 2}}</div> <div ng-non-bindable>Ignored: {{1 + 2}}</div> @@ -26378,6 +29624,8 @@ function isObjectEmpty(obj) { */ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 }); +/* exported ngOptionsDirective */ + /* global jqLiteRemove */ var ngOptionsMinErr = minErr('ngOptions'); @@ -26393,13 +29641,12 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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. + * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `<option>` elements instead of + * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits: + * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression + * - reduced memory consumption by not creating a new scope for each repeated instance + * - increased render speed by creating the options in a documentFragment instead of individually * * 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` @@ -26488,13 +29735,8 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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: + * @param {string} ngModel Assignable AngularJS expression to data-bind to. + * @param {comprehension_expression} ngOptions in one of the following forms: * * * for array data sources: * * `label` **`for`** `value` **`in`** `array` @@ -26533,9 +29775,16 @@ var ngOptionsMinErr = minErr('ngOptions'); * 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). + * @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 {string=} ngAttrSize sets the size of the select element dynamically. Uses the + * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive. * * @example - <example module="selectExample"> + <example module="selectExample" name="select"> <file name="index.html"> <script> angular.module('selectExample', []) @@ -26608,9 +29857,9 @@ var ngOptionsMinErr = minErr('ngOptions'); </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]+?))?$/; +/* eslint-disable max-len */ +// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999 +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) @@ -26620,19 +29869,19 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s // 7: object item value variable name // 8: collection expression // 9: track by expression -// jshint maxlen: 100 +/* eslint-enable */ -var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { +var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $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}", + 'Expected expression in form of ' + + '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' + + ' but got \'{0}\'. Element: {1}', optionsExp, startingTag(selectElement)); } @@ -26720,8 +29969,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index]; var value = optionValues[key]; - var locals = getLocals(optionValues[key], key); - var selectValue = getTrackByValueFn(optionValues[key], locals); + var locals = getLocals(value, key); + var selectValue = getTrackByValueFn(value, locals); watchedArray.push(selectValue); // Only need to watch the displayFn if there is a specific label expression @@ -26774,7 +30023,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { 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; + return trackBy ? copy(option.viewValue) : option.viewValue; } }; } @@ -26784,8 +30033,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { // we can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. - var optionTemplate = document.createElement('option'), - optGroupTemplate = document.createElement('optgroup'); + var optionTemplate = window.document.createElement('option'), + optGroupTemplate = window.document.createElement('optgroup'); function ngOptionsPostLink(scope, selectElement, attr, ctrls) { @@ -26795,72 +30044,65 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { // 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); + selectCtrl.hasEmptyOption = true; + selectCtrl.emptyOption = children.eq(i); break; } } - var providedEmptyOption = !!emptyOption; + var providedEmptyOption = !!selectCtrl.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(); + // This stores the newly created options before they are appended to the select. + // Since the contents are removed from the fragment when it is appended, + // we only need to create it once. + var listFragment = $document[0].createDocumentFragment(); + + // Overwrite the implementation. ngOptions doesn't use hashes + selectCtrl.generateUnknownOptionValue = function(val) { + return '?'; }; // Update the controller methods for multiple selectable options if (!multiple) { selectCtrl.writeValue = function writeNgOptionsValue(value) { + var selectedOption = options.selectValueMap[selectElement.val()]; var option = options.getOptionFromViewValue(value); - if (option && !option.disabled) { + // Make sure to remove the selected attribute from the previously selected option + // Otherwise, screen readers might get confused + if (selectedOption) selectedOption.element.removeAttribute('selected'); + + if (option) { + // Don't update the option when it is already selected. + // For example, the browser will select the first option by default. In that case, + // most properties are set automatically - except the `selected` attribute, which we + // set always + if (selectElement[0].value !== option.selectValue) { - removeUnknownOption(); - removeEmptyOption(); + selectCtrl.removeUnknownOption(); + selectCtrl.unselectEmptyOption(); selectElement[0].value = option.selectValue; option.element.selected = true; - option.element.setAttribute('selected', 'selected'); } + + option.element.setAttribute('selected', 'selected'); } else { - if (value === null || providedEmptyOption) { - removeUnknownOption(); - renderEmptyOption(); + + if (providedEmptyOption) { + selectCtrl.selectEmptyOption(); + } else if (selectCtrl.unknownOption.parent().length) { + selectCtrl.updateUnknownOption(value); } else { - removeEmptyOption(); - renderUnknownOption(); + selectCtrl.renderUnknownOption(value); } } }; @@ -26870,8 +30112,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { var selectedOption = options.selectValueMap[selectElement.val()]; if (selectedOption && !selectedOption.disabled) { - removeEmptyOption(); - removeUnknownOption(); + selectCtrl.unselectEmptyOption(); + selectCtrl.removeUnknownOption(); return options.getViewValueFromOption(selectedOption); } return null; @@ -26879,6 +30121,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { // If we are using `track by` then we must watch the tracked value on the model // since ngModel only watches for object identity change + // FIXME: When a user selects an option, this watch will fire needlessly if (ngOptions.trackBy) { scope.$watch( function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); }, @@ -26888,22 +30131,17 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { } else { - ngModelCtrl.$isEmpty = function(value) { - return !value || value.length === 0; - }; + selectCtrl.writeValue = function writeNgOptionsMultiple(values) { + // Only set `<option>.selected` if necessary, in order to prevent some browsers from + // scrolling to `<option>` elements that are outside the `<select>` element's viewport. + var selectedOptions = values && values.map(getAndUpdateSelectedOption) || []; - selectCtrl.writeValue = function writeNgOptionsMultiple(value) { options.items.forEach(function(option) { - option.element.selected = false; + if (option.element.selected && !includes(selectedOptions, option)) { + option.element.selected = false; + } }); - - if (value) { - value.forEach(function(item) { - var option = options.getOptionFromViewValue(item); - if (option && !option.disabled) option.element.selected = true; - }); - } }; @@ -26936,23 +30174,48 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { } } - if (providedEmptyOption) { // we need to remove it before calling selectElement.empty() because otherwise IE will // remove the label from the element. wtf? - emptyOption.remove(); + selectCtrl.emptyOption.remove(); // compile the element since there might be bindings in it - $compile(emptyOption)(scope); + $compile(selectCtrl.emptyOption)(scope); + + if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) { + // This means the empty option has currently no actual DOM node, probably because + // it has been modified by a transclusion directive. + selectCtrl.hasEmptyOption = false; + + // Redefine the registerOption function, which will catch + // options that are added by ngIf etc. (rendering of the node is async because of + // lazy transclusion) + selectCtrl.registerOption = function(optionScope, optionEl) { + if (optionEl.val() === '') { + selectCtrl.hasEmptyOption = true; + selectCtrl.emptyOption = optionEl; + selectCtrl.emptyOption.removeClass('ng-scope'); + // This ensures the new empty option is selected if previously no option was selected + ngModelCtrl.$render(); + + optionEl.on('$destroy', function() { + selectCtrl.hasEmptyOption = false; + selectCtrl.emptyOption = undefined; + }); + } + }; + + } else { + // remove the class, which is added automatically because we recompile the element and it + // becomes the compilation root + selectCtrl.emptyOption.removeClass('ng-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)); } + selectElement.empty(); + // We need to do this here to ensure that the options object is defined // when we first hit it in writeNgOptionsValue updateOptions(); @@ -26962,6 +30225,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { // ------------------------------------------------------------------ // + function addOptionElement(option, parent) { + var optionElement = optionTemplate.cloneNode(false); + parent.appendChild(optionElement); + updateOptionElement(option, optionElement); + } + + function getAndUpdateSelectedOption(viewValue) { + var option = options.getOptionFromViewValue(viewValue); + var element = option && option.element; + + if (element && !element.selected) element.selected = true; + + return option; + } function updateOptionElement(option, element) { option.element = element; @@ -26975,143 +30252,78 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { 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; - } + element.value = option.selectValue; } + function updateOptions() { + var previousValue = options && selectCtrl.readValue(); - 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 || - current.value === '')) { - current = current.nextSibling; + // We must remove all current options, but cannot simply set innerHTML = null + // since the providedEmptyOption might have an ngIf on it that inserts comments which we + // must preserve. + // Instead, iterate over the current option elements and remove them or their optgroup + // parents + if (options) { + + for (var i = options.items.length - 1; i >= 0; i--) { + var option = options.items[i]; + if (isDefined(option.group)) { + jqLiteRemove(option.element.parentNode); + } else { + jqLiteRemove(option.element); + } } } - return current; - } - - - function updateOptions() { - - var previousValue = options && selectCtrl.readValue(); options = ngOptions.getOptions(); - var groupMap = {}; - var currentElement = selectElement[0].firstChild; + var groupElementMap = {}; // Ensure that the empty option is always there if it was explicitly provided if (providedEmptyOption) { - selectElement.prepend(emptyOption); + selectElement.prepend(selectCtrl.emptyOption); } - currentElement = skipEmptyAndUnknownOptions(currentElement); - - options.items.forEach(function updateOption(option) { - var group; + options.items.forEach(function addOption(option) { var groupElement; - var optionElement; if (isDefined(option.group)) { // This option is to live in a group // See if we have already created this group - group = groupMap[option.group]; + groupElement = groupElementMap[option.group]; - if (!group) { + if (!groupElement) { - // We have not already created this group - groupElement = addOrReuseElement(selectElement[0], - currentElement, - 'optgroup', - optGroupTemplate); - // Move to the next element - currentElement = groupElement.nextSibling; + groupElement = optGroupTemplate.cloneNode(false); + listFragment.appendChild(groupElement); // Update the label on the group element - groupElement.label = option.group; + // "null" is special cased because of Safari + groupElement.label = option.group === null ? 'null' : option.group; // Store it for use later - group = groupMap[option.group] = { - groupElement: groupElement, - currentOptionElement: groupElement.firstChild - }; - + groupElementMap[option.group] = groupElement; } - // So now we have a group for this option we add the option to the group - optionElement = addOrReuseElement(group.groupElement, - group.currentOptionElement, - 'option', - optionTemplate); - updateOptionElement(option, optionElement); - // Move to the next element - group.currentOptionElement = optionElement.nextSibling; + addOptionElement(option, groupElement); } else { // This option is not in a group - optionElement = addOrReuseElement(selectElement[0], - currentElement, - 'option', - optionTemplate); - updateOptionElement(option, optionElement); - // Move to the next element - currentElement = optionElement.nextSibling; + addOptionElement(option, listFragment); } }); - - // Now remove all excess options and group - Object.keys(groupMap).forEach(function(key) { - removeExcessElements(groupMap[key].currentOptionElement); - }); - removeExcessElements(currentElement); + selectElement[0].appendChild(listFragment); ngModelCtrl.$render(); // Check to see if the value has changed due to the update to the options if (!ngModelCtrl.$isEmpty(previousValue)) { var nextValue = selectCtrl.readValue(); - if (ngOptions.trackBy ? !equals(previousValue, nextValue) : previousValue !== nextValue) { + var isNotPrimitive = ngOptions.trackBy || multiple; + if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) { ngModelCtrl.$setViewValue(nextValue); ngModelCtrl.$render(); } @@ -27228,7 +30440,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) { * @param {number=} offset Offset to deduct from the total number. * * @example - <example module="pluralizeExample"> + <example module="pluralizeExample" name="ng-pluralize"> <file name="index.html"> <script> angular.module('pluralizeExample', []) @@ -27342,7 +30554,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) { var count = parseFloat(newVal); - var countIsNaN = isNaN(count); + var countIsNaN = isNumberNaN(count); if (!countIsNaN && !(count in whens)) { // If an explicit number rule such as 1, 2, 3... is defined, just use it. @@ -27351,13 +30563,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, } // If both `count` and `lastCount` are NaN, we don't need to re-register a watch. - // In JS `NaN !== NaN`, so we have to exlicitly check. - if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) { + // In JS `NaN !== NaN`, so we have to explicitly check. + if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) { watchRemover(); var whenExpFn = whensExpFns[count]; if (isUndefined(whenExpFn)) { if (newVal != null) { - $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp); + $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp); } watchRemover = noop; updateElementText(); @@ -27375,10 +30587,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, }; }]; +/* exported ngRepeatDirective */ + /** * @ngdoc directive * @name ngRepeat * @multiElement + * @restrict A * * @description * The `ngRepeat` directive instantiates a template once per item from a collection. Each template @@ -27411,17 +30626,23 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * <div ng-repeat="(key, value) in myObj"> ... </div> * ``` * - * You need to be aware that the JavaScript specification does not define the order of keys - * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive - * used to sort the keys alphabetically.) + * However, there are a few limitations compared to array iteration: * - * 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). + * - The JavaScript specification does not define the order of keys + * returned for an object, so Angular relies on the order returned by the browser + * when running `for key in myObj`. Browsers generally follow the strategy of providing + * keys in the order in which they were defined, although there are exceptions when keys are deleted + * and reinstated. See the + * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes). * - * 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 + * - `ngRepeat` will silently *ignore* object keys starting with `$`, because + * it's a prefix used by Angular for public (`$`) and private (`$$`) properties. + * + * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with + * objects, and will throw an error if used with one. + * + * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array + * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter) * or implement a `$watch` on the object yourself. * @@ -27429,7 +30650,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * # Tracking and Duplicates * * `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: + * 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. @@ -27437,7 +30658,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * * 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 + * 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 @@ -27464,19 +30685,29 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * ``` * * <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` + * If you are working with objects that have a unique identifier property, you should track + * by this identifier instead of the object instance. Should you reload your data later, `ngRepeat` * will not have to rebuild the DOM elements for items it has already rendered, even if the * JavaScript objects in the collection have been substituted for new ones. For large collections, - * this signifincantly improves rendering performance. If you don't have a unique identifier, + * this significantly improves rendering performance. If you don't have a unique identifier, * `track by $index` can also provide a performance boost. * </div> + * * ```html * <div ng-repeat="model in collection track by model.id"> * {{model.name}} * </div> * ``` * + * <br /> + * <div class="alert alert-warning"> + * Avoid using `track by $index` when the repeated template contains + * {@link guide/expression#one-time-binding one-time bindings}. In such cases, the `nth` DOM + * element will always be matched with the `nth` item of the array, so the bindings on that element + * will not be updated even when the corresponding item changes, essentially causing the view to get + * out-of-sync with the underlying data. + * </div> + * * When no `track by` expression is provided, it is equivalent to tracking by the built-in * `$id` function, which tracks items by their identity: * ```html @@ -27485,15 +30716,17 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * </div> * ``` * + * <br /> * <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> + * <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. @@ -27539,11 +30772,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**). * * @animations - * **.enter** - when a new item is added to the list or when an item is revealed after a filter - * - * **.leave** - when an item is removed from the list or when an item is filtered out + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter | + * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out | + * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered | * - * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered + * See the example below for defining CSS animations with ngRepeat. * * @element ANY * @scope @@ -27597,34 +30832,39 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` . * * @example - * This example initializes the scope to a list of names and - * then uses `ngRepeat` to display every person: - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed + * results by name or by age. New (entering) and removed (leaving) items are animated. + <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true" name="ng-repeat"> <file name="index.html"> - <div ng-init="friends = [ - {name:'John', age:25, gender:'boy'}, - {name:'Jessie', age:30, gender:'girl'}, - {name:'Johanna', age:28, gender:'girl'}, - {name:'Joy', age:15, gender:'girl'}, - {name:'Mary', age:28, gender:'girl'}, - {name:'Peter', age:95, gender:'boy'}, - {name:'Sebastian', age:50, gender:'boy'}, - {name:'Erika', age:27, gender:'girl'}, - {name:'Patrick', age:40, gender:'boy'}, - {name:'Samantha', age:60, gender:'girl'} - ]"> + <div ng-controller="repeatController"> I have {{friends.length}} friends. They are: <input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" /> <ul class="example-animate-container"> <li class="animate-repeat" ng-repeat="friend in friends | filter:q as results"> [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old. </li> - <li class="animate-repeat" ng-if="results.length == 0"> + <li class="animate-repeat" ng-if="results.length === 0"> <strong>No results found...</strong> </li> </ul> </div> </file> + <file name="script.js"> + angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) { + $scope.friends = [ + {name:'John', age:25, gender:'boy'}, + {name:'Jessie', age:30, gender:'girl'}, + {name:'Johanna', age:28, gender:'girl'}, + {name:'Joy', age:15, gender:'girl'}, + {name:'Mary', age:28, gender:'girl'}, + {name:'Peter', age:95, gender:'boy'}, + {name:'Sebastian', age:50, gender:'boy'}, + {name:'Erika', age:27, gender:'girl'}, + {name:'Patrick', age:40, gender:'boy'}, + {name:'Samantha', age:60, gender:'girl'} + ]; + }); + </file> <file name="animations.css"> .example-animate-container { background:white; @@ -27635,7 +30875,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, } .animate-repeat { - line-height:40px; + line-height:30px; list-style:none; box-sizing:border-box; } @@ -27657,7 +30897,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, .animate-repeat.ng-move.ng-move-active, .animate-repeat.ng-enter.ng-enter-active { opacity:1; - max-height:40px; + max-height:30px; } </file> <file name="protractor.js" type="protractor"> @@ -27684,7 +30924,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, </file> </example> */ -var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { +var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) { var NG_REMOVED = '$$NG_REMOVED'; var ngRepeatMinErr = minErr('ngRepeat'); @@ -27696,9 +30936,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { scope.$first = (index === 0); scope.$last = (index === (arrayLength - 1)); scope.$middle = !(scope.$first || scope.$last); - // jshint bitwise: false - scope.$odd = !(scope.$even = (index&1) === 0); - // jshint bitwise: true + // eslint-disable-next-line no-bitwise + scope.$odd = !(scope.$even = (index & 1) === 0); }; var getBlockStart = function(block) { @@ -27719,12 +30958,12 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { $$tlb: true, compile: function ngRepeatCompile($element, $attr) { var expression = $attr.ngRepeat; - var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' '); + var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression); var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/); if (!match) { - throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.", + throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.', expression); } @@ -27733,10 +30972,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { var aliasAs = match[3]; var trackByExp = match[4]; - match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/); + match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/); if (!match) { - throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.", + throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.', lhs); } var valueIdentifier = match[3] || match[1]; @@ -27744,7 +30983,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) || /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) { - throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.", + throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.', aliasAs); } @@ -27840,7 +31079,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { if (block && block.scope) lastBlockMap[block.id] = block; }); throw ngRepeatMinErr('dupes', - "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", + 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}', expression, trackById, value); } else { // new never before seen block @@ -27881,9 +31120,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { nextNode = nextNode.nextSibling; } while (nextNode && nextNode[NG_REMOVED]); - if (getBlockStart(block) != nextNode) { + 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); @@ -27895,8 +31134,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 @@ -27922,11 +31160,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * @multiElement * * @description - * The `ngShow` directive shows or hides the given HTML element based on the expression - * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding - * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined - * in AngularJS and sets the display style to none (using an !important flag). - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * The `ngShow` directive shows or hides the given HTML element based on the expression provided to + * the `ngShow` attribute. + * + * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element. + * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an + * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see + * {@link ng.directive:ngCsp ngCsp}). * * ```html * <!-- when $scope.myValue is truthy (element is visible) --> @@ -27936,31 +31176,32 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * <div ng-show="myValue" class="ng-hide"></div> * ``` * - * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class - * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed - * from the element causing the element not to appear hidden. + * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added + * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide` + * CSS class is removed from the element causing the element not to appear hidden. * - * ## Why is !important used? + * ## Why is `!important` used? * - * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector - * can be easily overridden by heavier selectors. For example, something as simple - * as changing the display style on a HTML list item would make hidden elements appear visible. - * This also becomes a bigger issue when dealing with CSS frameworks. + * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the + * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as + * simple as changing the display style on a HTML list item would make hidden elements appear + * visible. This also becomes a bigger issue when dealing with CSS frameworks. * - * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector - * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the - * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * By using `!important`, the show and hide behavior will work as expected despite any clash between + * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a + * developer chooses to override the styling to change how to hide an element then it is just a + * matter of using `!important` in their own CSS code. * * ### Overriding `.ng-hide` * - * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change - * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` - * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope - * with extra animation classes that can be added. + * By default, the `.ng-hide` class will style the element with `display: none !important`. If you + * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for + * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually + * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added. * * ```css * .ng-hide:not(.ng-hide-animate) { - * /* this is just another form of hiding an element */ + * /* These are just alternative ways of hiding an element */ * display: block!important; * position: absolute; * top: -9999px; @@ -27968,29 +31209,20 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * } * ``` * - * By default you don't need to override in CSS anything and the animations will work around the display style. + * By default you don't need to override anything in CSS and the animations will work around the + * display style. * * ## A note about animations with `ngShow` * - * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass except that - * you must also include the !important flag to override the display property - * so that you can perform an animation when the element is hidden during the time of the animation. + * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the + * directive expression is true and false. This system works like the animation system present with + * `ngClass` except that you must also include the `!important` flag to override the display + * property so that the elements are not actually hidden during the animation. * * ```css - * // - * //a working example can be found at the bottom of this page - * // + * /* A working example can be found at the bottom of this page. */ * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * /* this is required as of 1.3x to properly - * apply all styling in a show/hide animation */ - * transition: 0s linear all; - * } - * - * .my-element.ng-hide-add-active, - * .my-element.ng-hide-remove-active { - * /* the transition is defined in the active class */ - * transition: 1s linear all; + * transition: all 0.5s linear; * } * * .my-element.ng-hide-add { ... } @@ -27999,74 +31231,108 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate'; * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * - * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display - * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property + * to block during animation states - ngAnimate will automatically handle the style toggling for you. * * @animations - * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible - * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden + * | Animation | Occurs | + * |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------| + * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden. | + * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngShow` expression evaluates to a truthy value and just before contents are set to visible. | * * @element ANY - * @param {expression} ngShow If the {@link guide/expression expression} is truthy - * then the element is shown or hidden respectively. + * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the + * element is shown/hidden respectively. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + * A simple example, animating the element's opacity: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-simple"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/> - <div> - Show: - <div class="check-element animate-show" ng-show="checked"> - <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. - </div> + Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br /> + <div class="check-element animate-show-hide" ng-show="checked"> + I show up when your checkbox is checked. </div> - <div> - Hide: - <div class="check-element animate-show" ng-hide="checked"> - <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. - </div> - </div> - </file> - <file name="glyphicons.css"> - @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); </file> <file name="animations.css"> - .animate-show { - line-height: 20px; + .animate-show-hide.ng-hide { + opacity: 0; + } + + .animate-show-hide.ng-hide-add, + .animate-show-hide.ng-hide-remove { + transition: all linear 0.5s; + } + + .check-element { + border: 1px solid black; opacity: 1; padding: 10px; - border: 1px solid black; - background: white; } + </file> + <file name="protractor.js" type="protractor"> + it('should check ngShow', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - .animate-show.ng-hide-add, .animate-show.ng-hide-remove { - transition: all linear 0.5s; + expect(checkElem.isDisplayed()).toBe(false); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(true); + }); + </file> + </example> + * + * <hr /> + * @example + * A more complex example, featuring different show/hide animations: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-complex"> + <file name="index.html"> + Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br /> + <div class="check-element funky-show-hide" ng-show="checked"> + I show up when your checkbox is checked. + </div> + </file> + <file name="animations.css"> + body { + overflow: hidden; + perspective: 1000px; } - .animate-show.ng-hide { - line-height: 0; - opacity: 0; - padding: 0 10px; + .funky-show-hide.ng-hide-add { + transform: rotateZ(0); + transform-origin: right; + transition: all 0.5s ease-in-out; + } + + .funky-show-hide.ng-hide-add.ng-hide-add-active { + transform: rotateZ(-135deg); + } + + .funky-show-hide.ng-hide-remove { + transform: rotateY(90deg); + transform-origin: left; + transition: all 0.5s ease; + } + + .funky-show-hide.ng-hide-remove.ng-hide-remove-active { + transform: rotateY(0); } .check-element { - padding: 10px; border: 1px solid black; - background: white; + opacity: 1; + padding: 10px; } </file> <file name="protractor.js" type="protractor"> - var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); - var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); - - it('should check ng-show / ng-hide', function() { - expect(thumbsUp.isDisplayed()).toBeFalsy(); - expect(thumbsDown.isDisplayed()).toBeTruthy(); - - element(by.model('checked')).click(); + it('should check ngShow', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); + expect(checkElem.isDisplayed()).toBe(false); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(true); }); </file> </example> @@ -28096,11 +31362,13 @@ var ngShowDirective = ['$animate', function($animate) { * @multiElement * * @description - * The `ngHide` directive shows or hides the given HTML element based on the expression - * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding - * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined - * in AngularJS and sets the display style to none (using an !important flag). - * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}). + * The `ngHide` directive shows or hides the given HTML element based on the expression provided to + * the `ngHide` attribute. + * + * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element. + * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an + * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see + * {@link ng.directive:ngCsp ngCsp}). * * ```html * <!-- when $scope.myValue is truthy (element is hidden) --> @@ -28110,30 +31378,32 @@ var ngShowDirective = ['$animate', function($animate) { * <div ng-hide="myValue"></div> * ``` * - * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class - * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed - * from the element causing the element not to appear hidden. + * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added + * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide` + * CSS class is removed from the element causing the element not to appear hidden. * - * ## Why is !important used? + * ## Why is `!important` used? * - * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector - * can be easily overridden by heavier selectors. For example, something as simple - * as changing the display style on a HTML list item would make hidden elements appear visible. - * This also becomes a bigger issue when dealing with CSS frameworks. + * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the + * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as + * simple as changing the display style on a HTML list item would make hidden elements appear + * visible. This also becomes a bigger issue when dealing with CSS frameworks. * - * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector - * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the - * styling to change how to hide an element then it is just a matter of using !important in their own CSS code. + * By using `!important`, the show and hide behavior will work as expected despite any clash between + * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a + * developer chooses to override the styling to change how to hide an element then it is just a + * matter of using `!important` in their own CSS code. * * ### Overriding `.ng-hide` * - * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change - * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide` - * class in CSS: + * By default, the `.ng-hide` class will style the element with `display: none !important`. If you + * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for + * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually + * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added. * * ```css - * .ng-hide { - * /* this is just another form of hiding an element */ + * .ng-hide:not(.ng-hide-animate) { + * /* These are just alternative ways of hiding an element */ * display: block!important; * position: absolute; * top: -9999px; @@ -28141,20 +31411,20 @@ var ngShowDirective = ['$animate', function($animate) { * } * ``` * - * By default you don't need to override in CSS anything and the animations will work around the display style. + * By default you don't need to override in CSS anything and the animations will work around the + * display style. * * ## A note about animations with `ngHide` * - * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression - * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide` - * CSS class is added and removed for you instead of your own CSS class. + * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the + * directive expression is true and false. This system works like the animation system present with + * `ngClass` except that you must also include the `!important` flag to override the display + * property so that the elements are not actually hidden during the animation. * * ```css - * // - * //a working example can be found at the bottom of this page - * // + * /* A working example can be found at the bottom of this page. */ * .my-element.ng-hide-add, .my-element.ng-hide-remove { - * transition: 0.5s linear all; + * transition: all 0.5s linear; * } * * .my-element.ng-hide-add { ... } @@ -28163,71 +31433,109 @@ var ngShowDirective = ['$animate', function($animate) { * .my-element.ng-hide-remove.ng-hide-remove-active { ... } * ``` * - * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display - * property to block during animation states--ngAnimate will handle the style toggling automatically for you. + * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property + * to block during animation states - ngAnimate will automatically handle the style toggling for you. * * @animations - * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden - * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible + * | Animation | Occurs | + * |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------| + * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden. | + * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible. | + * * * @element ANY - * @param {expression} ngHide If the {@link guide/expression expression} is truthy then - * the element is shown or hidden respectively. + * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the + * element is hidden/shown respectively. * * @example - <example module="ngAnimate" deps="angular-animate.js" animations="true"> + * A simple example, animating the element's opacity: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-simple"> <file name="index.html"> - Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/> - <div> - Show: - <div class="check-element animate-hide" ng-show="checked"> - <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked. - </div> - </div> - <div> - Hide: - <div class="check-element animate-hide" ng-hide="checked"> - <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked. - </div> + Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br /> + <div class="check-element animate-show-hide" ng-hide="checked"> + I hide when your checkbox is checked. </div> </file> - <file name="glyphicons.css"> - @import url(../../components/bootstrap-3.1.1/css/bootstrap.css); - </file> <file name="animations.css"> - .animate-hide { + .animate-show-hide.ng-hide { + opacity: 0; + } + + .animate-show-hide.ng-hide-add, + .animate-show-hide.ng-hide-remove { transition: all linear 0.5s; - line-height: 20px; + } + + .check-element { + border: 1px solid black; opacity: 1; padding: 10px; - border: 1px solid black; - background: white; } + </file> + <file name="protractor.js" type="protractor"> + it('should check ngHide', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - .animate-hide.ng-hide { - line-height: 0; - opacity: 0; - padding: 0 10px; + expect(checkElem.isDisplayed()).toBe(true); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(false); + }); + </file> + </example> + * + * <hr /> + * @example + * A more complex example, featuring different show/hide animations: + * + <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-complex"> + <file name="index.html"> + Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br /> + <div class="check-element funky-show-hide" ng-hide="checked"> + I hide when your checkbox is checked. + </div> + </file> + <file name="animations.css"> + body { + overflow: hidden; + perspective: 1000px; + } + + .funky-show-hide.ng-hide-add { + transform: rotateZ(0); + transform-origin: right; + transition: all 0.5s ease-in-out; + } + + .funky-show-hide.ng-hide-add.ng-hide-add-active { + transform: rotateZ(-135deg); + } + + .funky-show-hide.ng-hide-remove { + transform: rotateY(90deg); + transform-origin: left; + transition: all 0.5s ease; + } + + .funky-show-hide.ng-hide-remove.ng-hide-remove-active { + transform: rotateY(0); } .check-element { - padding: 10px; border: 1px solid black; - background: white; + opacity: 1; + padding: 10px; } </file> <file name="protractor.js" type="protractor"> - var thumbsUp = element(by.css('span.glyphicon-thumbs-up')); - var thumbsDown = element(by.css('span.glyphicon-thumbs-down')); - - it('should check ng-show / ng-hide', function() { - expect(thumbsUp.isDisplayed()).toBeFalsy(); - expect(thumbsDown.isDisplayed()).toBeTruthy(); + it('should check ngHide', function() { + var checkbox = element(by.model('checked')); + var checkElem = element(by.css('.check-element')); - element(by.model('checked')).click(); - - expect(thumbsUp.isDisplayed()).toBeTruthy(); - expect(thumbsDown.isDisplayed()).toBeFalsy(); + expect(checkElem.isDisplayed()).toBe(true); + checkbox.click(); + expect(checkElem.isDisplayed()).toBe(false); }); </file> </example> @@ -28256,6 +31564,11 @@ var ngHideDirective = ['$animate', function($animate) { * @description * The `ngStyle` directive allows you to set CSS style on an HTML element conditionally. * + * @knownIssue + * You should not use {@link guide/interpolation interpolation} in the value of the `style` + * attribute, when using the `ngStyle` directive on the same element. + * See {@link guide/interpolation#known-issues here} for more info. + * * @element ANY * @param {expression} ngStyle * @@ -28267,7 +31580,7 @@ var ngHideDirective = ['$animate', function($animate) { * See the 'background-color' style in the example below. * * @example - <example> + <example name="ng-style"> <file name="index.html"> <input type="button" value="set color" ng-click="myStyle={color:'red'}"> <input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}"> @@ -28330,8 +31643,10 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * </div> * @animations - * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container - * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM + * | Animation | Occurs | + * |----------------------------------|-------------------------------------| + * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container | + * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM | * * @usage * @@ -28351,14 +31666,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { * * * `ngSwitchWhen`: the case statement to match against. If match then this * case will be displayed. If the same match appears multiple times, all the - * elements will be displayed. + * elements will be displayed. It is possible to associate multiple values to + * the same `ngSwitchWhen` by defining the optional attribute + * `ngSwitchWhenSeparator`. The separator will be used to split the value of + * the `ngSwitchWhen` attribute into multiple tokens, and the element will show + * if any of the `ngSwitch` evaluates to any of these tokens. * * `ngSwitchDefault`: the default case when no other case match. If there * are multiple default cases, all of them will be displayed when no other * case match. * * * @example - <example module="switchExample" deps="angular-animate.js" animations="true"> + <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch"> <file name="index.html"> <div ng-controller="ExampleController"> <select ng-model="selection" ng-options="item for item in items"> @@ -28367,7 +31686,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { <hr/> <div class="animate-switch-container" ng-switch on="selection"> - <div class="animate-switch" ng-switch-when="settings">Settings Div</div> + <div class="animate-switch" ng-switch-when="settings|options" ng-switch-when-separator="|">Settings Div</div> <div class="animate-switch" ng-switch-when="home">Home Span</div> <div class="animate-switch" ng-switch-default>default</div> </div> @@ -28376,7 +31695,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { <file name="script.js"> angular.module('switchExample', ['ngAnimate']) .controller('ExampleController', ['$scope', function($scope) { - $scope.items = ['settings', 'home', 'other']; + $scope.items = ['settings', 'home', 'options', 'other']; $scope.selection = $scope.items[0]; }]); </file> @@ -28423,19 +31742,23 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) { select.all(by.css('option')).get(1).click(); expect(switchElem.getText()).toMatch(/Home Span/); }); - it('should select default', function() { + it('should change to settings via "options"', function() { select.all(by.css('option')).get(2).click(); + expect(switchElem.getText()).toMatch(/Settings Div/); + }); + it('should select default', function() { + select.all(by.css('option')).get(3).click(); expect(switchElem.getText()).toMatch(/default/); }); </file> </example> */ -var ngSwitchDirective = ['$animate', function($animate) { +var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) { return { require: 'ngSwitch', // asks for $scope to fool the BC controller module - controller: ['$scope', function ngSwitchController() { + controller: ['$scope', function NgSwitchController() { this.cases = {}; }], link: function(scope, element, attr, ngSwitchController) { @@ -28446,21 +31769,24 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedScopes = []; var spliceFactory = function(array, index) { - return function() { array.splice(index, 1); }; + return function(response) { + if (response !== false) array.splice(index, 1); + }; }; scope.$watch(watchExpr, function ngSwitchWatchAction(value) { var i, ii; - for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) { - $animate.cancel(previousLeaveAnimations[i]); + + // Start with the last, in case the array is modified during the loop + while (previousLeaveAnimations.length) { + $animate.cancel(previousLeaveAnimations.pop()); } - previousLeaveAnimations.length = 0; for (i = 0, ii = selectedScopes.length; i < ii; ++i) { var selected = getBlockNodes(selectedElements[i].clone); selectedScopes[i].$destroy(); - var promise = previousLeaveAnimations[i] = $animate.leave(selected); - promise.then(spliceFactory(previousLeaveAnimations, i)); + var runner = previousLeaveAnimations[i] = $animate.leave(selected); + runner.done(spliceFactory(previousLeaveAnimations, i)); } selectedElements.length = 0; @@ -28471,7 +31797,7 @@ var ngSwitchDirective = ['$animate', function($animate) { selectedTransclude.transclude(function(caseElement, selectedScope) { selectedScopes.push(selectedScope); var anchor = selectedTransclude.element; - caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: '); + caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen'); var block = { clone: caseElement }; selectedElements.push(block); @@ -28490,8 +31816,16 @@ var ngSwitchWhenDirective = ngDirective({ require: '^ngSwitch', multiElement: true, link: function(scope, element, attrs, ctrl, $transclude) { - ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []); - ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element }); + + var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter( + // Filter duplicate cases + function(element, index, array) { return array[index - 1] !== element; } + ); + + forEach(cases, function(whenCase) { + ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []); + ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element }); + }); } }); @@ -28517,124 +31851,220 @@ var ngSwitchDefaultDirective = ngDirective({ * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name * as the value of the `ng-transclude` or `ng-transclude-slot` attribute. * - * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted. + * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing + * content of this element will be removed before the transcluded content is inserted. + * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback + * content in the case that no transcluded content is provided. * * @element ANY * - * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided or empty then - * the default slot is used. + * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty + * or its value is the same as the name of the attribute then the default slot is used. * * @example - * ### Default transclusion - * This example demonstrates simple transclusion. - <example name="simpleTranscludeExample" module="transcludeExample"> - <file name="index.html"> - <script> - angular.module('transcludeExample', []) - .directive('pane', function(){ - return { - restrict: 'E', - transclude: true, - scope: { title:'@' }, - template: '<div style="border: 1px solid black;">' + - '<div style="background-color: gray">{{title}}</div>' + - '<ng-transclude></ng-transclude>' + - '</div>' - }; - }) - .controller('ExampleController', ['$scope', function($scope) { - $scope.title = 'Lorem Ipsum'; - $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - }]); - </script> - <div ng-controller="ExampleController"> - <input ng-model="title" aria-label="title"> <br/> - <textarea ng-model="text" aria-label="text"></textarea> <br/> - <pane title="{{title}}">{{text}}</pane> - </div> - </file> - <file name="protractor.js" type="protractor"> - it('should have transcluded', function() { - var titleElement = element(by.model('title')); - titleElement.clear(); - titleElement.sendKeys('TITLE'); - var textElement = element(by.model('text')); - textElement.clear(); - textElement.sendKeys('TEXT'); - expect(element(by.binding('title')).getText()).toEqual('TITLE'); - expect(element(by.binding('text')).getText()).toEqual('TEXT'); - }); - </file> - </example> + * ### Basic transclusion + * This example demonstrates basic transclusion of content into a component directive. + * <example name="simpleTranscludeExample" module="transcludeExample"> + * <file name="index.html"> + * <script> + * angular.module('transcludeExample', []) + * .directive('pane', function(){ + * return { + * restrict: 'E', + * transclude: true, + * scope: { title:'@' }, + * template: '<div style="border: 1px solid black;">' + + * '<div style="background-color: gray">{{title}}</div>' + + * '<ng-transclude></ng-transclude>' + + * '</div>' + * }; + * }) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.title = 'Lorem Ipsum'; + * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + * }]); + * </script> + * <div ng-controller="ExampleController"> + * <input ng-model="title" aria-label="title"> <br/> + * <textarea ng-model="text" aria-label="text"></textarea> <br/> + * <pane title="{{title}}"><span>{{text}}</span></pane> + * </div> + * </file> + * <file name="protractor.js" type="protractor"> + * it('should have transcluded', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.binding('title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * }); + * </file> + * </example> + * + * @example + * ### Transclude fallback content + * This example shows how to use `NgTransclude` with fallback content, that + * is displayed if no transcluded content is provided. + * + * <example module="transcludeFallbackContentExample" name="ng-transclude"> + * <file name="index.html"> + * <script> + * angular.module('transcludeFallbackContentExample', []) + * .directive('myButton', function(){ + * return { + * restrict: 'E', + * transclude: true, + * scope: true, + * template: '<button style="cursor: pointer;">' + + * '<ng-transclude>' + + * '<b style="color: red;">Button1</b>' + + * '</ng-transclude>' + + * '</button>' + * }; + * }); + * </script> + * <!-- fallback button content --> + * <my-button id="fallback"></my-button> + * <!-- modified button content --> + * <my-button id="modified"> + * <i style="color: green;">Button2</i> + * </my-button> + * </file> + * <file name="protractor.js" type="protractor"> + * it('should have different transclude element content', function() { + * expect(element(by.id('fallback')).getText()).toBe('Button1'); + * expect(element(by.id('modified')).getText()).toBe('Button2'); + * }); + * </file> + * </example> * * @example * ### Multi-slot transclusion - <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample"> - <file name="index.html"> - <div ng-controller="ExampleController"> - <input ng-model="title" aria-label="title"> <br/> - <textarea ng-model="text" aria-label="text"></textarea> <br/> - <pane> - <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title> - <pane-body><p>{{text}}</p></pane-body> - </pane> - </div> - </file> - <file name="app.js"> - angular.module('multiSlotTranscludeExample', []) - .directive('pane', function(){ - return { - restrict: 'E', - transclude: { - 'paneTitle': '?title', - 'paneBody': 'body' - }, - template: '<div style="border: 1px solid black;">' + - '<div ng-transclude="title" style="background-color: gray"></div>' + - '<div ng-transclude="body"></div>' + - '</div>' - }; - }) - .controller('ExampleController', ['$scope', function($scope) { - $scope.title = 'Lorem Ipsum'; - $scope.link = "https://google.com"; - $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; - }]); - </file> - <file name="protractor.js" type="protractor"> - it('should have transcluded the title and the body', function() { - var titleElement = element(by.model('title')); - titleElement.clear(); - titleElement.sendKeys('TITLE'); - var textElement = element(by.model('text')); - textElement.clear(); - textElement.sendKeys('TEXT'); - expect(element(by.binding('title')).getText()).toEqual('TITLE'); - expect(element(by.binding('text')).getText()).toEqual('TEXT'); - }); - </file> - </example> */ + * This example demonstrates using multi-slot transclusion in a component directive. + * <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample"> + * <file name="index.html"> + * <style> + * .title, .footer { + * background-color: gray + * } + * </style> + * <div ng-controller="ExampleController"> + * <input ng-model="title" aria-label="title"> <br/> + * <textarea ng-model="text" aria-label="text"></textarea> <br/> + * <pane> + * <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title> + * <pane-body><p>{{text}}</p></pane-body> + * </pane> + * </div> + * </file> + * <file name="app.js"> + * angular.module('multiSlotTranscludeExample', []) + * .directive('pane', function() { + * return { + * restrict: 'E', + * transclude: { + * 'title': '?paneTitle', + * 'body': 'paneBody', + * 'footer': '?paneFooter' + * }, + * template: '<div style="border: 1px solid black;">' + + * '<div class="title" ng-transclude="title">Fallback Title</div>' + + * '<div ng-transclude="body"></div>' + + * '<div class="footer" ng-transclude="footer">Fallback Footer</div>' + + * '</div>' + * }; + * }) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.title = 'Lorem Ipsum'; + * $scope.link = 'https://google.com'; + * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...'; + * }]); + * </file> + * <file name="protractor.js" type="protractor"> + * it('should have transcluded the title and the body', function() { + * var titleElement = element(by.model('title')); + * titleElement.clear(); + * titleElement.sendKeys('TITLE'); + * var textElement = element(by.model('text')); + * textElement.clear(); + * textElement.sendKeys('TEXT'); + * expect(element(by.css('.title')).getText()).toEqual('TITLE'); + * expect(element(by.binding('text')).getText()).toEqual('TEXT'); + * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer'); + * }); + * </file> + * </example> + */ var ngTranscludeMinErr = minErr('ngTransclude'); -var ngTranscludeDirective = ngDirective({ - restrict: 'EAC', - link: function($scope, $element, $attrs, controller, $transclude) { +var ngTranscludeDirective = ['$compile', function($compile) { + return { + restrict: 'EAC', + terminal: true, + compile: function ngTranscludeCompile(tElement) { - function ngTranscludeCloneAttachFn(clone) { - $element.empty(); - $element.append(clone); - } + // Remove and cache any original content to act as a fallback + var fallbackLinkFn = $compile(tElement.contents()); + tElement.empty(); - if (!$transclude) { - throw ngTranscludeMinErr('orphan', - 'Illegal use of ngTransclude directive in the template! ' + - 'No parent directive that requires a transclusion found. ' + - 'Element: {0}', - startingTag($element)); - } + return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) { - $transclude(ngTranscludeCloneAttachFn, null, $attrs.ngTransclude || $attrs.ngTranscludeSlot); - } -}); + if (!$transclude) { + throw ngTranscludeMinErr('orphan', + 'Illegal use of ngTransclude directive in the template! ' + + 'No parent directive that requires a transclusion found. ' + + 'Element: {0}', + startingTag($element)); + } + + + // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default + if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) { + $attrs.ngTransclude = ''; + } + var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot; + + // If the slot is required and no transclusion content is provided then this call will throw an error + $transclude(ngTranscludeCloneAttachFn, null, slotName); + + // If the slot is optional and no transclusion content is provided then use the fallback content + if (slotName && !$transclude.isSlotFilled(slotName)) { + useFallbackContent(); + } + + function ngTranscludeCloneAttachFn(clone, transcludedScope) { + if (clone.length && notWhitespace(clone)) { + $element.append(clone); + } else { + useFallbackContent(); + // There is nothing linked against the transcluded scope since no content was available, + // so it should be safe to clean up the generated scope. + transcludedScope.$destroy(); + } + } + + function useFallbackContent() { + // Since this is the fallback content rather than the transcluded content, + // we link against the scope of this directive rather than the transcluded scope + fallbackLinkFn($scope, function(clone) { + $element.append(clone); + }); + } + + function notWhitespace(nodes) { + for (var i = 0, ii = nodes.length; i < ii; i++) { + var node = nodes[i]; + if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) { + return true; + } + } + } + }; + } + }; +}]; /** * @ngdoc directive @@ -28652,7 +32082,7 @@ var ngTranscludeDirective = ngDirective({ * @param {string} id Cache name of the template. * * @example - <example> + <example name="script-tag"> <file name="index.html"> <script type="text/ng-template" id="/tpl.html"> Content of the template. @@ -28674,7 +32104,7 @@ var scriptDirective = ['$templateCache', function($templateCache) { restrict: 'E', terminal: true, compile: function(element, attr) { - if (attr.type == 'text/ng-template') { + if (attr.type === 'text/ng-template') { var templateUrl = attr.id, text = element[0].text; @@ -28684,15 +32114,20 @@ var scriptDirective = ['$templateCache', function($templateCache) { }; }]; +/* exported selectDirective, optionDirective */ + 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; - } +function setOptionSelectedStatus(optionEl, value) { + optionEl.prop('selected', value); // needed for IE + /** + * When unselecting an option, setting the property to null / false should be enough + * However, screenreaders might react to the selected attribute instead, see + * https://github.com/angular/angular.js/issues/14419 + * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false + * or null + */ + optionEl.attr('selected', value); } /** @@ -28704,13 +32139,16 @@ function chromeHack(optionElement) { * added `<option>` elements, perhaps by an `ngRepeat` directive. */ var SelectController = - ['$element', '$scope', '$attrs', function($element, $scope, $attrs) { + ['$element', '$scope', /** @this */ function($element, $scope) { var self = this, - optionsMap = new HashMap(); + optionsMap = new NgMap(); + + self.selectValueMap = {}; // Keys are the hashed values, values the original values // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors self.ngModelCtrl = noopNgModelController; + self.multiple = false; // 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 @@ -28718,43 +32156,95 @@ var SelectController = // // We can't just jqLite('<option>') since jqLite is not smart enough // to create it in <select> and IE barfs otherwise. - self.unknownOption = jqLite(document.createElement('option')); + self.unknownOption = jqLite(window.document.createElement('option')); + + // The empty option is an option with the value '' that te application developer can + // provide inside the select. When the model changes to a value that doesn't match an option, + // it is selected - so if an empty option is provided, no unknown option is generated. + // However, the empty option is not removed when the model matches an option. It is always selectable + // and indicates that a "null" selection has been made. + self.hasEmptyOption = false; + self.emptyOption = undefined; + self.renderUnknownOption = function(val) { - var unknownVal = '? ' + hashKey(val) + ' ?'; + var unknownVal = self.generateUnknownOptionValue(val); self.unknownOption.val(unknownVal); $element.prepend(self.unknownOption); + setOptionSelectedStatus(self.unknownOption, true); $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.updateUnknownOption = function(val) { + var unknownVal = self.generateUnknownOptionValue(val); + self.unknownOption.val(unknownVal); + setOptionSelectedStatus(self.unknownOption, true); + $element.val(unknownVal); + }; + + self.generateUnknownOptionValue = function(val) { + return '? ' + hashKey(val) + ' ?'; + }; self.removeUnknownOption = function() { if (self.unknownOption.parent()) self.unknownOption.remove(); }; + self.selectEmptyOption = function() { + if (self.emptyOption) { + $element.val(''); + setOptionSelectedStatus(self.emptyOption, true); + } + }; + + self.unselectEmptyOption = function() { + if (self.hasEmptyOption) { + self.emptyOption.removeAttr('selected'); + } + }; + + $scope.$on('$destroy', function() { + // disable unknown option so that we don't do work when the whole select is being destroyed + self.renderUnknownOption = noop; + }); // 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(); + var val = $element.val(); + // ngValue added option values are stored in the selectValueMap, normal interpolations are not + var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val; + + if (self.hasOption(realVal)) { + return realVal; + } + + return null; }; // 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) { + // Make sure to remove the selected attribute from the previously selected option + // Otherwise, screen readers might get confused + var currentlySelectedOption = $element[0].options[$element[0].selectedIndex]; + if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false); + if (self.hasOption(value)) { self.removeUnknownOption(); - $element.val(value); - if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy + + var hashedVal = hashKey(value); + $element.val(hashedVal in self.selectValueMap ? hashedVal : value); + + // Set selected attribute and property on selected option for screen readers + var selectedOption = $element[0].options[$element[0].selectedIndex]; + setOptionSelectedStatus(jqLite(selectedOption), true); } else { if (value == null && self.emptyOption) { self.removeUnknownOption(); - $element.val(''); + self.selectEmptyOption(); + } else if (self.unknownOption.parent().length) { + self.updateUnknownOption(value); } else { self.renderUnknownOption(value); } @@ -28764,14 +32254,19 @@ var SelectController = // Tell the select control that an option, with the given value, has been added self.addOption = function(value, element) { + // Skip comment nodes, as they only pollute the `optionsMap` + if (element[0].nodeType === NODE_TYPE_COMMENT) return; + assertNotHasOwnProperty(value, '"option value"'); if (value === '') { + self.hasEmptyOption = true; self.emptyOption = element; } var count = optionsMap.get(value) || 0; - optionsMap.put(value, count + 1); - self.ngModelCtrl.$render(); - chromeHack(element); + optionsMap.set(value, count + 1); + // Only render at the end of a digest. This improves render performance when many options + // are added during a digest and ensures all relevant options are correctly marked as selected + scheduleRender(); }; // Tell the select control that an option, with the given value, has been removed @@ -28779,12 +32274,13 @@ var SelectController = var count = optionsMap.get(value); if (count) { if (count === 1) { - optionsMap.remove(value); + optionsMap.delete(value); if (value === '') { + self.hasEmptyOption = false; self.emptyOption = undefined; } } else { - optionsMap.put(value, count - 1); + optionsMap.set(value, count - 1); } } }; @@ -28795,35 +32291,131 @@ var SelectController = }; + var renderScheduled = false; + function scheduleRender() { + if (renderScheduled) return; + renderScheduled = true; + $scope.$$postDigest(function() { + renderScheduled = false; + self.ngModelCtrl.$render(); + }); + } + + var updateScheduled = false; + function scheduleViewValueUpdate(renderAfter) { + if (updateScheduled) return; + + updateScheduled = true; + + $scope.$$postDigest(function() { + if ($scope.$$destroyed) return; + + updateScheduled = false; + self.ngModelCtrl.$setViewValue(self.readValue()); + if (renderAfter) self.ngModelCtrl.$render(); + }); + } + + self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) { - if (interpolateValueFn) { + if (optionAttrs.$attr.ngValue) { + // The value attribute is set by ngValue + var oldVal, hashedVal = NaN; + optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { + + var removal; + var previouslySelected = optionElement.prop('selected'); + + if (isDefined(hashedVal)) { + self.removeOption(oldVal); + delete self.selectValueMap[hashedVal]; + removal = true; + } + + hashedVal = hashKey(newVal); + oldVal = newVal; + self.selectValueMap[hashedVal] = newVal; + self.addOption(newVal, optionElement); + // Set the attribute directly instead of using optionAttrs.$set - this stops the observer + // from firing a second time. Other $observers on value will also get the result of the + // ngValue expression, not the hashed value + optionElement.attr('value', hashedVal); + + if (removal && previouslySelected) { + scheduleViewValueUpdate(); + } + + }); + } else if (interpolateValueFn) { // The value attribute is interpolated - var oldVal; optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) { + // This method is overwritten in ngOptions and has side-effects! + self.readValue(); + + var removal; + var previouslySelected = optionElement.prop('selected'); + if (isDefined(oldVal)) { self.removeOption(oldVal); + removal = true; } oldVal = newVal; self.addOption(newVal, optionElement); + + if (removal && previouslySelected) { + scheduleViewValueUpdate(); + } }); } else if (interpolateTextFn) { // The text content is interpolated optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) { optionAttrs.$set('value', newVal); + var previouslySelected = optionElement.prop('selected'); if (oldVal !== newVal) { self.removeOption(oldVal); } self.addOption(newVal, optionElement); + + if (oldVal && previouslySelected) { + scheduleViewValueUpdate(); + } }); } else { // The value attribute is static self.addOption(optionAttrs.value, optionElement); } + + optionAttrs.$observe('disabled', function(newVal) { + + // Since model updates will also select disabled options (like ngOptions), + // we only have to handle options becoming disabled, not enabled + + if (newVal === 'true' || newVal && optionElement.prop('selected')) { + if (self.multiple) { + scheduleViewValueUpdate(true); + } else { + self.ngModelCtrl.$setViewValue(null); + self.ngModelCtrl.$render(); + } + } + }); + optionElement.on('$destroy', function() { - self.removeOption(optionAttrs.value); - self.ngModelCtrl.$render(); + var currentValue = self.readValue(); + var removeValue = optionAttrs.value; + + self.removeOption(removeValue); + scheduleRender(); + + if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 || + currentValue === removeValue + ) { + // When multiple (selected) options are destroyed at the same time, we don't want + // to run a model update for each of them. Instead, run a single update in the $$postDigest + scheduleViewValueUpdate(true); + } }); }; }]; @@ -28834,24 +32426,34 @@ var SelectController = * @restrict E * * @description - * HTML `SELECT` element with angular data-binding. + * HTML `select` element with angular data-binding. * * 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). - * Ìt also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or + * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or * {@link ngOptions `ngOptions`} directives. * * When an item in the `<select>` menu is selected, the 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. + * Value and textContent can be interpolated. * - * <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> + * ## Matching model and option values + * + * In general, the match between the model and an option is evaluated by strictly comparing the model + * value against the value of the available options. + * + * If you are setting the option value with the option's `value` attribute, or textContent, the + * value will always be a `string` which means that the model value must also be a string. + * Otherwise the `select` directive cannot match them correctly. + * + * To bind the model to a non-string value, you can use one of the following strategies: + * - the {@link ng.ngOptions `ngOptions`} directive + * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value}) + * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be + * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example}) + * - model $parsers / $formatters to convert the string value + * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example}) * * 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. @@ -28860,13 +32462,17 @@ var SelectController = * be nested into the `<select>` element. This element will then represent the `null` or "not selected" * option. See example below for demonstration. * - * <div class="alert alert-info"> + * ## Choosing between `ngRepeat` and `ngOptions` + * * 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> + * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits: + * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the + * comprehension expression + * - reduced memory consumption by not creating a new scope for each repeated instance + * - increased render speed by creating the options in a documentFragment instead of individually + * + * Specifically, select with repeated options slows down significantly starting at 2000 options in + * Chrome and Internet Explorer / Edge. * * * @param {string} ngModel Assignable angular expression to data-bind to. @@ -28881,6 +32487,8 @@ var SelectController = * 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`}. + * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the + * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive. * * @example * ### Simple `select` elements with static options @@ -28921,7 +32529,7 @@ var SelectController = * $scope.data = { * singleSelect: null, * multipleSelect: [], - * option1: 'option-1', + * option1: 'option-1' * }; * * $scope.forceUnknownOption = function() { @@ -28932,34 +32540,65 @@ var SelectController = *</example> * * ### Using `ngRepeat` to generate `select` options - * <example name="ngrepeat-select" module="ngrepeatSelect"> + * <example name="select-ngrepeat" 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"> + * <select name="repeatSelect" id="repeatSelect" ng-model="data.model"> * <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option> * </select> * </form> * <hr> - * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/> + * <tt>model = {{data.model}}</tt><br/> * </div> * </file> * <file name="app.js"> * angular.module('ngrepeatSelect', []) * .controller('ExampleController', ['$scope', function($scope) { * $scope.data = { - * repeatSelect: null, + * model: null, * availableOptions: [ * {id: '1', name: 'Option A'}, * {id: '2', name: 'Option B'}, * {id: '3', name: 'Option C'} - * ], + * ] * }; * }]); * </file> *</example> * + * ### Using `ngValue` to bind the model to an array of objects + * <example name="select-ngvalue" module="ngvalueSelect"> + * <file name="index.html"> + * <div ng-controller="ExampleController"> + * <form name="myForm"> + * <label for="ngvalueselect"> ngvalue select: </label> + * <select size="6" name="ngvalueselect" ng-model="data.model" multiple> + * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option> + * </select> + * </form> + * <hr> + * <pre>model = {{data.model | json}}</pre><br/> + * </div> + * </file> + * <file name="app.js"> + * angular.module('ngvalueSelect', []) + * .controller('ExampleController', ['$scope', function($scope) { + * $scope.data = { + * model: null, + * availableOptions: [ + {value: 'myString', name: 'string'}, + {value: 1, name: 'integer'}, + {value: true, name: 'boolean'}, + {value: null, name: 'null'}, + {value: {prop: 'value'}, name: 'object'}, + {value: ['a'], name: 'array'} + * ] + * }; + * }]); + * </file> + *</example> * * ### Using `select` with `ngOptions` and setting a default value * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples. @@ -29025,7 +32664,6 @@ var SelectController = * </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> @@ -29040,31 +32678,31 @@ var selectDirective = function() { controller: SelectController, priority: 1, link: { - pre: selectPreLink + pre: selectPreLink, + post: selectPostLink } }; function selectPreLink(scope, element, attr, ctrls) { - // if ngModel is not defined, we don't need to do anything + var selectCtrl = ctrls[0]; var ngModelCtrl = ctrls[1]; - if (!ngModelCtrl) return; - var selectCtrl = ctrls[0]; + // if ngModel is not defined, we don't need to do anything but set the registerOption + // function to noop, so options don't get added internally + if (!ngModelCtrl) { + selectCtrl.registerOption = noop; + return; + } - selectCtrl.ngModelCtrl = ngModelCtrl; - // We delegate rendering to the `writeValue` method, which can be changed - // if the select can have multiple selected values or if the options are being - // generated by `ngOptions` - ngModelCtrl.$render = function() { - selectCtrl.writeValue(ngModelCtrl.$viewValue); - }; + selectCtrl.ngModelCtrl = ngModelCtrl; // 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() { + selectCtrl.removeUnknownOption(); scope.$apply(function() { ngModelCtrl.$setViewValue(selectCtrl.readValue()); }); @@ -29075,13 +32713,15 @@ var selectDirective = function() { // 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) { + selectCtrl.multiple = true; // 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); + if (option.selected && !option.disabled) { + var val = option.value; + array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val); } }); return array; @@ -29089,9 +32729,21 @@ var selectDirective = function() { // 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)); + var shouldBeSelected = !!value && (includes(value, option.value) || + includes(value, selectCtrl.selectValueMap[option.value])); + var currentlySelected = option.selected; + + // IE and Edge, adding options to the selection via shift+click/UP/DOWN, + // will de-select already selected options if "selected" on those options was set + // more than once (i.e. when the options were already selected) + // So we only modify the selected property if neccessary. + // Note: this behavior cannot be replicated via unit tests because it only shows in the + // actual user interface. + if (shouldBeSelected !== currentlySelected) { + setOptionSelectedStatus(jqLite(option), shouldBeSelected); + } + }); }; @@ -29114,6 +32766,23 @@ var selectDirective = function() { } } + + function selectPostLink(scope, element, attrs, ctrls) { + // if ngModel is not defined, we don't need to do anything + var ngModelCtrl = ctrls[1]; + if (!ngModelCtrl) return; + + var selectCtrl = ctrls[0]; + + // We delegate rendering to the `writeValue` method, which can be changed + // if the select can have multiple selected values or if the options are being + // generated by `ngOptions`. + // This must be done in the postLink fn to prevent $render to be called before + // all nodes have been linked correctly. + ngModelCtrl.$render = function() { + selectCtrl.writeValue(ngModelCtrl.$viewValue); + }; + } }; @@ -29125,21 +32794,23 @@ var optionDirective = ['$interpolate', function($interpolate) { restrict: 'E', priority: 100, compile: function(element, attr) { + var interpolateValueFn, interpolateTextFn; - if (isDefined(attr.value)) { + if (isDefined(attr.ngValue)) { + // Will be handled by registerOption + } else if (isDefined(attr.value)) { // If the value attribute is defined, check if it contains an interpolation - var interpolateValueFn = $interpolate(attr.value, true); + 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); + 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', @@ -29155,11 +32826,65 @@ var optionDirective = ['$interpolate', function($interpolate) { }; }]; -var styleDirective = valueFn({ - restrict: 'E', - terminal: false -}); +/** + * @ngdoc directive + * @name ngRequired + * @restrict A + * + * @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', @@ -29179,7 +32904,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', @@ -29211,7 +33010,72 @@ var patternDirective = function() { }; }; +/** + * @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', @@ -29222,7 +33086,7 @@ var maxlengthDirective = function() { var maxlength = -1; attr.$observe('maxlength', function(value) { var intVal = toInt(value); - maxlength = isNaN(intVal) ? -1 : intVal; + maxlength = isNumberNaN(intVal) ? -1 : intVal; ctrl.$validate(); }); ctrl.$validators.maxlength = function(modelValue, viewValue) { @@ -29232,6 +33096,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', @@ -29252,13 +33180,15 @@ var minlengthDirective = function() { }; if (window.angular.bootstrap) { - //AngularJS is already loaded, so we can return here... - console.log('WARNING: Tried to load angular more than once.'); + // AngularJS is already loaded, so we can return here... + if (window.console) { + console.log('WARNING: Tried to load angular more than once.'); + } return; } -//try to bind to jquery now so that one can write jqLite(document).ready() -//but we will rebind on bootstrap again. +// try to bind to jquery now so that one can write jqLite(fn) +// but we will rebind on bootstrap again. bindJQuery(); publishExternalAPI(angular); @@ -29344,6 +33274,20 @@ $provide.value("$locale", { "Nov", "Dec" ], + "STANDALONEMONTH": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ], "WEEKENDRANGE": [ 5, 6 @@ -29387,14 +33331,15 @@ $provide.value("$locale", { ] }, "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); + jqLite(function() { + angularInit(window.document, bootstrap); }); -})(window, document); +})(window); !window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>');
\ No newline at end of file |