Last active
December 25, 2015 18:49
-
-
Save tatey/7023659 to your computer and use it in GitHub Desktop.
Experimenting with $provide.copy. See http://stackoverflow.com/questions/19353515/overriding-dependency-at-runtime-in-angularjs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @license AngularJS v1.2.0-rc.3 | |
* (c) 2010-2012 Google, Inc. http://angularjs.org | |
* License: MIT | |
*/ | |
(function(window, document, undefined) {'use strict'; | |
/** | |
* @description | |
* | |
* This object provides a utility for producing rich Error messages within | |
* Angular. It can be called as follows: | |
* | |
* var exampleMinErr = minErr('example'); | |
* throw exampleMinErr('one', 'This {0} is {1}', foo, bar); | |
* | |
* The above creates an instance of minErr in the example namespace. The | |
* resulting error will have a namespaced error code of example.one. The | |
* resulting error will replace {0} with the value of foo, and {1} with the | |
* value of bar. The object is not restricted in the number of arguments it can | |
* take. | |
* | |
* If fewer arguments are specified than necessary for interpolation, the extra | |
* interpolation markers will be preserved in the final string. | |
* | |
* Since data will be parsed statically during a build step, some restrictions | |
* are applied with respect to how minErr instances are created and called. | |
* Instances should have names of the form namespaceMinErr for a minErr created | |
* using minErr('namespace') . Error codes, namespaces and template strings | |
* should all be static strings, not variables or general expressions. | |
* | |
* @param {string} module The namespace to use for the new minErr instance. | |
* @returns {function(string, string, ...): Error} instance | |
*/ | |
function minErr(module) { | |
return function () { | |
var code = arguments[0], | |
prefix = '[' + (module ? module + ':' : '') + code + '] ', | |
template = arguments[1], | |
templateArgs = arguments, | |
stringify = function (obj) { | |
if (isFunction(obj)) { | |
return obj.toString().replace(/ \{[\s\S]*$/, ''); | |
} else if (isUndefined(obj)) { | |
return 'undefined'; | |
} else if (!isString(obj)) { | |
return JSON.stringify(obj); | |
} | |
return obj; | |
}, | |
message, i; | |
message = prefix + template.replace(/\{\d+\}/g, function (match) { | |
var index = +match.slice(1, -1), arg; | |
if (index + 2 < templateArgs.length) { | |
arg = templateArgs[index + 2]; | |
if (isFunction(arg)) { | |
return arg.toString().replace(/ ?\{[\s\S]*$/, ''); | |
} else if (isUndefined(arg)) { | |
return 'undefined'; | |
} else if (!isString(arg)) { | |
return toJson(arg); | |
} | |
return arg; | |
} | |
return match; | |
}); | |
message = message + '\nhttp://errors.angularjs.org/' + version.full + '/' + | |
(module ? module + '/' : '') + code; | |
for (i = 2; i < arguments.length; i++) { | |
message = message + (i == 2 ? '?' : '&') + 'p' + (i-2) + '=' + | |
encodeURIComponent(stringify(arguments[i])); | |
} | |
return new Error(message); | |
}; | |
} | |
//////////////////////////////////// | |
/** | |
* @ngdoc function | |
* @name angular.lowercase | |
* @function | |
* | |
* @description Converts the specified string to lowercase. | |
* @param {string} string String to be converted to lowercase. | |
* @returns {string} Lowercased string. | |
*/ | |
var lowercase = function(string){return isString(string) ? string.toLowerCase() : string;}; | |
/** | |
* @ngdoc function | |
* @name angular.uppercase | |
* @function | |
* | |
* @description Converts the specified string to uppercase. | |
* @param {string} string String to be converted to uppercase. | |
* @returns {string} Uppercased string. | |
*/ | |
var uppercase = function(string){return isString(string) ? string.toUpperCase() : string;}; | |
var manualLowercase = function(s) { | |
return isString(s) | |
? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);}) | |
: s; | |
}; | |
var manualUppercase = function(s) { | |
return isString(s) | |
? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);}) | |
: s; | |
}; | |
// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish | |
// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods | |
// with correct but slower alternatives. | |
if ('i' !== 'I'.toLowerCase()) { | |
lowercase = manualLowercase; | |
uppercase = manualUppercase; | |
} | |
var /** holds major version number for IE or NaN for real browsers */ | |
msie, | |
jqLite, // delay binding since jQuery could be loaded after us. | |
jQuery, // delay binding | |
slice = [].slice, | |
push = [].push, | |
toString = Object.prototype.toString, | |
ngMinErr = minErr('ng'), | |
_angular = window.angular, | |
/** @name angular */ | |
angular = window.angular || (window.angular = {}), | |
angularModule, | |
nodeName_, | |
uid = ['0', '0', '0']; | |
/** | |
* IE 11 changed the format of the UserAgent string. | |
* See http://msdn.microsoft.com/en-us/library/ms537503.aspx | |
*/ | |
msie = int((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); | |
if (isNaN(msie)) { | |
msie = int((/trident\/.*; rv:(\d+)/.exec(lowercase(navigator.userAgent)) || [])[1]); | |
} | |
/** | |
* @private | |
* @param {*} obj | |
* @return {boolean} Returns true if `obj` is an array or array-like object (NodeList, Arguments, String ...) | |
*/ | |
function isArrayLike(obj) { | |
if (obj == null || isWindow(obj)) { | |
return false; | |
} | |
var length = obj.length; | |
if (obj.nodeType === 1 && length) { | |
return true; | |
} | |
return isString(obj) || isArray(obj) || length === 0 || | |
typeof length === 'number' && length > 0 && (length - 1) in obj; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.forEach | |
* @function | |
* | |
* @description | |
* Invokes the `iterator` function once for each item in `obj` collection, which can be either an | |
* object or an array. The `iterator` function is invoked with `iterator(value, key)`, where `value` | |
* is the value of an object property or an array element and `key` is the object property key or | |
* array element index. Specifying a `context` for the function is optional. | |
* | |
* Note: this function was previously known as `angular.foreach`. | |
* | |
<pre> | |
var values = {name: 'misko', gender: 'male'}; | |
var log = []; | |
angular.forEach(values, function(value, key){ | |
this.push(key + ': ' + value); | |
}, log); | |
expect(log).toEqual(['name: misko', 'gender:male']); | |
</pre> | |
* | |
* @param {Object|Array} obj Object to iterate over. | |
* @param {Function} iterator Iterator function. | |
* @param {Object=} context Object to become context (`this`) for the iterator function. | |
* @returns {Object|Array} Reference to `obj`. | |
*/ | |
function forEach(obj, iterator, context) { | |
var key; | |
if (obj) { | |
if (isFunction(obj)){ | |
for (key in obj) { | |
if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { | |
iterator.call(context, obj[key], key); | |
} | |
} | |
} else if (obj.forEach && obj.forEach !== forEach) { | |
obj.forEach(iterator, context); | |
} else if (isArrayLike(obj)) { | |
for (key = 0; key < obj.length; key++) | |
iterator.call(context, obj[key], key); | |
} else { | |
for (key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
iterator.call(context, obj[key], key); | |
} | |
} | |
} | |
} | |
return obj; | |
} | |
function sortedKeys(obj) { | |
var keys = []; | |
for (var key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
keys.push(key); | |
} | |
} | |
return keys.sort(); | |
} | |
function forEachSorted(obj, iterator, context) { | |
var keys = sortedKeys(obj); | |
for ( var i = 0; i < keys.length; i++) { | |
iterator.call(context, obj[keys[i]], keys[i]); | |
} | |
return keys; | |
} | |
/** | |
* when using forEach the params are value, key, but it is often useful to have key, value. | |
* @param {function(string, *)} iteratorFn | |
* @returns {function(*, string)} | |
*/ | |
function reverseParams(iteratorFn) { | |
return function(value, key) { iteratorFn(key, value) }; | |
} | |
/** | |
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric | |
* characters such as '012ABC'. The reason why we are not using simply a number counter is that | |
* the number string gets longer over time, and it can also overflow, where as the nextId | |
* will grow much slower, it is a string, and it will never overflow. | |
* | |
* @returns an unique alpha-numeric string | |
*/ | |
function nextUid() { | |
var index = uid.length; | |
var digit; | |
while(index) { | |
index--; | |
digit = uid[index].charCodeAt(0); | |
if (digit == 57 /*'9'*/) { | |
uid[index] = 'A'; | |
return uid.join(''); | |
} | |
if (digit == 90 /*'Z'*/) { | |
uid[index] = '0'; | |
} else { | |
uid[index] = String.fromCharCode(digit + 1); | |
return uid.join(''); | |
} | |
} | |
uid.unshift('0'); | |
return uid.join(''); | |
} | |
/** | |
* Set or clear the hashkey for an object. | |
* @param obj object | |
* @param h the hashkey (!truthy to delete the hashkey) | |
*/ | |
function setHashKey(obj, h) { | |
if (h) { | |
obj.$$hashKey = h; | |
} | |
else { | |
delete obj.$$hashKey; | |
} | |
} | |
/** | |
* @ngdoc function | |
* @name angular.extend | |
* @function | |
* | |
* @description | |
* Extends the destination object `dst` by copying all of the properties from the `src` object(s) | |
* to `dst`. You can specify multiple `src` objects. | |
* | |
* @param {Object} dst Destination object. | |
* @param {...Object} src Source object(s). | |
* @returns {Object} Reference to `dst`. | |
*/ | |
function extend(dst) { | |
var h = dst.$$hashKey; | |
forEach(arguments, function(obj){ | |
if (obj !== dst) { | |
forEach(obj, function(value, key){ | |
dst[key] = value; | |
}); | |
} | |
}); | |
setHashKey(dst,h); | |
return dst; | |
} | |
function int(str) { | |
return parseInt(str, 10); | |
} | |
function inherit(parent, extra) { | |
return extend(new (extend(function() {}, {prototype:parent}))(), extra); | |
} | |
/** | |
* @ngdoc function | |
* @name angular.noop | |
* @function | |
* | |
* @description | |
* A function that performs no operations. This function can be useful when writing code in the | |
* functional style. | |
<pre> | |
function foo(callback) { | |
var result = calculateResult(); | |
(callback || angular.noop)(result); | |
} | |
</pre> | |
*/ | |
function noop() {} | |
noop.$inject = []; | |
/** | |
* @ngdoc function | |
* @name angular.identity | |
* @function | |
* | |
* @description | |
* A function that returns its first argument. This function is useful when writing code in the | |
* functional style. | |
* | |
<pre> | |
function transformer(transformationFn, value) { | |
return (transformationFn || angular.identity)(value); | |
}; | |
</pre> | |
*/ | |
function identity($) {return $;} | |
identity.$inject = []; | |
function valueFn(value) {return function() {return value;};} | |
/** | |
* @ngdoc function | |
* @name angular.isUndefined | |
* @function | |
* | |
* @description | |
* Determines if a reference is undefined. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is undefined. | |
*/ | |
function isUndefined(value){return typeof value == 'undefined';} | |
/** | |
* @ngdoc function | |
* @name angular.isDefined | |
* @function | |
* | |
* @description | |
* Determines if a reference is defined. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is defined. | |
*/ | |
function isDefined(value){return typeof value != 'undefined';} | |
/** | |
* @ngdoc function | |
* @name angular.isObject | |
* @function | |
* | |
* @description | |
* Determines if a reference is an `Object`. Unlike `typeof` in JavaScript, `null`s are not | |
* considered to be objects. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is an `Object` but not `null`. | |
*/ | |
function isObject(value){return value != null && typeof value == 'object';} | |
/** | |
* @ngdoc function | |
* @name angular.isString | |
* @function | |
* | |
* @description | |
* Determines if a reference is a `String`. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a `String`. | |
*/ | |
function isString(value){return typeof value == 'string';} | |
/** | |
* @ngdoc function | |
* @name angular.isNumber | |
* @function | |
* | |
* @description | |
* Determines if a reference is a `Number`. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a `Number`. | |
*/ | |
function isNumber(value){return typeof value == 'number';} | |
/** | |
* @ngdoc function | |
* @name angular.isDate | |
* @function | |
* | |
* @description | |
* Determines if a value is a date. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a `Date`. | |
*/ | |
function isDate(value){ | |
return toString.apply(value) == '[object Date]'; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.isArray | |
* @function | |
* | |
* @description | |
* Determines if a reference is an `Array`. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is an `Array`. | |
*/ | |
function isArray(value) { | |
return toString.apply(value) == '[object Array]'; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.isFunction | |
* @function | |
* | |
* @description | |
* Determines if a reference is a `Function`. | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a `Function`. | |
*/ | |
function isFunction(value){return typeof value == 'function';} | |
/** | |
* Determines if a value is a regular expression object. | |
* | |
* @private | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a `RegExp`. | |
*/ | |
function isRegExp(value) { | |
return toString.apply(value) == '[object RegExp]'; | |
} | |
/** | |
* Checks if `obj` is a window object. | |
* | |
* @private | |
* @param {*} obj Object to check | |
* @returns {boolean} True if `obj` is a window obj. | |
*/ | |
function isWindow(obj) { | |
return obj && obj.document && obj.location && obj.alert && obj.setInterval; | |
} | |
function isScope(obj) { | |
return obj && obj.$evalAsync && obj.$watch; | |
} | |
function isFile(obj) { | |
return toString.apply(obj) === '[object File]'; | |
} | |
function isBoolean(value) { | |
return typeof value == 'boolean'; | |
} | |
var trim = (function() { | |
// native trim is way faster: http://jsperf.com/angular-trim-test | |
// but IE doesn't have it... :-( | |
// TODO: we should move this into IE/ES5 polyfill | |
if (!String.prototype.trim) { | |
return function(value) { | |
return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; | |
}; | |
} | |
return function(value) { | |
return isString(value) ? value.trim() : value; | |
}; | |
})(); | |
/** | |
* @ngdoc function | |
* @name angular.isElement | |
* @function | |
* | |
* @description | |
* Determines if a reference is a DOM element (or wrapped jQuery element). | |
* | |
* @param {*} value Reference to check. | |
* @returns {boolean} True if `value` is a DOM element (or wrapped jQuery element). | |
*/ | |
function isElement(node) { | |
return node && | |
(node.nodeName // we are a direct element | |
|| (node.on && node.find)); // we have an on and find method part of jQuery API | |
} | |
/** | |
* @param str 'key1,key2,...' | |
* @returns {object} in the form of {key1:true, key2:true, ...} | |
*/ | |
function makeMap(str){ | |
var obj = {}, items = str.split(","), i; | |
for ( i = 0; i < items.length; i++ ) | |
obj[ items[i] ] = true; | |
return obj; | |
} | |
if (msie < 9) { | |
nodeName_ = function(element) { | |
element = element.nodeName ? element : element[0]; | |
return (element.scopeName && element.scopeName != 'HTML') | |
? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; | |
}; | |
} else { | |
nodeName_ = function(element) { | |
return element.nodeName ? element.nodeName : element[0].nodeName; | |
}; | |
} | |
function map(obj, iterator, context) { | |
var results = []; | |
forEach(obj, function(value, index, list) { | |
results.push(iterator.call(context, value, index, list)); | |
}); | |
return results; | |
} | |
/** | |
* @description | |
* Determines the number of elements in an array, the number of properties an object has, or | |
* the length of a string. | |
* | |
* Note: This function is used to augment the Object type in Angular expressions. See | |
* {@link angular.Object} for more information about Angular arrays. | |
* | |
* @param {Object|Array|string} obj Object, array, or string to inspect. | |
* @param {boolean} [ownPropsOnly=false] Count only "own" properties in an object | |
* @returns {number} The size of `obj` or `0` if `obj` is neither an object nor an array. | |
*/ | |
function size(obj, ownPropsOnly) { | |
var size = 0, key; | |
if (isArray(obj) || isString(obj)) { | |
return obj.length; | |
} else if (isObject(obj)){ | |
for (key in obj) | |
if (!ownPropsOnly || obj.hasOwnProperty(key)) | |
size++; | |
} | |
return size; | |
} | |
function includes(array, obj) { | |
return indexOf(array, obj) != -1; | |
} | |
function indexOf(array, obj) { | |
if (array.indexOf) return array.indexOf(obj); | |
for ( var i = 0; i < array.length; i++) { | |
if (obj === array[i]) return i; | |
} | |
return -1; | |
} | |
function arrayRemove(array, value) { | |
var index = indexOf(array, value); | |
if (index >=0) | |
array.splice(index, 1); | |
return value; | |
} | |
function isLeafNode (node) { | |
if (node) { | |
switch (node.nodeName) { | |
case "OPTION": | |
case "PRE": | |
case "TITLE": | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.copy | |
* @function | |
* | |
* @description | |
* Creates a deep copy of `source`, which should be an object or an array. | |
* | |
* * If no destination is supplied, a copy of the object or array is created. | |
* * If a destination is provided, all of its elements (for array) 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. | |
* | |
* Note: this function is used to augment the Object type in Angular expressions. See | |
* {@link ng.$filter} for more information about Angular arrays. | |
* | |
* @param {*} source The source that will be used to make a copy. | |
* Can be any type, including primitives, `null`, and `undefined`. | |
* @param {(Object|Array)=} destination Destination into which the source is copied. If | |
* provided, must be of the same type as `source`. | |
* @returns {*} The copy or updated `destination`, if `destination` was specified. | |
* | |
* @example | |
<doc:example> | |
<doc:source> | |
<div ng-controller="Controller"> | |
<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> | |
function Controller($scope) { | |
$scope.master= {}; | |
$scope.update = function(user) { | |
// Example with 1 argument | |
$scope.master= angular.copy(user); | |
}; | |
$scope.reset = function() { | |
// Example with 2 arguments | |
angular.copy($scope.master, $scope.user); | |
}; | |
$scope.reset(); | |
} | |
</script> | |
</doc:source> | |
</doc:example> | |
*/ | |
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."); | |
} | |
if (!destination) { | |
destination = source; | |
if (source) { | |
if (isArray(source)) { | |
destination = copy(source, []); | |
} else if (isDate(source)) { | |
destination = new Date(source.getTime()); | |
} else if (isRegExp(source)) { | |
destination = new RegExp(source.source); | |
} else if (isObject(source)) { | |
destination = copy(source, {}); | |
} | |
} | |
} else { | |
if (source === destination) throw ngMinErr('cpi', "Can't copy! Source and destination are identical."); | |
if (isArray(source)) { | |
destination.length = 0; | |
for ( var i = 0; i < source.length; i++) { | |
destination.push(copy(source[i])); | |
} | |
} else { | |
var h = destination.$$hashKey; | |
forEach(destination, function(value, key){ | |
delete destination[key]; | |
}); | |
for ( var key in source) { | |
destination[key] = copy(source[key]); | |
} | |
setHashKey(destination,h); | |
} | |
} | |
return destination; | |
} | |
/** | |
* Create a shallow copy of an object | |
*/ | |
function shallowCopy(src, dst) { | |
dst = dst || {}; | |
for(var key in src) { | |
// shallowCopy is only ever called by $compile nodeLinkFn, which has control over src | |
// so we don't need to worry hasOwnProperty here | |
if (src.hasOwnProperty(key) && key.substr(0, 2) !== '$$') { | |
dst[key] = src[key]; | |
} | |
} | |
return dst; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.equals | |
* @function | |
* | |
* @description | |
* Determines if two objects or two values are equivalent. Supports value types, regular expressions, arrays and | |
* objects. | |
* | |
* Two objects or values are considered equivalent if at least one of the following is true: | |
* | |
* * Both objects or values pass `===` comparison. | |
* * Both objects or values are of the same type and all of their properties pass `===` comparison. | |
* * Both values are NaN. (In JavaScript, NaN == NaN => false. But we consider two NaN as equal) | |
* * Both values represent the same regular expression (In JavasScript, | |
* /abc/ == /abc/ => false. But we consider two regular expressions as equal when their textual | |
* representation matches). | |
* | |
* During a property comparison, properties of `function` type and properties with names | |
* that begin with `$` are ignored. | |
* | |
* Scope and DOMWindow objects are being compared only by identify (`===`). | |
* | |
* @param {*} o1 Object or value to compare. | |
* @param {*} o2 Object or value to compare. | |
* @returns {boolean} True if arguments are equal. | |
*/ | |
function equals(o1, o2) { | |
if (o1 === o2) return true; | |
if (o1 === null || o2 === null) return false; | |
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN | |
var t1 = typeof o1, t2 = typeof o2, length, key, keySet; | |
if (t1 == t2) { | |
if (t1 == 'object') { | |
if (isArray(o1)) { | |
if (!isArray(o2)) return false; | |
if ((length = o1.length) == o2.length) { | |
for(key=0; key<length; key++) { | |
if (!equals(o1[key], o2[key])) return false; | |
} | |
return true; | |
} | |
} else if (isDate(o1)) { | |
return isDate(o2) && o1.getTime() == o2.getTime(); | |
} else if (isRegExp(o1) && isRegExp(o2)) { | |
return o1.toString() == o2.toString(); | |
} else { | |
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) || isArray(o2)) return false; | |
keySet = {}; | |
for(key in o1) { | |
if (key.charAt(0) === '$' || isFunction(o1[key])) continue; | |
if (!equals(o1[key], o2[key])) return false; | |
keySet[key] = true; | |
} | |
for(key in o2) { | |
if (!keySet.hasOwnProperty(key) && | |
key.charAt(0) !== '$' && | |
o2[key] !== undefined && | |
!isFunction(o2[key])) return false; | |
} | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
function concat(array1, array2, index) { | |
return array1.concat(slice.call(array2, index)); | |
} | |
function sliceArgs(args, startIndex) { | |
return slice.call(args, startIndex || 0); | |
} | |
/** | |
* @ngdoc function | |
* @name angular.bind | |
* @function | |
* | |
* @description | |
* Returns a function which calls function `fn` bound to `self` (`self` becomes the `this` for | |
* `fn`). You can supply optional `args` that are prebound to the function. This feature is also | |
* known as [partial application](http://en.wikipedia.org/wiki/Partial_application), as distinguished | |
* from [function currying](http://en.wikipedia.org/wiki/Currying#Contrast_with_partial_function_application). | |
* | |
* @param {Object} self Context which `fn` should be evaluated in. | |
* @param {function()} fn Function to be bound. | |
* @param {...*} args Optional arguments to be prebound to the `fn` function call. | |
* @returns {function()} Function that wraps the `fn` with all the specified bindings. | |
*/ | |
function bind(self, fn) { | |
var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : []; | |
if (isFunction(fn) && !(fn instanceof RegExp)) { | |
return curryArgs.length | |
? function() { | |
return arguments.length | |
? fn.apply(self, curryArgs.concat(slice.call(arguments, 0))) | |
: fn.apply(self, curryArgs); | |
} | |
: function() { | |
return arguments.length | |
? fn.apply(self, arguments) | |
: fn.call(self); | |
}; | |
} else { | |
// in IE, native methods are not functions so they cannot be bound (note: they don't need to be) | |
return fn; | |
} | |
} | |
function toJsonReplacer(key, value) { | |
var val = value; | |
if (typeof key === 'string' && key.charAt(0) === '$') { | |
val = undefined; | |
} else if (isWindow(value)) { | |
val = '$WINDOW'; | |
} else if (value && document === value) { | |
val = '$DOCUMENT'; | |
} else if (isScope(value)) { | |
val = '$SCOPE'; | |
} | |
return val; | |
} | |
/** | |
* @ngdoc function | |
* @name angular.toJson | |
* @function | |
* | |
* @description | |
* 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 {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. | |
* @returns {string|undefined} JSON-ified string representing `obj`. | |
*/ | |
function toJson(obj, pretty) { | |
if (typeof obj === 'undefined') return undefined; | |
return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); | |
} | |
/** | |
* @ngdoc function | |
* @name angular.fromJson | |
* @function | |
* | |
* @description | |
* Deserializes a JSON string. | |
* | |
* @param {string} json JSON string to deserialize. | |
* @returns {Object|Array|Date|string|number} Deserialized thingy. | |
*/ | |
function fromJson(json) { | |
return isString(json) | |
? JSON.parse(json) | |
: json; | |
} | |
function toBoolean(value) { | |
if (value && value.length !== 0) { | |
var v = lowercase("" + value); | |
value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); | |
} else { | |
value = false; | |
} | |
return value; | |
} | |
/** | |
* @returns {string} Returns the string representation of the element. | |
*/ | |
function startingTag(element) { | |
element = jqLite(element).clone(); | |
try { | |
// turns out IE does not let you set .html() on elements which | |
// are not allowed to have children. So we just ignore it. | |
element.html(''); | |
} catch(e) {} | |
// As Per DOM Standards | |
var TEXT_NODE = 3; | |
var elemHtml = jqLite('<div>').append(element).html(); | |
try { | |
return element[0].nodeType === TEXT_NODE ? lowercase(elemHtml) : | |
elemHtml. | |
match(/^(<[^>]+>)/)[1]. | |
replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); }); | |
} catch(e) { | |
return lowercase(elemHtml); | |
} | |
} | |
///////////////////////////////////////////////// | |
/** | |
* Tries to decode the URI component without throwing an exception. | |
* | |
* @private | |
* @param str value potential URI component to check. | |
* @returns {boolean} True if `value` can be decoded | |
* with the decodeURIComponent function. | |
*/ | |
function tryDecodeURIComponent(value) { | |
try { | |
return decodeURIComponent(value); | |
} catch(e) { | |
// Ignore any invalid uri component | |
} | |
} | |
/** | |
* Parses an escaped url query string into key-value pairs. | |
* @returns Object.<(string|boolean)> | |
*/ | |
function parseKeyValue(/**string*/keyValue) { | |
var obj = {}, key_value, key; | |
forEach((keyValue || "").split('&'), function(keyValue){ | |
if ( keyValue ) { | |
key_value = keyValue.split('='); | |
key = tryDecodeURIComponent(key_value[0]); | |
if ( isDefined(key) ) { | |
var val = isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true; | |
if (!obj[key]) { | |
obj[key] = val; | |
} else if(isArray(obj[key])) { | |
obj[key].push(val); | |
} else { | |
obj[key] = [obj[key],val]; | |
} | |
} | |
} | |
}); | |
return obj; | |
} | |
function toKeyValue(obj) { | |
var parts = []; | |
forEach(obj, function(value, key) { | |
if (isArray(value)) { | |
forEach(value, function(arrayValue) { | |
parts.push(encodeUriQuery(key, true) + (arrayValue === true ? '' : '=' + encodeUriQuery(arrayValue, true))); | |
}); | |
} else { | |
parts.push(encodeUriQuery(key, true) + (value === true ? '' : '=' + encodeUriQuery(value, true))); | |
} | |
}); | |
return parts.length ? parts.join('&') : ''; | |
} | |
/** | |
* We need our custom method because encodeURIComponent is too aggressive and doesn't follow | |
* http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path | |
* segments: | |
* segment = *pchar | |
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | |
* pct-encoded = "%" HEXDIG HEXDIG | |
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" | |
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" | |
* / "*" / "+" / "," / ";" / "=" | |
*/ | |
function encodeUriSegment(val) { | |
return encodeUriQuery(val, true). | |
replace(/%26/gi, '&'). | |
replace(/%3D/gi, '='). | |
replace(/%2B/gi, '+'); | |
} | |
/** | |
* 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 / "/" / "?" ) | |
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@" | |
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" | |
* pct-encoded = "%" HEXDIG HEXDIG | |
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")" | |
* / "*" / "+" / "," / ";" / "=" | |
*/ | |
function encodeUriQuery(val, pctEncodeSpaces) { | |
return encodeURIComponent(val). | |
replace(/%40/gi, '@'). | |
replace(/%3A/gi, ':'). | |
replace(/%24/g, '$'). | |
replace(/%2C/gi, ','). | |
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); | |
} | |
/** | |
* @ngdoc directive | |
* @name ng.directive:ngApp | |
* | |
* @element ANY | |
* @param {angular.Module} ngApp an optional application | |
* {@link angular.module module} name to load. | |
* | |
* @description | |
* | |
* Use this directive to auto-bootstrap an application. Only | |
* one ngApp directive can be used per HTML document. The directive | |
* designates the root of the application and is typically placed | |
* at the root of the page. | |
* | |
* The first ngApp found in the document will be auto-bootstrapped. To use multiple applications in an | |
* HTML document you must manually bootstrap them using {@link angular.bootstrap}. | |
* Applications cannot be nested. | |
* | |
* In the example below if the `ngApp` directive were not placed | |
* on the `html` element then the document would not be compiled | |
* and the `{{ 1+2 }}` would not be resolved to `3`. | |
* | |
* `ngApp` is the easiest way to bootstrap an application. | |
* | |
<doc:example> | |
<doc:source> | |
I can add: 1 + 2 = {{ 1+2 }} | |
</doc:source> | |
</doc:example> | |
* | |
*/ | |
function angularInit(element, bootstrap) { | |
var elements = [element], | |
appElement, | |
module, | |
names = ['ng:app', 'ng-app', 'x-ng-app', 'data-ng-app'], | |
NG_APP_CLASS_REGEXP = /\sng[:\-]app(:\s*([\w\d_]+);?)?\s/; | |
function append(element) { | |
element && elements.push(element); | |
} | |
forEach(names, function(name) { | |
names[name] = true; | |
append(document.getElementById(name)); | |
name = name.replace(':', '\\:'); | |
if (element.querySelectorAll) { | |
forEach(element.querySelectorAll('.' + name), append); | |
forEach(element.querySelectorAll('.' + name + '\\:'), append); | |
forEach(element.querySelectorAll('[' + name + ']'), append); | |
} | |
}); | |
forEach(elements, function(element) { | |
if (!appElement) { | |
var className = ' ' + element.className + ' '; | |
var match = NG_APP_CLASS_REGEXP.exec(className); | |
if (match) { | |
appElement = element; | |
module = (match[2] || '').replace(/\s+/g, ','); | |
} else { | |
forEach(element.attributes, function(attr) { | |
if (!appElement && names[attr.name]) { | |
appElement = element; | |
module = attr.value; | |
} | |
}); | |
} | |
} | |
}); | |
if (appElement) { | |
bootstrap(appElement, module ? [module] : []); | |
} | |
} | |
/** | |
* @ngdoc function | |
* @name angular.bootstrap | |
* @description | |
* Use this function to manually start up angular application. | |
* | |
* See: {@link guide/bootstrap Bootstrap} | |
* | |
* Note that ngScenario-based end-to-end tests cannot use this function to bootstrap manually. | |
* They must use {@link api/ng.directive:ngApp ngApp}. | |
* | |
* @param {Element} element DOM element which is the root of angular application. | |
* @param {Array<String|Function|Array>=} modules an array of modules to load into the application. | |
* Each item in the array should be the name of a predefined module or a (DI annotated) | |
* function that will be invoked by the injector as a run block. See: {@link angular.module modules} | |
* @returns {AUTO.$injector} Returns the newly created injector for this app. | |
*/ | |
function bootstrap(element, modules) { | |
var doBootstrap = function() { | |
element = jqLite(element); | |
if (element.injector()) { | |
var tag = (element[0] === document) ? 'document' : startingTag(element); | |
throw ngMinErr('btstrpd', "App Already Bootstrapped with this Element '{0}'", tag); | |
} | |
modules = modules || []; | |
modules.unshift(['$provide', function($provide) { | |
$provide.value('$rootElement', element); | |
}]); | |
modules.unshift('ng'); | |
var injector = createInjector(modules); | |
injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector', '$animate', | |
function(scope, element, compile, injector, animate) { | |
scope.$apply(function() { | |
element.data('$injector', injector); | |
compile(element)(scope); | |
}); | |
animate.enabled(true); | |
}] | |
); | |
return injector; | |
}; | |
var NG_DEFER_BOOTSTRAP = /^NG_DEFER_BOOTSTRAP!/; | |
if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) { | |
return doBootstrap(); | |
} | |
window.name = window.name.replace(NG_DEFER_BOOTSTRAP, ''); | |
angular.resumeBootstrap = function(extraModules) { | |
forEach(extraModules, function(module) { | |
modules.push(module); | |
}); | |
doBootstrap(); | |
}; | |
} | |
var SNAKE_CASE_REGEXP = /[A-Z]/g; | |
function snake_case(name, separator){ | |
separator = separator || '_'; | |
return name.replace(SNAKE_CASE_REGEXP, function(letter, pos) { | |
return (pos ? separator : '') + letter.toLowerCase(); | |
}); | |
} | |
function bindJQuery() { | |
// bind to jQuery if present; | |
jQuery = window.jQuery; | |
// reset to jQuery or default to us. | |
if (jQuery) { | |
jqLite = jQuery; | |
extend(jQuery.fn, { | |
scope: JQLitePrototype.scope, | |
controller: JQLitePrototype.controller, | |
injector: JQLitePrototype.injector, | |
inheritedData: JQLitePrototype.inheritedData | |
}); | |
// Method signature: JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) | |
JQLitePatchJQueryRemove('remove', true, true, false); | |
JQLitePatchJQueryRemove('empty', false, false, false); | |
JQLitePatchJQueryRemove('html', false, false, true); | |
} else { | |
jqLite = JQLite; | |
} | |
angular.element = jqLite; | |
} | |
/** | |
* throw error if the argument is falsy. | |
*/ | |
function assertArg(arg, name, reason) { | |
if (!arg) { | |
throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required")); | |
} | |
return arg; | |
} | |
function assertArgFn(arg, name, acceptArrayAnnotation) { | |
if (acceptArrayAnnotation && isArray(arg)) { | |
arg = arg[arg.length - 1]; | |
} | |
assertArg(isFunction(arg), name, 'not a function, got ' + | |
(arg && typeof arg == 'object' ? arg.constructor.name || 'Object' : typeof arg)); | |
return arg; | |
} | |
/** | |
* throw error if the name given is hasOwnProperty | |
* @param {String} name the name to test | |
* @param {String} context the context in which the name is used, such as module or directive | |
*/ | |
function assertNotHasOwnProperty(name, context) { | |
if (name === 'hasOwnProperty') { | |
throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context); | |
} | |
} | |
/** | |
* Return the value accessible from the object by path. Any undefined traversals are ignored | |
* @param {Object} obj starting object | |
* @param {string} path path to traverse | |
* @param {boolean=true} bindFnToScope | |
* @returns value as accessible by path | |
*/ | |
//TODO(misko): this function needs to be removed | |
function getter(obj, path, bindFnToScope) { | |
if (!path) return obj; | |
var keys = path.split('.'); | |
var key; | |
var lastInstance = obj; | |
var len = keys.length; | |
for (var i = 0; i < len; i++) { | |
key = keys[i]; | |
if (obj) { | |
obj = (lastInstance = obj)[key]; | |
} | |
} | |
if (!bindFnToScope && isFunction(obj)) { | |
return bind(lastInstance, obj); | |
} | |
return obj; | |
} | |
/** | |
* @ngdoc interface | |
* @name angular.Module | |
* @description | |
* | |
* Interface for configuring angular {@link angular.module modules}. | |
*/ | |
function setupModuleLoader(window) { | |
var $injectorMinErr = minErr('$injector'); | |
function ensure(obj, name, factory) { | |
return obj[name] || (obj[name] = factory()); | |
} | |
return ensure(ensure(window, 'angular', Object), 'module', function() { | |
/** @type {Object.<string, angular.Module>} */ | |
var modules = {}; | |
/** | |
* @ngdoc function | |
* @name angular.module | |
* @description | |
* | |
* The `angular.module` is a global place for creating, registering and retrieving Angular modules. | |
* All modules (angular core or 3rd party) that should be available to an application must be | |
* registered using this mechanism. | |
* | |
* When passed two or more arguments, a new module is created. If passed only one argument, an | |
* existing module (the name passed as the first argument to `module`) is retrieved. | |
* | |
* | |
* # Module | |
* | |
* A module is a collection of services, directives, filters, and configuration information. | |
* `angular.module` is used to configure the {@link AUTO.$injector $injector}. | |
* | |
* <pre> | |
* // Create a new module | |
* var myModule = angular.module('myModule', []); | |
* | |
* // register a new service | |
* myModule.value('appName', 'MyCoolApp'); | |
* | |
* // configure existing services inside initialization blocks. | |
* myModule.config(function($locationProvider) { | |
* // Configure existing providers | |
* $locationProvider.hashPrefix('!'); | |
* }); | |
* </pre> | |
* | |
* Then you can create an injector and load your modules like this: | |
* | |
* <pre> | |
* var injector = angular.injector(['ng', 'MyModule']) | |
* </pre> | |
* | |
* However it's more likely that you'll just use | |
* {@link ng.directive:ngApp ngApp} or | |
* {@link angular.bootstrap} to simplify this process for you. | |
* | |
* @param {!string} name The name of the module to create or retrieve. | |
* @param {Array.<string>=} requires If specified then new module is being created. If unspecified then the | |
* 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. | |
*/ | |
return function module(name, requires, configFn) { | |
assertNotHasOwnProperty(name, 'module'); | |
if (requires && modules.hasOwnProperty(name)) { | |
modules[name] = null; | |
} | |
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); | |
} | |
/** @type {!Array.<Array.<*>>} */ | |
var invokeQueue = []; | |
/** @type {!Array.<Function>} */ | |
var runBlocks = []; | |
var config = invokeLater('$injector', 'invoke'); | |
/** @type {angular.Module} */ | |
var moduleInstance = { | |
// Private state | |
_invokeQueue: invokeQueue, | |
_runBlocks: runBlocks, | |
/** | |
* @ngdoc property | |
* @name angular.Module#requires | |
* @propertyOf angular.Module | |
* @returns {Array.<string>} List of module names which must be loaded before this module. | |
* @description | |
* Holds the list of modules which the injector will load before the current module is loaded. | |
*/ | |
requires: requires, | |
/** | |
* @ngdoc property | |
* @name angular.Module#name | |
* @propertyOf angular.Module | |
* @returns {string} Name of the module. | |
* @description | |
*/ | |
name: name, | |
/** | |
* @ngdoc method | |
* @name angular.Module#provider | |
* @methodOf angular.Module | |
* @param {string} name service name | |
* @param {Function} providerType Construction function for creating new instance of the service. | |
* @description | |
* See {@link AUTO.$provide#provider $provide.provider()}. | |
*/ | |
provider: invokeLater('$provide', 'provider'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#factory | |
* @methodOf angular.Module | |
* @param {string} name service name | |
* @param {Function} providerFunction Function for creating new instance of the service. | |
* @description | |
* See {@link AUTO.$provide#factory $provide.factory()}. | |
*/ | |
factory: invokeLater('$provide', 'factory'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#service | |
* @methodOf angular.Module | |
* @param {string} name service name | |
* @param {Function} constructor A constructor function that will be instantiated. | |
* @description | |
* See {@link AUTO.$provide#service $provide.service()}. | |
*/ | |
service: invokeLater('$provide', 'service'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#value | |
* @methodOf angular.Module | |
* @param {string} name service name | |
* @param {*} object Service instance object. | |
* @description | |
* See {@link AUTO.$provide#value $provide.value()}. | |
*/ | |
value: invokeLater('$provide', 'value'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#constant | |
* @methodOf angular.Module | |
* @param {string} name constant name | |
* @param {*} object Constant value. | |
* @description | |
* Because the constant are fixed, they get applied before other provide methods. | |
* See {@link AUTO.$provide#constant $provide.constant()}. | |
*/ | |
constant: invokeLater('$provide', 'constant', 'unshift'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#animation | |
* @methodOf angular.Module | |
* @param {string} name animation name | |
* @param {Function} animationFactory Factory function for creating new instance of an animation. | |
* @description | |
* | |
* **NOTE**: animations take effect only if the **ngAnimate** module is loaded. | |
* | |
* | |
* Defines an animation hook that can be later used with {@link ngAnimate.$animate $animate} service and | |
* directives that use this service. | |
* | |
* <pre> | |
* module.animation('.animation-name', function($inject1, $inject2) { | |
* return { | |
* eventName : function(element, done) { | |
* //code to run the animation | |
* //once complete, then run done() | |
* return function cancellationFunction(element) { | |
* //code to cancel the animation | |
* } | |
* } | |
* } | |
* }) | |
* </pre> | |
* | |
* See {@link ngAnimate.$animateProvider#register $animateProvider.register()} and | |
* {@link ngAnimate ngAnimate module} for more information. | |
*/ | |
animation: invokeLater('$animateProvider', 'register'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#filter | |
* @methodOf angular.Module | |
* @param {string} name Filter name. | |
* @param {Function} filterFactory Factory function for creating new instance of filter. | |
* @description | |
* See {@link ng.$filterProvider#register $filterProvider.register()}. | |
*/ | |
filter: invokeLater('$filterProvider', 'register'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#controller | |
* @methodOf angular.Module | |
* @param {string|Object} name Controller name, or an object map of controllers where the | |
* keys are the names and the values are the constructors. | |
* @param {Function} constructor Controller constructor function. | |
* @description | |
* See {@link ng.$controllerProvider#register $controllerProvider.register()}. | |
*/ | |
controller: invokeLater('$controllerProvider', 'register'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#directive | |
* @methodOf angular.Module | |
* @param {string|Object} name Directive name, or an object map of directives where the | |
* keys are the names and the values are the factories. | |
* @param {Function} directiveFactory Factory function for creating new instance of | |
* directives. | |
* @description | |
* See {@link ng.$compileProvider#directive $compileProvider.directive()}. | |
*/ | |
directive: invokeLater('$compileProvider', 'directive'), | |
/** | |
* @ngdoc method | |
* @name angular.Module#config | |
* @methodOf angular.Module | |
* @param {Function} configFn Execute this function on module load. Useful for service | |
* configuration. | |
* @description | |
* Use this method to register work which needs to be performed on module loading. | |
*/ | |
config: config, | |
/** | |
* @ngdoc method | |
* @name angular.Module#run | |
* @methodOf angular.Module | |
* @param {Function} initializationFn Execute this function after injector creation. | |
* Useful for application initialization. | |
* @description | |
* Use this method to register work which should be performed when the injector is done | |
* loading all modules. | |
*/ | |
run: function(block) { | |
runBlocks.push(block); | |
return this; | |
} | |
}; | |
if (configFn) { | |
config(configFn); | |
} | |
return moduleInstance; | |
/** | |
* @param {string} provider | |
* @param {string} method | |
* @param {String=} insertMethod | |
* @returns {angular.Module} | |
*/ | |
function invokeLater(provider, method, insertMethod) { | |
return function() { | |
invokeQueue[insertMethod || 'push']([provider, method, arguments]); | |
return moduleInstance; | |
} | |
} | |
}); | |
}; | |
}); | |
} | |
/** | |
* @ngdoc property | |
* @name angular.version | |
* @description | |
* An object that contains information about the current AngularJS version. This object has the | |
* following properties: | |
* | |
* - `full` – `{string}` – Full version string, such as "0.9.18". | |
* - `major` – `{number}` – Major version number, such as "0". | |
* - `minor` – `{number}` – Minor version number, such as "9". | |
* - `dot` – `{number}` – Dot version number, such as "18". | |
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat". | |
*/ | |
var version = { | |
full: '1.2.0-rc.3', // all of these placeholder strings will be replaced by grunt's | |
major: 1, // package task | |
minor: 2, | |
dot: 0, | |
codeName: 'ferocious-twitch' | |
}; | |
function publishExternalAPI(angular){ | |
extend(angular, { | |
'bootstrap': bootstrap, | |
'copy': copy, | |
'extend': extend, | |
'equals': equals, | |
'element': jqLite, | |
'forEach': forEach, | |
'injector': createInjector, | |
'noop':noop, | |
'bind':bind, | |
'toJson': toJson, | |
'fromJson': fromJson, | |
'identity':identity, | |
'isUndefined': isUndefined, | |
'isDefined': isDefined, | |
'isString': isString, | |
'isFunction': isFunction, | |
'isObject': isObject, | |
'isNumber': isNumber, | |
'isElement': isElement, | |
'isArray': isArray, | |
'$$minErr': minErr, | |
'version': version, | |
'isDate': isDate, | |
'lowercase': lowercase, | |
'uppercase': uppercase, | |
'callbacks': {counter: 0} | |
}); | |
angularModule = setupModuleLoader(window); | |
try { | |
angularModule('ngLocale'); | |
} catch (e) { | |
angularModule('ngLocale', []).provider('$locale', $LocaleProvider); | |
} | |
angularModule('ng', ['ngLocale'], ['$provide', | |
function ngModule($provide) { | |
$provide.provider('$compile', $CompileProvider). | |
directive({ | |
a: htmlAnchorDirective, | |
input: inputDirective, | |
textarea: inputDirective, | |
form: formDirective, | |
script: scriptDirective, | |
select: selectDirective, | |
style: styleDirective, | |
option: optionDirective, | |
ngBind: ngBindDirective, | |
ngBindHtml: ngBindHtmlDirective, | |
ngBindTemplate: ngBindTemplateDirective, | |
ngClass: ngClassDirective, | |
ngClassEven: ngClassEvenDirective, | |
ngClassOdd: ngClassOddDirective, | |
ngCsp: ngCspDirective, | |
ngCloak: ngCloakDirective, | |
ngController: ngControllerDirective, | |
ngForm: ngFormDirective, | |
ngHide: ngHideDirective, | |
ngIf: ngIfDirective, | |
ngInclude: ngIncludeDirective, | |
ngInit: ngInitDirective, | |
ngNonBindable: ngNonBindableDirective, | |
ngPluralize: ngPluralizeDirective, | |
ngRepeat: ngRepeatDirective, | |
ngShow: ngShowDirective, | |
ngStyle: ngStyleDirective, | |
ngSwitch: ngSwitchDirective, | |
ngSwitchWhen: ngSwitchWhenDirective, | |
ngSwitchDefault: ngSwitchDefaultDirective, | |
ngOptions: ngOptionsDirective, | |
ngTransclude: ngTranscludeDirective, | |
ngModel: ngModelDirective, | |
ngList: ngListDirective, | |
ngChange: ngChangeDirective, | |
required: requiredDirective, | |
ngRequired: requiredDirective, | |
ngValue: ngValueDirective | |
}). | |
directive(ngAttributeAliasDirectives). | |
directive(ngEventDirectives); | |
$provide.provider({ | |
$anchorScroll: $AnchorScrollProvider, | |
$animate: $AnimateProvider, | |
$browser: $BrowserProvider, | |
$cacheFactory: $CacheFactoryProvider, | |
$controller: $ControllerProvider, | |
$document: $DocumentProvider, | |
$exceptionHandler: $ExceptionHandlerProvider, | |
$filter: $FilterProvider, | |
$interpolate: $InterpolateProvider, | |
$interval: $IntervalProvider, | |
$http: $HttpProvider, | |
$httpBackend: $HttpBackendProvider, | |
$location: $LocationProvider, | |
$log: $LogProvider, | |
$parse: $ParseProvider, | |
$rootScope: $RootScopeProvider, | |
$q: $QProvider, | |
$sce: $SceProvider, | |
$sceDelegate: $SceDelegateProvider, | |
$sniffer: $SnifferProvider, | |
$templateCache: $TemplateCacheProvider, | |
$timeout: $TimeoutProvider, | |
$window: $WindowProvider | |
}); | |
} | |
]); | |
} | |
////////////////////////////////// | |
//JQLite | |
////////////////////////////////// | |
/** | |
* @ngdoc function | |
* @name angular.element | |
* @function | |
* | |
* @description | |
* Wraps a raw DOM element or HTML string as a [jQuery](http://jquery.com) element. | |
* | |
* 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." | |
* | |
* <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 load it before `DOMContentLoaded` event fired. | |
* | |
* <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or | |
* jqLite; they are never raw DOM references.</div> | |
* | |
* ## Angular's jqLite | |
* jqLite provides only the following jQuery methods: | |
* | |
* - [`addClass()`](http://api.jquery.com/addClass/) | |
* - [`after()`](http://api.jquery.com/after/) | |
* - [`append()`](http://api.jquery.com/append/) | |
* - [`attr()`](http://api.jquery.com/attr/) | |
* - [`bind()`](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/) | |
* - [`data()`](http://api.jquery.com/data/) | |
* - [`eq()`](http://api.jquery.com/eq/) | |
* - [`find()`](http://api.jquery.com/find/) - Limited to lookups by tag name | |
* - [`hasClass()`](http://api.jquery.com/hasClass/) | |
* - [`html()`](http://api.jquery.com/html/) | |
* - [`next()`](http://api.jquery.com/next/) - Does not support selectors | |
* - [`on()`](http://api.jquery.com/on/) - Does not support namespaces, selectors or eventData | |
* - [`off()`](http://api.jquery.com/off/) - Does not support namespaces or selectors | |
* - [`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/) | |
* - [`remove()`](http://api.jquery.com/remove/) | |
* - [`removeAttr()`](http://api.jquery.com/removeAttr/) | |
* - [`removeClass()`](http://api.jquery.com/removeClass/) | |
* - [`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/off/) - Does not support namespaces | |
* - [`val()`](http://api.jquery.com/val/) | |
* - [`wrap()`](http://api.jquery.com/wrap/) | |
* | |
* ## jQuery/jqLite Extras | |
* Angular also provides the following additional methods and events to both jQuery and jqLite: | |
* | |
* ### Events | |
* - `$destroy` - AngularJS intercepts all jqLite/jQuery's DOM destruction apis and fires this event | |
* on all DOM nodes being removed. This can be used to clean up any 3rd party bindings to the DOM | |
* element before it is removed. | |
* | |
* ### Methods | |
* - `controller(name)` - retrieves the controller of the current element or its parent. By default | |
* retrieves controller associated with the `ngController` directive. If `name` is provided as | |
* camelCase directive name, then the controller for this directive will be retrieved (e.g. | |
* `'ngModel'`). | |
* - `injector()` - retrieves the injector of the current element or its parent. | |
* - `scope()` - retrieves the {@link api/ng.$rootScope.Scope scope} of the current | |
* element or its parent. | |
* - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top | |
* parent element is reached. | |
* | |
* @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery. | |
* @returns {Object} jQuery object. | |
*/ | |
var jqCache = JQLite.cache = {}, | |
jqName = JQLite.expando = 'ng-' + new Date().getTime(), | |
jqId = 1, | |
addEventListenerFn = (window.document.addEventListener | |
? function(element, type, fn) {element.addEventListener(type, fn, false);} | |
: function(element, type, fn) {element.attachEvent('on' + type, fn);}), | |
removeEventListenerFn = (window.document.removeEventListener | |
? function(element, type, fn) {element.removeEventListener(type, fn, false); } | |
: function(element, type, fn) {element.detachEvent('on' + type, fn); }); | |
function jqNextId() { return ++jqId; } | |
var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g; | |
var MOZ_HACK_REGEXP = /^moz([A-Z])/; | |
var jqLiteMinErr = minErr('jqLite'); | |
/** | |
* Converts snake_case to camelCase. | |
* Also there is special case for Moz prefix starting with upper case 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'); | |
} | |
///////////////////////////////////////////// | |
// jQuery mutation patch | |
// | |
// In conjunction with bindJQuery intercepts all jQuery's DOM destruction apis and fires a | |
// $destroy event on all DOM nodes being removed. | |
// | |
///////////////////////////////////////////// | |
function JQLitePatchJQueryRemove(name, dispatchThis, filterElems, getterIfNoArguments) { | |
var originalJqFn = jQuery.fn[name]; | |
originalJqFn = originalJqFn.$original || originalJqFn; | |
removePatch.$original = originalJqFn; | |
jQuery.fn[name] = removePatch; | |
function removePatch(param) { | |
var list = filterElems && param ? [this.filter(param)] : [this], | |
fireEvent = dispatchThis, | |
set, setIndex, setLength, | |
element, childIndex, childLength, children; | |
if (!getterIfNoArguments || param != null) { | |
while(list.length) { | |
set = list.shift(); | |
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) { | |
element = jqLite(set[setIndex]); | |
if (fireEvent) { | |
element.triggerHandler('$destroy'); | |
} else { | |
fireEvent = !fireEvent; | |
} | |
for(childIndex = 0, childLength = (children = element.children()).length; | |
childIndex < childLength; | |
childIndex++) { | |
list.push(jQuery(children[childIndex])); | |
} | |
} | |
} | |
} | |
return originalJqFn.apply(this, arguments); | |
} | |
} | |
///////////////////////////////////////////// | |
function JQLite(element) { | |
if (element instanceof JQLite) { | |
return element; | |
} | |
if (!(this instanceof JQLite)) { | |
if (isString(element) && 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); | |
} | |
if (isString(element)) { | |
var div = document.createElement('div'); | |
// Read about the NoScope elements here: | |
// http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx | |
div.innerHTML = '<div> </div>' + element; // IE insanity to make NoScope elements work! | |
div.removeChild(div.firstChild); // remove the superfluous div | |
JQLiteAddNodes(this, div.childNodes); | |
var fragment = jqLite(document.createDocumentFragment()); | |
fragment.append(this); // detach the elements from the temporary DOM div. | |
} else { | |
JQLiteAddNodes(this, element); | |
} | |
} | |
function JQLiteClone(element) { | |
return element.cloneNode(true); | |
} | |
function JQLiteDealoc(element){ | |
JQLiteRemoveData(element); | |
for ( var i = 0, children = element.childNodes || []; i < children.length; i++) { | |
JQLiteDealoc(children[i]); | |
} | |
} | |
function JQLiteOff(element, type, fn, unsupported) { | |
if (isDefined(unsupported)) throw jqLiteMinErr('offargs', 'jqLite#off() does not support the `selector` argument'); | |
var events = JQLiteExpandoStore(element, 'events'), | |
handle = JQLiteExpandoStore(element, 'handle'); | |
if (!handle) return; //no listeners registered | |
if (isUndefined(type)) { | |
forEach(events, function(eventHandler, type) { | |
removeEventListenerFn(element, type, eventHandler); | |
delete events[type]; | |
}); | |
} else { | |
forEach(type.split(' '), function(type) { | |
if (isUndefined(fn)) { | |
removeEventListenerFn(element, type, events[type]); | |
delete events[type]; | |
} else { | |
arrayRemove(events[type] || [], fn); | |
} | |
}); | |
} | |
} | |
function JQLiteRemoveData(element, name) { | |
var expandoId = element[jqName], | |
expandoStore = jqCache[expandoId]; | |
if (expandoStore) { | |
if (name) { | |
delete jqCache[expandoId].data[name]; | |
return; | |
} | |
if (expandoStore.handle) { | |
expandoStore.events.$destroy && expandoStore.handle({}, '$destroy'); | |
JQLiteOff(element); | |
} | |
delete jqCache[expandoId]; | |
element[jqName] = undefined; // ie does not allow deletion of attributes on elements. | |
} | |
} | |
function JQLiteExpandoStore(element, key, value) { | |
var expandoId = element[jqName], | |
expandoStore = jqCache[expandoId || -1]; | |
if (isDefined(value)) { | |
if (!expandoStore) { | |
element[jqName] = expandoId = jqNextId(); | |
expandoStore = jqCache[expandoId] = {}; | |
} | |
expandoStore[key] = value; | |
} else { | |
return expandoStore && expandoStore[key]; | |
} | |
} | |
function JQLiteData(element, key, value) { | |
var data = JQLiteExpandoStore(element, 'data'), | |
isSetter = isDefined(value), | |
keyDefined = !isSetter && isDefined(key), | |
isSimpleGetter = keyDefined && !isObject(key); | |
if (!data && !isSimpleGetter) { | |
JQLiteExpandoStore(element, 'data', data = {}); | |
} | |
if (isSetter) { | |
data[key] = value; | |
} else { | |
if (keyDefined) { | |
if (isSimpleGetter) { | |
// don't create data in this case. | |
return data && data[key]; | |
} else { | |
extend(data, key); | |
} | |
} else { | |
return data; | |
} | |
} | |
} | |
function JQLiteHasClass(element, selector) { | |
if (!element.getAttribute) return false; | |
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) + " ", " ")) | |
); | |
}); | |
} | |
} | |
function JQLiteAddClass(element, cssClasses) { | |
if (cssClasses && element.setAttribute) { | |
var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ') | |
.replace(/[\n\t]/g, " "); | |
forEach(cssClasses.split(' '), function(cssClass) { | |
cssClass = trim(cssClass); | |
if (existingClasses.indexOf(' ' + cssClass + ' ') === -1) { | |
existingClasses += cssClass + ' '; | |
} | |
}); | |
element.setAttribute('class', trim(existingClasses)); | |
} | |
} | |
function JQLiteAddNodes(root, elements) { | |
if (elements) { | |
elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements)) | |
? elements | |
: [ elements ]; | |
for(var i=0; i < elements.length; i++) { | |
root.push(elements[i]); | |
} | |
} | |
} | |
function JQLiteController(element, name) { | |
return JQLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller'); | |
} | |
function JQLiteInheritedData(element, name, value) { | |
element = jqLite(element); | |
// if element is the document object work with the html element instead | |
// this makes $(document).scope() possible | |
if(element[0].nodeType == 9) { | |
element = element.find('html'); | |
} | |
while (element.length) { | |
if ((value = element.data(name)) !== undefined) return value; | |
element = element.parent(); | |
} | |
} | |
////////////////////////////////////////// | |
// 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 already is 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. | |
JQLite(window).on('load', trigger); // fallback to window.onload for others | |
} | |
}, | |
toString: function() { | |
var value = []; | |
forEach(this, function(e){ value.push('' + e);}); | |
return '[' + value.join(', ') + ']'; | |
}, | |
eq: function(index) { | |
return (index >= 0) ? jqLite(this[index]) : jqLite(this[this.length + index]); | |
}, | |
length: 0, | |
push: push, | |
sort: [].sort, | |
splice: [].splice | |
}; | |
////////////////////////////////////////// | |
// Functions iterating getter/setters. | |
// these functions return self on setter and | |
// value on get. | |
////////////////////////////////////////// | |
var BOOLEAN_ATTR = {}; | |
forEach('multiple,selected,checked,disabled,readOnly,required,open'.split(','), function(value) { | |
BOOLEAN_ATTR[lowercase(value)] = value; | |
}); | |
var BOOLEAN_ELEMENTS = {}; | |
forEach('input,select,option,textarea,button,form,details'.split(','), function(value) { | |
BOOLEAN_ELEMENTS[uppercase(value)] = true; | |
}); | |
function getBooleanAttrName(element, name) { | |
// check dom last since we will most likely fail on name | |
var booleanAttr = BOOLEAN_ATTR[name.toLowerCase()]; | |
// booleanAttr is here twice to minimize DOM access | |
return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr; | |
} | |
forEach({ | |
data: JQLiteData, | |
inheritedData: JQLiteInheritedData, | |
scope: function(element) { | |
return JQLiteInheritedData(element, '$scope'); | |
}, | |
controller: JQLiteController , | |
injector: function(element) { | |
return JQLiteInheritedData(element, '$injector'); | |
}, | |
removeAttr: function(element,name) { | |
element.removeAttribute(name); | |
}, | |
hasClass: JQLiteHasClass, | |
css: function(element, name, value) { | |
name = camelCase(name); | |
if (isDefined(value)) { | |
element.style[name] = value; | |
} else { | |
var val; | |
if (msie <= 8) { | |
// this is some IE specific weirdness that jQuery 1.6.4 does not sure why | |
val = element.currentStyle && element.currentStyle[name]; | |
if (val === '') val = 'auto'; | |
} | |
val = val || element.style[name]; | |
if (msie <= 8) { | |
// jquery weirdness :-/ | |
val = (val === '') ? undefined : val; | |
} | |
return val; | |
} | |
}, | |
attr: function(element, name, value){ | |
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); | |
} | |
} 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) | |
return ret === null ? undefined : ret; | |
} | |
}, | |
prop: function(element, name, value) { | |
if (isDefined(value)) { | |
element[name] = value; | |
} else { | |
return element[name]; | |
} | |
}, | |
text: (function() { | |
var NODE_TYPE_TEXT_PROPERTY = []; | |
if (msie < 9) { | |
NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/ | |
NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/ | |
} else { | |
NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/ | |
NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/ | |
} | |
getText.$dv = ''; | |
return getText; | |
function getText(element, value) { | |
var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType] | |
if (isUndefined(value)) { | |
return textProp ? element[textProp] : ''; | |
} | |
element[textProp] = value; | |
} | |
})(), | |
val: function(element, value) { | |
if (isUndefined(value)) { | |
if (nodeName_(element) === 'SELECT' && element.multiple) { | |
var result = []; | |
forEach(element.options, function (option) { | |
if (option.selected) { | |
result.push(option.value || option.text); | |
} | |
}); | |
return result.length === 0 ? null : result; | |
} | |
return element.value; | |
} | |
element.value = value; | |
}, | |
html: function(element, value) { | |
if (isUndefined(value)) { | |
return element.innerHTML; | |
} | |
for (var i = 0, childNodes = element.childNodes; i < childNodes.length; i++) { | |
JQLiteDealoc(childNodes[i]); | |
} | |
element.innerHTML = value; | |
} | |
}, function(fn, name){ | |
/** | |
* Properties: writes return selection, reads return first value | |
*/ | |
JQLite.prototype[name] = function(arg1, arg2) { | |
var i, key; | |
// JQLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it | |
// in a way that survives minification. | |
if (((fn.length == 2 && (fn !== JQLiteHasClass && fn !== JQLiteController)) ? arg1 : arg2) === undefined) { | |
if (isObject(arg1)) { | |
// we are a write, but the object properties are the key/values | |
for(i=0; i < this.length; i++) { | |
if (fn === JQLiteData) { | |
// data() takes the whole object in jQuery | |
fn(this[i], arg1); | |
} else { | |
for (key in arg1) { | |
fn(this[i], key, arg1[key]); | |
} | |
} | |
} | |
// return self for chaining | |
return this; | |
} else { | |
// we are a read, so read the first child. | |
var value = fn.$dv; | |
// Only if we have $dv do we iterate over all, otherwise it is just the first element. | |
var jj = value == undefined ? Math.min(this.length, 1) : this.length; | |
for (var j = 0; j < jj; j++) { | |
var nodeValue = fn(this[j], arg1, arg2); | |
value = value ? value + nodeValue : nodeValue; | |
} | |
return value; | |
} | |
} else { | |
// we are a write, so apply to all children | |
for(i=0; i < this.length; i++) { | |
fn(this[i], arg1, arg2); | |
} | |
// return self for chaining | |
return this; | |
} | |
}; | |
}); | |
function createEventHandler(element, events) { | |
var eventHandler = function (event, type) { | |
if (!event.preventDefault) { | |
event.preventDefault = function() { | |
event.returnValue = false; //ie | |
}; | |
} | |
if (!event.stopPropagation) { | |
event.stopPropagation = function() { | |
event.cancelBubble = true; //ie | |
}; | |
} | |
if (!event.target) { | |
event.target = event.srcElement || document; | |
} | |
if (isUndefined(event.defaultPrevented)) { | |
var prevent = event.preventDefault; | |
event.preventDefault = function() { | |
event.defaultPrevented = true; | |
prevent.call(event); | |
}; | |
event.defaultPrevented = false; | |
} | |
event.isDefaultPrevented = function() { | |
return event.defaultPrevented || event.returnValue == false; | |
}; | |
forEach(events[type || event.type], function(fn) { | |
fn.call(element, event); | |
}); | |
// Remove monkey-patched methods (IE), | |
// as they would cause memory leaks in IE8. | |
if (msie <= 8) { | |
// IE7/8 does not allow to delete property on native object | |
event.preventDefault = null; | |
event.stopPropagation = null; | |
event.isDefaultPrevented = null; | |
} else { | |
// It shouldn't affect normal browsers (native methods are defined on prototype). | |
delete event.preventDefault; | |
delete event.stopPropagation; | |
delete event.isDefaultPrevented; | |
} | |
}; | |
eventHandler.elem = element; | |
return eventHandler; | |
} | |
////////////////////////////////////////// | |
// Functions iterating traversal. | |
// These functions chain results into a single | |
// selector. | |
////////////////////////////////////////// | |
forEach({ | |
removeData: JQLiteRemoveData, | |
dealoc: JQLiteDealoc, | |
on: function onFn(element, type, fn, unsupported){ | |
if (isDefined(unsupported)) throw jqLiteMinErr('onargs', 'jqLite#on() does not support the `selector` or `eventData` parameters'); | |
var events = JQLiteExpandoStore(element, 'events'), | |
handle = JQLiteExpandoStore(element, 'handle'); | |
if (!events) JQLiteExpandoStore(element, 'events', events = {}); | |
if (!handle) JQLiteExpandoStore(element, 'handle', handle = createEventHandler(element, events)); | |
forEach(type.split(' '), function(type){ | |
var eventFns = events[type]; | |
if (!eventFns) { | |
if (type == 'mouseenter' || type == 'mouseleave') { | |
var contains = document.body.contains || document.body.compareDocumentPosition ? | |
function( a, b ) { | |
var adown = a.nodeType === 9 ? a.documentElement : a, | |
bup = b && b.parentNode; | |
return a === bup || !!( bup && bup.nodeType === 1 && ( | |
adown.contains ? | |
adown.contains( bup ) : | |
a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 | |
)); | |
} : | |
function( a, b ) { | |
if ( b ) { | |
while ( (b = b.parentNode) ) { | |
if ( b === a ) { | |
return true; | |
} | |
} | |
} | |
return false; | |
}; | |
events[type] = []; | |
// Refer to jQuery's implementation of mouseenter & mouseleave | |
// Read about mouseenter and mouseleave: | |
// http://www.quirksmode.org/js/events_mouse.html#link8 | |
var eventmap = { mouseleave : "mouseout", mouseenter : "mouseover"}; | |
onFn(element, eventmap[type], function(event) { | |
var target = this, related = event.relatedTarget; | |
// For mousenter/leave call the handler if related is outside the target. | |
// NB: No relatedTarget if the mouse left/entered the browser window | |
if ( !related || (related !== target && !contains(target, related)) ){ | |
handle(event, type); | |
} | |
}); | |
} else { | |
addEventListenerFn(element, type, handle); | |
events[type] = []; | |
} | |
eventFns = events[type] | |
} | |
eventFns.push(fn); | |
}); | |
}, | |
off: JQLiteOff, | |
replaceWith: function(element, replaceNode) { | |
var index, parent = element.parentNode; | |
JQLiteDealoc(element); | |
forEach(new JQLite(replaceNode), function(node){ | |
if (index) { | |
parent.insertBefore(node, index.nextSibling); | |
} else { | |
parent.replaceChild(node, element); | |
} | |
index = node; | |
}); | |
}, | |
children: function(element) { | |
var children = []; | |
forEach(element.childNodes, function(element){ | |
if (element.nodeType === 1) | |
children.push(element); | |
}); | |
return children; | |
}, | |
contents: function(element) { | |
return element.childNodes || []; | |
}, | |
append: function(element, node) { | |
forEach(new JQLite(node), function(child){ | |
if (element.nodeType === 1 || element.nodeType === 11) { | |
element.appendChild(child); | |
} | |
}); | |
}, | |
prepend: function(element, node) { | |
if (element.nodeType === 1) { | |
var index = element.firstChild; | |
forEach(new JQLite(node), function(child){ | |
element.insertBefore(child, index); | |
}); | |
} | |
}, | |
wrap: function(element, wrapNode) { | |
wrapNode = jqLite(wrapNode)[0]; | |
var parent = element.parentNode; | |
if (parent) { | |
parent.replaceChild(wrapNode, element); | |
} | |
wrapNode.appendChild(element); | |
}, | |
remove: function(element) { | |
JQLiteDealoc(element); | |
var parent = element.parentNode; | |
if (parent) parent.removeChild(element); | |
}, | |
after: function(element, newElement) { | |
var index = element, parent = element.parentNode; | |
forEach(new JQLite(newElement), function(node){ | |
parent.insertBefore(node, index.nextSibling); | |
index = node; | |
}); | |
}, | |
addClass: JQLiteAddClass, | |
removeClass: JQLiteRemoveClass, | |
toggleClass: function(element, selector, condition) { | |
if (isUndefined(condition)) { | |
condition = !JQLiteHasClass(element, selector); | |
} | |
(condition ? JQLiteAddClass : JQLiteRemoveClass)(element, selector); | |
}, | |
parent: function(element) { | |
var parent = element.parentNode; | |
return parent && parent.nodeType !== 11 ? parent : null; | |
}, | |
next: function(element) { | |
if (element.nextElementSibling) { | |
return element.nextElementSibling; | |
} | |
// IE8 doesn't have nextElementSibling | |
var elm = element.nextSibling; | |
while (elm != null && elm.nodeType !== 1) { | |
elm = elm.nextSibling; | |
} | |
return elm; | |
}, | |
find: function(element, selector) { | |
return element.getElementsByTagName(selector); | |
}, | |
clone: JQLiteClone, | |
triggerHandler: function(element, eventName, eventData) { | |
var eventFns = (JQLiteExpandoStore(element, 'events') || {})[eventName]; | |
eventData = eventData || []; | |
var event = [{ | |
preventDefault: noop, | |
stopPropagation: noop | |
}]; | |
forEach(eventFns, function(fn) { | |
fn.apply(element, event.concat(eventData)); | |
}); | |
} | |
}, function(fn, name){ | |
/** | |
* chaining functions | |
*/ | |
JQLite.prototype[name] = function(arg1, arg2, arg3) { | |
var value; | |
for(var i=0; i < this.length; i++) { | |
if (value == undefined) { | |
value = fn(this[i], arg1, arg2, arg3); | |
if (value !== undefined) { | |
// any function which returns a value needs to be wrapped | |
value = jqLite(value); | |
} | |
} else { | |
JQLiteAddNodes(value, fn(this[i], arg1, arg2, arg3)); | |
} | |
} | |
return value == undefined ? this : value; | |
}; | |
// bind legacy bind/unbind to on/off | |
JQLite.prototype.bind = JQLite.prototype.on; | |
JQLite.prototype.unbind = JQLite.prototype.off; | |
}); | |
/** | |
* Computes a hash of an 'obj'. | |
* Hash of a: | |
* string is string | |
* number is number as string | |
* object is either result of calling $$hashKey function on the object or uniquely generated id, | |
* that is also assigned to the $$hashKey property of the object. | |
* | |
* @param obj | |
* @returns {string} hash string such that the same input will have the same hash string. | |
* The resulting string key is in 'type:hashKey' format. | |
*/ | |
function hashKey(obj) { | |
var objType = typeof obj, | |
key; | |
if (objType == 'object' && obj !== null) { | |
if (typeof (key = obj.$$hashKey) == 'function') { | |
// must invoke on object to keep the right this | |
key = obj.$$hashKey(); | |
} else if (key === undefined) { | |
key = obj.$$hashKey = nextUid(); | |
} | |
} else { | |
key = obj; | |
} | |
return objType + ':' + key; | |
} | |
/** | |
* HashMap which can use objects as keys | |
*/ | |
function HashMap(array){ | |
forEach(array, this.put, this); | |
} | |
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)] = value; | |
}, | |
/** | |
* @param key | |
* @returns the value for the key | |
*/ | |
get: function(key) { | |
return this[hashKey(key)]; | |
}, | |
/** | |
* Remove the key/value pair | |
* @param key | |
*/ | |
remove: function(key) { | |
var value = this[key = hashKey(key)]; | |
delete this[key]; | |
return value; | |
} | |
}; | |
/** | |
* @ngdoc function | |
* @name angular.injector | |
* @function | |
* | |
* @description | |
* Creates an injector function that can be used for retrieving services as well as for | |
* dependency injection (see {@link guide/di dependency injection}). | |
* | |
* @param {Array.<string|Function>} modules A list of module functions or their aliases. See | |
* {@link angular.module}. The `ng` module must be explicitly added. | |
* @returns {function()} Injector function. See {@link AUTO.$injector $injector}. | |
* | |
* @example | |
* Typical usage | |
* <pre> | |
* // create an injector | |
* var $injector = angular.injector(['ng']); | |
* | |
* // use the injector to kick off your application | |
* // use the type inference to auto inject arguments, or use implicit injection | |
* $injector.invoke(function($rootScope, $compile, $document){ | |
* $compile($document)($rootScope); | |
* $rootScope.$digest(); | |
* }); | |
* </pre> | |
*/ | |
/** | |
* @ngdoc overview | |
* @name AUTO | |
* @description | |
* | |
* Implicit module which gets automatically added to each {@link AUTO.$injector $injector}. | |
*/ | |
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; | |
var FN_ARG_SPLIT = /,/; | |
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; | |
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; | |
var $injectorMinErr = minErr('$injector'); | |
function annotate(fn) { | |
var $inject, | |
fnText, | |
argDecl, | |
last; | |
if (typeof fn == 'function') { | |
if (!($inject = fn.$inject)) { | |
$inject = []; | |
if (fn.length) { | |
fnText = fn.toString().replace(STRIP_COMMENTS, ''); | |
argDecl = fnText.match(FN_ARGS); | |
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ | |
arg.replace(FN_ARG, function(all, underscore, name){ | |
$inject.push(name); | |
}); | |
}); | |
} | |
fn.$inject = $inject; | |
} | |
} else if (isArray(fn)) { | |
last = fn.length - 1; | |
assertArgFn(fn[last], 'fn'); | |
$inject = fn.slice(0, last); | |
} else { | |
assertArgFn(fn, 'fn', true); | |
} | |
return $inject; | |
} | |
/////////////////////////////////////// | |
/** | |
* @ngdoc object | |
* @name AUTO.$injector | |
* @function | |
* | |
* @description | |
* | |
* `$injector` is used to retrieve object instances as defined by | |
* {@link AUTO.$provide provider}, instantiate types, invoke methods, | |
* and load modules. | |
* | |
* The following always holds true: | |
* | |
* <pre> | |
* var $injector = angular.injector(); | |
* expect($injector.get('$injector')).toBe($injector); | |
* expect($injector.invoke(function($injector){ | |
* return $injector; | |
* }).toBe($injector); | |
* </pre> | |
* | |
* # Injection Function Annotation | |
* | |
* JavaScript does not have annotations, and annotations are needed for dependency injection. The | |
* following are all valid ways of annotating function with injection arguments and are equivalent. | |
* | |
* <pre> | |
* // inferred (only works if code not minified/obfuscated) | |
* $injector.invoke(function(serviceA){}); | |
* | |
* // annotated | |
* function explicit(serviceA) {}; | |
* explicit.$inject = ['serviceA']; | |
* $injector.invoke(explicit); | |
* | |
* // inline | |
* $injector.invoke(['serviceA', function(serviceA){}]); | |
* </pre> | |
* | |
* ## Inference | |
* | |
* In JavaScript calling `toString()` on a function returns the function definition. The definition can then be | |
* parsed and the function arguments can be extracted. *NOTE:* This does not work with minification, and obfuscation | |
* tools since these tools change the argument names. | |
* | |
* ## `$inject` Annotation | |
* By adding a `$inject` property onto a function the injection parameters can be specified. | |
* | |
* ## Inline | |
* As an array of injection names, where the last item in the array is the function to call. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$injector#get | |
* @methodOf AUTO.$injector | |
* | |
* @description | |
* Return an instance of the service. | |
* | |
* @param {string} name The name of the instance to retrieve. | |
* @return {*} The instance. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$injector#invoke | |
* @methodOf AUTO.$injector | |
* | |
* @description | |
* Invoke the method and supply the method arguments from the `$injector`. | |
* | |
* @param {!function} fn The function to invoke. Function parameters are injected according to the | |
* {@link guide/di $inject Annotation} rules. | |
* @param {Object=} self The `this` for the invoked method. | |
* @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before | |
* the `$injector` is consulted. | |
* @returns {*} the value returned by the invoked `fn` function. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$injector#has | |
* @methodOf AUTO.$injector | |
* | |
* @description | |
* Allows the user to query if the particular service exist. | |
* | |
* @param {string} Name of the service to query. | |
* @returns {boolean} returns true if injector has given service. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$injector#instantiate | |
* @methodOf AUTO.$injector | |
* @description | |
* Create a new instance of JS type. The method takes a constructor function invokes the new operator and supplies | |
* all of the arguments to the constructor function as specified by the constructor annotation. | |
* | |
* @param {function} Type Annotated constructor function. | |
* @param {Object=} locals Optional object. If preset then any argument names are read from this object first, before | |
* the `$injector` is consulted. | |
* @returns {Object} new instance of `Type`. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$injector#annotate | |
* @methodOf AUTO.$injector | |
* | |
* @description | |
* Returns an array of service names which the function is requesting for injection. This API is used by the injector | |
* to determine which services need to be injected into the function when the function is invoked. There are three | |
* ways in which the function can be annotated with the needed dependencies. | |
* | |
* # Argument names | |
* | |
* The simplest form is to extract the dependencies from the arguments of the function. This is done by converting | |
* the function into a string using `toString()` method and extracting the argument names. | |
* <pre> | |
* // Given | |
* function MyController($scope, $route) { | |
* // ... | |
* } | |
* | |
* // Then | |
* expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); | |
* </pre> | |
* | |
* This method does not work with code minification / obfuscation. For this reason the following annotation strategies | |
* are supported. | |
* | |
* # The `$inject` property | |
* | |
* If a function has an `$inject` property and its value is an array of strings, then the strings represent names of | |
* services to be injected into the function. | |
* <pre> | |
* // Given | |
* var MyController = function(obfuscatedScope, obfuscatedRoute) { | |
* // ... | |
* } | |
* // Define function dependencies | |
* MyController.$inject = ['$scope', '$route']; | |
* | |
* // Then | |
* expect(injector.annotate(MyController)).toEqual(['$scope', '$route']); | |
* </pre> | |
* | |
* # The array notation | |
* | |
* It is often desirable to inline Injected functions and that's when setting the `$inject` property is very | |
* inconvenient. In these situations using the array notation to specify the dependencies in a way that survives | |
* minification is a better choice: | |
* | |
* <pre> | |
* // We wish to write this (not minification / obfuscation safe) | |
* injector.invoke(function($compile, $rootScope) { | |
* // ... | |
* }); | |
* | |
* // We are forced to write break inlining | |
* var tmpFn = function(obfuscatedCompile, obfuscatedRootScope) { | |
* // ... | |
* }; | |
* tmpFn.$inject = ['$compile', '$rootScope']; | |
* injector.invoke(tmpFn); | |
* | |
* // To better support inline function the inline annotation is supported | |
* injector.invoke(['$compile', '$rootScope', function(obfCompile, obfRootScope) { | |
* // ... | |
* }]); | |
* | |
* // Therefore | |
* expect(injector.annotate( | |
* ['$compile', '$rootScope', function(obfus_$compile, obfus_$rootScope) {}]) | |
* ).toEqual(['$compile', '$rootScope']); | |
* </pre> | |
* | |
* @param {function|Array.<string|Function>} fn Function for which dependent service names need to be retrieved as described | |
* above. | |
* | |
* @returns {Array.<string>} The names of the services which the function requires. | |
*/ | |
/** | |
* @ngdoc object | |
* @name AUTO.$provide | |
* | |
* @description | |
* | |
* The {@link AUTO.$provide $provide} service has a number of methods for registering components with | |
* the {@link AUTO.$injector $injector}. Many of these functions are also exposed on {@link angular.Module}. | |
* | |
* An Angular **service** is a singleton object created by a **service factory**. These **service | |
* factories** are functions which, in turn, are created by a **service provider**. | |
* The **service providers** are constructor functions. When instantiated they must contain a property | |
* called `$get`, which holds the **service factory** function. | |
* | |
* When you request a service, the {@link AUTO.$injector $injector} is responsible for finding the | |
* correct **service provider**, instantiating it and then calling its `$get` **service factory** | |
* function to get the instance of the **service**. | |
* | |
* Often services have no configuration options and there is no need to add methods to the service | |
* provider. The provider will be no more than a constructor function with a `$get` property. For | |
* 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.$injector $injector} | |
* * {@link AUTO.$provide#constant constant(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 | |
* services, not providers. | |
* * {@link AUTO.$provide#factory factory(fn)} - registers a service **factory function**, `fn`, 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` that | |
* will be wrapped in a **service provider** object, whose `$get` property will instantiate a new | |
* object using the given constructor function. | |
* | |
* See the individual methods for more information and examples. | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#provider | |
* @methodOf AUTO.$provide | |
* @description | |
* | |
* Register a **provider function** with the {@link AUTO.$injector $injector}. Provider functions are | |
* constructor functions, whose instances are responsible for "providing" a factory for a service. | |
* | |
* Service provider names start with the name of the service they provide followed by `Provider`. | |
* For example, the {@link ng.$log $log} service has a provider called {@link ng.$logProvider $logProvider}. | |
* | |
* Service provider objects can have additional methods which allow configuration of the provider and | |
* its service. Importantly, you can configure what kind of service is created by the `$get` method, | |
* or how that service will act. For example, the {@link ng.$logProvider $logProvider} has a method | |
* {@link ng.$logProvider#debugEnabled debugEnabled} | |
* which lets you specify whether the {@link ng.$log $log} service will log debug messages to the | |
* console or not. | |
* | |
* @param {string} name The name of the instance. NOTE: the provider will be available under `name + 'Provider'` key. | |
* @param {(Object|function())} provider If the provider is: | |
* | |
* - `Object`: then it should have a `$get` method. The `$get` method will be invoked using | |
* {@link AUTO.$injector#invoke $injector.invoke()} when an instance needs to be created. | |
* - `Constructor`: a new instance of the provider will be created using | |
* {@link AUTO.$injector#instantiate $injector.instantiate()}, then treated as `object`. | |
* | |
* @returns {Object} registered provider instance | |
* @example | |
* | |
* The following example shows how to create a simple event tracking service and register it using | |
* {@link AUTO.$provide#provider $provide.provider()}. | |
* | |
* <pre> | |
* // Define the eventTracker provider | |
* function EventTrackerProvider() { | |
* var trackingUrl = '/track'; | |
* | |
* // A provider method for configuring where the tracked events should been saved | |
* this.setTrackingUrl = function(url) { | |
* trackingUrl = url; | |
* }; | |
* | |
* // The service factory function | |
* this.$get = ['$http', function($http) { | |
* var trackedEvents = {}; | |
* return { | |
* // Call this to track an event | |
* event: function(event) { | |
* var count = trackedEvents[event] || 0; | |
* count += 1; | |
* trackedEvents[event] = count; | |
* return count; | |
* }, | |
* // Call this to save the tracked events to the trackingUrl | |
* save: function() { | |
* $http.post(trackingUrl, trackedEvents); | |
* } | |
* }; | |
* }]; | |
* } | |
* | |
* describe('eventTracker', function() { | |
* var postSpy; | |
* | |
* beforeEach(module(function($provide) { | |
* // Register the eventTracker provider | |
* $provide.provider('eventTracker', EventTrackerProvider); | |
* })); | |
* | |
* beforeEach(module(function(eventTrackerProvider) { | |
* // Configure eventTracker provider | |
* eventTrackerProvider.setTrackingUrl('/custom-track'); | |
* })); | |
* | |
* it('tracks events', inject(function(eventTracker) { | |
* expect(eventTracker.event('login')).toEqual(1); | |
* expect(eventTracker.event('login')).toEqual(2); | |
* })); | |
* | |
* it('saves to the tracking url', inject(function(eventTracker, $http) { | |
* postSpy = spyOn($http, 'post'); | |
* eventTracker.event('login'); | |
* eventTracker.save(); | |
* expect(postSpy).toHaveBeenCalled(); | |
* expect(postSpy.mostRecentCall.args[0]).not.toEqual('/track'); | |
* expect(postSpy.mostRecentCall.args[0]).toEqual('/custom-track'); | |
* expect(postSpy.mostRecentCall.args[1]).toEqual({ 'login': 1 }); | |
* })); | |
* }); | |
* </pre> | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#factory | |
* @methodOf AUTO.$provide | |
* @description | |
* | |
* Register a **service factory**, which will be called to return the service instance. | |
* This is short for registering a service where its provider consists of only a `$get` property, | |
* which is the given service factory function. | |
* You should use {@link AUTO.$provide#factory $provide.factor(getFn)} if you do not need to configure | |
* your service in a provider. | |
* | |
* @param {string} name The name of the instance. | |
* @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand for | |
* `$provide.provider(name, {$get: $getFn})`. | |
* @returns {Object} registered provider instance | |
* | |
* @example | |
* Here is an example of registering a service | |
* <pre> | |
* $provide.factory('ping', ['$http', function($http) { | |
* return function ping() { | |
* return $http.send('/ping'); | |
* }; | |
* }]); | |
* </pre> | |
* You would then inject and use this service like this: | |
* <pre> | |
* someModule.controller('Ctrl', ['ping', function(ping) { | |
* ping(); | |
* }]); | |
* </pre> | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#service | |
* @methodOf AUTO.$provide | |
* @description | |
* | |
* 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. | |
* | |
* You should use {@link AUTO.$provide#service $provide.service(class)} if you define your service | |
* as a type/class. This is common when using {@link http://coffeescript.org CoffeeScript}. | |
* | |
* @param {string} name The name of the instance. | |
* @param {Function} constructor A class (constructor function) that will be instantiated. | |
* @returns {Object} registered provider instance | |
* | |
* @example | |
* Here is an example of registering a service using {@link AUTO.$provide#service $provide.service(class)} | |
* that is defined as a CoffeeScript class. | |
* <pre> | |
* class Ping | |
* constructor: (@$http)-> | |
* send: ()=> | |
* @$http.get('/ping') | |
* | |
* $provide.service('ping', ['$http', Ping]) | |
* </pre> | |
* You would then inject and use this service like this: | |
* <pre> | |
* someModule.controller 'Ctrl', ['ping', (ping)-> | |
* ping.send() | |
* ] | |
* </pre> | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#value | |
* @methodOf AUTO.$provide | |
* @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 provider's | |
* `$get` property is a factory function that takes no arguments and returns the **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}. | |
* | |
* @param {string} name The name of the instance. | |
* @param {*} value The value. | |
* @returns {Object} registered provider instance | |
* | |
* @example | |
* Here are some examples of creating value services. | |
* <pre> | |
* $provide.constant('ADMIN_USER', 'admin'); | |
* | |
* $provide.constant('RoleLookup', { admin: 0, writer: 1, reader: 2 }); | |
* | |
* $provide.constant('halfOf', function(value) { | |
* return value / 2; | |
* }); | |
* </pre> | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#constant | |
* @methodOf AUTO.$provide | |
* @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 injected | |
* into a module configuration function (see {@link angular.Module#config}) and it cannot be | |
* overridden by an Angular {@link AUTO.$provide#decorator decorator}. | |
* | |
* @param {string} name The name of the constant. | |
* @param {*} value The constant value. | |
* @returns {Object} registered instance | |
* | |
* @example | |
* Here a some examples of creating constants: | |
* <pre> | |
* $provide.constant('SHARD_HEIGHT', 306); | |
* | |
* $provide.constant('MY_COLOURS', ['red', 'blue', 'grey']); | |
* | |
* $provide.constant('double', function(value) { | |
* return value * 2; | |
* }); | |
* </pre> | |
*/ | |
/** | |
* @ngdoc method | |
* @name AUTO.$provide#decorator | |
* @methodOf AUTO.$provide | |
* @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. | |
* | |
* @param {string} name The name of the service to decorate. | |
* @param {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 | |
* 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, | |
* decorated or delegated to. | |
* | |
* @example | |
* Here we decorate the {@link ng.$log $log} service to convert warnings to errors by intercepting | |
* calls to {@link ng.$log#error $log.warn()}. | |
* <pre> | |
* $provider.decorator('$log', ['$delegate', function($delegate) { | |
* $delegate.warn = $delegate.error; | |
* return $delegate; | |
* }]); | |
* </pre> | |
*/ | |
function createInjector(modulesToLoad) { | |
var INSTANTIATING = {}, | |
providerSuffix = 'Provider', | |
path = [], | |
loadedModules = new HashMap(), | |
providerCache = { | |
$provide: { | |
copy: copy, | |
provider: supportObject(provider), | |
factory: supportObject(factory), | |
service: supportObject(service), | |
value: supportObject(value), | |
constant: supportObject(constant), | |
decorator: decorator | |
} | |
}, | |
providerInjector = (providerCache.$injector = | |
createInternalInjector(providerCache, function() { | |
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- ')); | |
})), | |
instanceCache = {}, | |
instanceInjector = (instanceCache.$injector = | |
createInternalInjector(instanceCache, function(servicename) { | |
var provider = providerInjector.get(servicename + providerSuffix); | |
return instanceInjector.invoke(provider.$get, provider); | |
})); | |
forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); }); | |
return instanceInjector; | |
//////////////////////////////////// | |
// $provider | |
//////////////////////////////////// | |
function supportObject(delegate) { | |
return function(key, value) { | |
if (isObject(key)) { | |
forEach(key, reverseParams(delegate)); | |
} else { | |
return delegate(key, value); | |
} | |
} | |
} | |
function provider(name, provider_) { | |
assertNotHasOwnProperty(name, 'service'); | |
if (isFunction(provider_) || isArray(provider_)) { | |
provider_ = providerInjector.instantiate(provider_); | |
} | |
if (!provider_.$get) { | |
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); | |
} | |
return providerCache[name + providerSuffix] = provider_; | |
} | |
function factory(name, factoryFn) { return provider(name, { $get: factoryFn }); } | |
function service(name, constructor) { | |
return factory(name, ['$injector', function($injector) { | |
return $injector.instantiate(constructor); | |
}]); | |
} | |
function value(name, value) { return factory(name, valueFn(value)); } | |
function constant(name, value) { | |
assertNotHasOwnProperty(name, 'constant'); | |
providerCache[name] = value; | |
instanceCache[name] = value; | |
} | |
function decorator(serviceName, decorFn) { | |
var origProvider = providerInjector.get(serviceName + providerSuffix), | |
orig$get = origProvider.$get; | |
origProvider.$get = function() { | |
var origInstance = instanceInjector.invoke(orig$get, origProvider); | |
return instanceInjector.invoke(decorFn, null, {$delegate: origInstance}); | |
}; | |
} | |
function copy(newServiceName, oldServiceName, copyFn) { | |
var origProvider = providerInjector.get(oldServiceName + providerSuffix), | |
orig$get = origProvider.$get; | |
factory(newServiceName, copyFn(orig$get)); | |
} | |
//////////////////////////////////// | |
// Module Loading | |
//////////////////////////////////// | |
function loadModules(modulesToLoad){ | |
var runBlocks = []; | |
forEach(modulesToLoad, function(module) { | |
if (loadedModules.get(module)) return; | |
loadedModules.put(module, true); | |
try { | |
if (isString(module)) { | |
var moduleFn = angularModule(module); | |
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks); | |
for(var invokeQueue = moduleFn._invokeQueue, i = 0, ii = invokeQueue.length; i < ii; i++) { | |
var invokeArgs = invokeQueue[i], | |
provider = providerInjector.get(invokeArgs[0]); | |
provider[invokeArgs[1]].apply(provider, invokeArgs[2]); | |
} | |
} else if (isFunction(module)) { | |
runBlocks.push(providerInjector.invoke(module)); | |
} else if (isArray(module)) { | |
runBlocks.push(providerInjector.invoke(module)); | |
} else { | |
assertArgFn(module, 'module'); | |
} | |
} catch (e) { | |
if (isArray(module)) { | |
module = module[module.length - 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. | |
e = e.message + '\n' + e.stack; | |
} | |
throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}", module, e.stack || e.message || e); | |
} | |
}); | |
return runBlocks; | |
} | |
//////////////////////////////////// | |
// internal Injector | |
//////////////////////////////////// | |
function createInternalInjector(cache, factory) { | |
function getService(serviceName) { | |
if (cache.hasOwnProperty(serviceName)) { | |
if (cache[serviceName] === INSTANTIATING) { | |
throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); | |
} | |
return cache[serviceName]; | |
} else { | |
try { | |
path.unshift(serviceName); | |
cache[serviceName] = INSTANTIATING; | |
return cache[serviceName] = factory(serviceName); | |
} finally { | |
path.shift(); | |
} | |
} | |
} | |
function invoke(fn, self, locals){ | |
var args = [], | |
$inject = annotate(fn), | |
length, i, | |
key; | |
for(i = 0, length = $inject.length; i < length; i++) { | |
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) | |
); | |
} | |
if (!fn.$inject) { | |
// this means that we must be an array. | |
fn = fn[length]; | |
} | |
// Performance optimization: http://jsperf.com/apply-vs-call-vs-invoke | |
switch (self ? -1 : args.length) { | |
case 0: return fn(); | |
case 1: return fn(args[0]); | |
case 2: return fn(args[0], args[1]); | |
case 3: return fn(args[0], args[1], args[2]); | |
case 4: return fn(args[0], args[1], args[2], args[3]); | |
case 5: return fn(args[0], args[1], args[2], args[3], args[4]); | |
case 6: return fn(args[0], args[1], args[2], args[3], args[4], args[5]); | |
case 7: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6]); | |
case 8: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]); | |
case 9: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8]); | |
case 10: return fn(args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7], args[8], args[9]); | |
default: return fn.apply(self, args); | |
} | |
} | |
function instantiate(Type, locals) { | |
var Constructor = function() {}, | |
instance, returnedValue; | |
// 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) {}]); | |
Constructor.prototype = (isArray(Type) ? Type[Type.length - 1] : Type).prototype; | |
instance = new Constructor(); | |
returnedValue = invoke(Type, instance, locals); | |
return isObject(returnedValue) ? returnedValue : instance; | |
} | |
return { | |
invoke: invoke, | |
instantiate: instantiate, | |
get: getService, | |
annotate: annotate, | |
has: function(name) { | |
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); | |
} | |
}; | |
} | |
} | |
/** | |
* @ngdoc function | |
* @name ng.$anchorScroll | |
* @requires $window | |
* @requires $location | |
* @requires $rootScope | |
* | |
* @description | |
* When called, it checks current value of `$location.hash()` and scroll to related element, | |
* according to rules specified in | |
* {@link http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document Html5 spec}. | |
* | |
* It also watches the `$location.hash()` and scrolls whenever it changes to match any anchor. | |
* This can be disabled by calling `$anchorScrollProvider.disableAutoScrolling()`. | |
* | |
* @example | |
<example> | |
<file name="index.html"> | |
<div id="scrollArea" ng-controller="ScrollCtrl"> | |
<a ng-click="gotoBottom()">Go to bottom</a> | |
<a id="bottom"></a> You're at the bottom! | |
</div> | |
</file> | |
<file name="script.js"> | |
function ScrollCtrl($scope, $location, $anchorScroll) { | |
$scope.gotoBottom = function (){ | |
// set the location.hash to the id of | |
// the element you wish to scroll to. | |
$location.hash('bottom'); | |
// call $anchorScroll() | |
$anchorScroll(); | |
} | |
} | |
</file> | |
<file name="style.css"> | |
#scrollArea { | |
height: 350px; | |
overflow: auto; | |
} | |
#bottom { | |
display: block; | |
margin-top: 2000px; | |
} | |
</file> | |
</example> | |
*/ | |
function $AnchorScrollProvider() { | |
var autoScrollingEnabled = true; | |
this.disableAutoScrolling = function() { | |
autoScrollingEnabled = false; | |
}; | |
this.$get = ['$window', '$location', '$rootScope', function($window, $location, $rootScope) { | |
var document = $window.document; | |
// helper function to get first anchor from a NodeList | |
// can't use filter.filter, as it accepts only instances of Array | |
// and IE can't convert NodeList to an array using [].slice | |
// TODO(vojta): use filter if we change it to accept lists as well | |
function getFirstAnchor(list) { | |
var result = null; | |
forEach(list, function(element) { | |
if (!result && lowercase(element.nodeName) === 'a') result = element; | |
}); | |
return result; | |
} | |
function scroll() { | |
var hash = $location.hash(), elm; | |
// empty hash, scroll to the top of the page | |
if (!hash) $window.scrollTo(0, 0); | |
// element with given id | |
else if ((elm = document.getElementById(hash))) elm.scrollIntoView(); | |
// first anchor with given name :-D | |
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) elm.scrollIntoView(); | |
// no element and hash == 'top', scroll to the top of the page | |
else if (hash === 'top') $window.scrollTo(0, 0); | |
} | |
// does not scroll when user clicks on anchor link that is currently on | |
// (no url change, no $location.hash() change), browser native does scroll | |
if (autoScrollingEnabled) { | |
$rootScope.$watch(function autoScrollWatch() {return $location.hash();}, | |
function autoScrollWatchAction() { | |
$rootScope.$evalAsync(scroll); | |
}); | |
} | |
return scroll; | |
}]; | |
} | |
var $animateMinErr = minErr('$animate'); | |
/** | |
* @ngdoc object | |
* @name ng.$animateProvider | |
* | |
* @description | |
* Default implementation of $animate that doesn't perform any animations, instead just synchronously performs DOM | |
* updates and calls done() callbacks. | |
* | |
* In order to enable animations the ngAnimate module has to be loaded. | |
* | |
* To see the functional implementation check out src/ngAnimate/animate.js | |
*/ | |
var $AnimateProvider = ['$provide', function($provide) { | |
this.$$selectors = {}; | |
/** | |
* @ngdoc function | |
* @name ng.$animateProvider#register | |
* @methodOf ng.$animateProvider | |
* | |
* @description | |
* Registers a new injectable animation factory function. The factory function produces the animation object which | |
* contains callback functions for each event that is expected to be animated. | |
* | |
* * `eventFn`: `function(Element, doneFunction)` The element to animate, the `doneFunction` must be called once the | |
* element animation is complete. If a function is returned then the animation service will use this function to | |
* cancel the animation whenever a cancel event is triggered. | |
* | |
* | |
*<pre> | |
* return { | |
* eventFn : function(element, done) { | |
* //code to run the animation | |
* //once complete, then run done() | |
* return function cancellationFunction() { | |
* //code to cancel the animation | |
* } | |
* } | |
* } | |
*</pre> | |
* | |
* @param {string} name The name of the animation. | |
* @param {function} factory The factory function that will be executed to return the animation object. | |
*/ | |
this.register = function(name, factory) { | |
var key = name + '-animation'; | |
if (name && name.charAt(0) != '.') throw $animateMinErr('notcsel', | |
"Expecting class selector starting with '.' got '{0}'.", name); | |
this.$$selectors[name.substr(1)] = key; | |
$provide.factory(key, factory); | |
}; | |
this.$get = ['$timeout', function($timeout) { | |
/** | |
* @ngdoc object | |
* @name ng.$animate | |
* | |
* @description | |
* The $animate service provides rudimentary DOM manipulation functions to insert, remove and move elements within | |
* the DOM, as well as adding and removing classes. This service is the core service used by the ngAnimate $animator | |
* service which provides high-level animation hooks for CSS and JavaScript. | |
* | |
* $animate is available in the AngularJS core, however, the ngAnimate module must be included to enable full out | |
* animation support. Otherwise, $animate will only perform simple DOM manipulation operations. | |
* | |
* To learn more about enabling animation support, click here to visit the {@link ngAnimate ngAnimate module page} | |
* as well as the {@link ngAnimate.$animate ngAnimate $animate service page}. | |
*/ | |
return { | |
/** | |
* @ngdoc function | |
* @name ng.$animate#enter | |
* @methodOf ng.$animate | |
* @function | |
* | |
* @description | |
* Inserts the element into the DOM either after the `after` element or within the `parent` element. Once complete, | |
* the done() callback will be fired (if provided). | |
* | |
* @param {jQuery/jqLite element} element the element which will be inserted into the DOM | |
* @param {jQuery/jqLite element} parent the parent element which will append the element as a child (if the after element is not present) | |
* @param {jQuery/jqLite element} after the sibling element which will append the element after itself | |
* @param {function=} done callback function that will be called after the element has been inserted into the DOM | |
*/ | |
enter : function(element, parent, after, done) { | |
var afterNode = after && after[after.length - 1]; | |
var parentNode = parent && parent[0] || afterNode && afterNode.parentNode; | |
// IE does not like undefined so we have to pass null. | |
var afterNextSibling = (afterNode && afterNode.nextSibling) || null; | |
forEach(element, function(node) { | |
parentNode.insertBefore(node, afterNextSibling); | |
}); | |
done && $timeout(done, 0, false); | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$animate#leave | |
* @methodOf ng.$animate | |
* @function | |
* | |
* @description | |
* Removes the element from the DOM. Once complete, the done() callback will be fired (if provided). | |
* | |
* @param {jQuery/jqLite element} element the element which will be removed from the DOM | |
* @param {function=} done callback function that will be called after the element has been removed from the DOM | |
*/ | |
leave : function(element, done) { | |
element.remove(); | |
done && $timeout(done, 0, false); | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$animate#move | |
* @methodOf ng.$animate | |
* @function | |
* | |
* @description | |
* Moves the position of the provided element within the DOM to be placed either after the `after` element or inside of the `parent` element. | |
* Once complete, the done() callback will be fired (if provided). | |
* | |
* @param {jQuery/jqLite element} element the element which will be moved around within the DOM | |
* @param {jQuery/jqLite element} parent the parent element where the element will be inserted into (if the after element is not present) | |
* @param {jQuery/jqLite element} after the sibling element where the element will be positioned next to | |
* @param {function=} done the callback function (if provided) that will be fired after the element has been moved to its new position | |
*/ | |
move : function(element, parent, after, done) { | |
// Do not remove element before insert. Removing will cause data associated with the | |
// element to be dropped. Insert will implicitly do the remove. | |
this.enter(element, parent, after, done); | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$animate#addClass | |
* @methodOf ng.$animate | |
* @function | |
* | |
* @description | |
* Adds the provided className CSS class value to the provided element. Once complete, the done() callback will be fired (if provided). | |
* | |
* @param {jQuery/jqLite element} element the element which will have the className value added to it | |
* @param {string} className the CSS class which will be added to the element | |
* @param {function=} done the callback function (if provided) that will be fired after the className value has been added to the element | |
*/ | |
addClass : function(element, className, done) { | |
className = isString(className) ? | |
className : | |
isArray(className) ? className.join(' ') : ''; | |
forEach(element, function (element) { | |
JQLiteAddClass(element, className); | |
}); | |
done && $timeout(done, 0, false); | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$animate#removeClass | |
* @methodOf ng.$animate | |
* @function | |
* | |
* @description | |
* Removes the provided className CSS class value from the provided element. Once complete, the done() callback will be fired (if provided). | |
* | |
* @param {jQuery/jqLite element} element the element which will have the className value removed from it | |
* @param {string} className the CSS class which will be removed from the element | |
* @param {function=} done the callback function (if provided) that will be fired after the className value has been removed from the element | |
*/ | |
removeClass : function(element, className, done) { | |
className = isString(className) ? | |
className : | |
isArray(className) ? className.join(' ') : ''; | |
forEach(element, function (element) { | |
JQLiteRemoveClass(element, className); | |
}); | |
done && $timeout(done, 0, false); | |
}, | |
enabled : noop | |
}; | |
}]; | |
}]; | |
/** | |
* ! This is a private undocumented service ! | |
* | |
* @name ng.$browser | |
* @requires $log | |
* @description | |
* This object has two goals: | |
* | |
* - hide all the global state in the browser caused by the window object | |
* - abstract away all the browser specific features and inconsistencies | |
* | |
* For tests we provide {@link ngMock.$browser mock implementation} of the `$browser` | |
* service, which can be used for convenient testing of the application without the interaction with | |
* the real browser apis. | |
*/ | |
/** | |
* @param {object} window The global window object. | |
* @param {object} document jQuery wrapped document. | |
* @param {function()} XHR XMLHttpRequest constructor. | |
* @param {object} $log console.log or an object with the same interface. | |
* @param {object} $sniffer $sniffer service | |
*/ | |
function Browser(window, document, $log, $sniffer) { | |
var self = this, | |
rawDocument = document[0], | |
location = window.location, | |
history = window.history, | |
setTimeout = window.setTimeout, | |
clearTimeout = window.clearTimeout, | |
pendingDeferIds = {}; | |
self.isMock = false; | |
var outstandingRequestCount = 0; | |
var outstandingRequestCallbacks = []; | |
// TODO(vojta): remove this temporary api | |
self.$$completeOutstandingRequest = completeOutstandingRequest; | |
self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; }; | |
/** | |
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` | |
* counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. | |
*/ | |
function completeOutstandingRequest(fn) { | |
try { | |
fn.apply(null, sliceArgs(arguments, 1)); | |
} finally { | |
outstandingRequestCount--; | |
if (outstandingRequestCount === 0) { | |
while(outstandingRequestCallbacks.length) { | |
try { | |
outstandingRequestCallbacks.pop()(); | |
} catch (e) { | |
$log.error(e); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* @private | |
* Note: this method is used only by scenario runner | |
* TODO(vojta): prefix this method with $$ ? | |
* @param {function()} callback Function that will be called when no outstanding request | |
*/ | |
self.notifyWhenNoOutstandingRequests = function(callback) { | |
// force browser to execute all pollFns - this is needed so that cookies and other pollers fire | |
// at some deterministic time in respect to the test runner's actions. Leaving things up to the | |
// regular poller would result in flaky tests. | |
forEach(pollFns, function(pollFn){ pollFn(); }); | |
if (outstandingRequestCount === 0) { | |
callback(); | |
} else { | |
outstandingRequestCallbacks.push(callback); | |
} | |
}; | |
////////////////////////////////////////////////////////////// | |
// Poll Watcher API | |
////////////////////////////////////////////////////////////// | |
var pollFns = [], | |
pollTimeout; | |
/** | |
* @name ng.$browser#addPollFn | |
* @methodOf ng.$browser | |
* | |
* @param {function()} fn Poll function to add | |
* | |
* @description | |
* Adds a function to the list of functions that poller periodically executes, | |
* and starts polling if not started yet. | |
* | |
* @returns {function()} the added function | |
*/ | |
self.addPollFn = function(fn) { | |
if (isUndefined(pollTimeout)) startPoller(100, setTimeout); | |
pollFns.push(fn); | |
return fn; | |
}; | |
/** | |
* @param {number} interval How often should browser call poll functions (ms) | |
* @param {function()} setTimeout Reference to a real or fake `setTimeout` function. | |
* | |
* @description | |
* Configures the poller to run in the specified intervals, using the specified | |
* setTimeout fn and kicks it off. | |
*/ | |
function startPoller(interval, setTimeout) { | |
(function check() { | |
forEach(pollFns, function(pollFn){ pollFn(); }); | |
pollTimeout = setTimeout(check, interval); | |
})(); | |
} | |
////////////////////////////////////////////////////////////// | |
// URL API | |
////////////////////////////////////////////////////////////// | |
var lastBrowserUrl = location.href, | |
baseElement = document.find('base'), | |
newLocation = null; | |
/** | |
* @name ng.$browser#url | |
* @methodOf ng.$browser | |
* | |
* @description | |
* GETTER: | |
* Without any argument, this method just returns current value of location.href. | |
* | |
* SETTER: | |
* With at least one argument, this method sets url to new value. | |
* If html5 history api supported, pushState/replaceState is used, otherwise | |
* location.href/location.replace is used. | |
* Returns its own instance to allow chaining | |
* | |
* NOTE: this api is intended for use only by the $location service. Please use the | |
* {@link ng.$location $location service} to change url. | |
* | |
* @param {string} url New url (when used as setter) | |
* @param {boolean=} replace Should new url replace current history record ? | |
*/ | |
self.url = function(url, replace) { | |
// Android Browser BFCache causes location reference to become stale. | |
if (location !== window.location) location = window.location; | |
// setter | |
if (url) { | |
if (lastBrowserUrl == url) return; | |
lastBrowserUrl = url; | |
if ($sniffer.history) { | |
if (replace) history.replaceState(null, '', url); | |
else { | |
history.pushState(null, '', url); | |
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462 | |
baseElement.attr('href', baseElement.attr('href')); | |
} | |
} else { | |
newLocation = url; | |
if (replace) { | |
location.replace(url); | |
} else { | |
location.href = url; | |
} | |
} | |
return self; | |
// getter | |
} else { | |
// - newLocation is a workaround for an IE7-9 issue with location.replace and location.href | |
// methods not updating location.href synchronously. | |
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 | |
return newLocation || location.href.replace(/%27/g,"'"); | |
} | |
}; | |
var urlChangeListeners = [], | |
urlChangeInit = false; | |
function fireUrlChange() { | |
newLocation = null; | |
if (lastBrowserUrl == self.url()) return; | |
lastBrowserUrl = self.url(); | |
forEach(urlChangeListeners, function(listener) { | |
listener(self.url()); | |
}); | |
} | |
/** | |
* @name ng.$browser#onUrlChange | |
* @methodOf ng.$browser | |
* @TODO(vojta): refactor to use node's syntax for events | |
* | |
* @description | |
* Register callback function that will be called, when url changes. | |
* | |
* It's only called when the url is changed by outside of angular: | |
* - user types different url into address bar | |
* - user clicks on history (forward/back) button | |
* - user clicks on a link | |
* | |
* It's not called when url is changed by $browser.url() method | |
* | |
* The listener gets called with new url as parameter. | |
* | |
* NOTE: this api is intended for use only by the $location service. Please use the | |
* {@link ng.$location $location service} to monitor url changes in angular apps. | |
* | |
* @param {function(string)} listener Listener function to be called when url changes. | |
* @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. | |
*/ | |
self.onUrlChange = function(callback) { | |
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 | |
// changed by push/replaceState | |
// html5 history api - popstate event | |
if ($sniffer.history) jqLite(window).on('popstate', fireUrlChange); | |
// hashchange event | |
if ($sniffer.hashchange) jqLite(window).on('hashchange', fireUrlChange); | |
// polling | |
else self.addPollFn(fireUrlChange); | |
urlChangeInit = true; | |
} | |
urlChangeListeners.push(callback); | |
return callback; | |
}; | |
////////////////////////////////////////////////////////////// | |
// Misc API | |
////////////////////////////////////////////////////////////// | |
/** | |
* @name ng.$browser#baseHref | |
* @methodOf ng.$browser | |
* | |
* @description | |
* Returns current <base href> | |
* (always relative - without domain) | |
* | |
* @returns {string=} current <base href> | |
*/ | |
self.baseHref = function() { | |
var href = baseElement.attr('href'); | |
return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : ''; | |
}; | |
////////////////////////////////////////////////////////////// | |
// Cookies API | |
////////////////////////////////////////////////////////////// | |
var lastCookies = {}; | |
var lastCookieString = ''; | |
var cookiePath = self.baseHref(); | |
/** | |
* @name ng.$browser#cookies | |
* @methodOf ng.$browser | |
* | |
* @param {string=} name Cookie name | |
* @param {string=} value Cookie value | |
* | |
* @description | |
* The cookies method provides a 'private' low level access to browser cookies. | |
* It is not meant to be used directly, use the $cookie service instead. | |
* | |
* The return values vary depending on the arguments that the method was called with as follows: | |
* <ul> | |
* <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li> | |
* <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li> | |
* <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li> | |
* </ul> | |
* | |
* @returns {Object} Hash of all cookies (if called without any parameter) | |
*/ | |
self.cookies = function(name, value) { | |
var cookieLength, cookieArray, cookie, i, index; | |
if (name) { | |
if (value === undefined) { | |
rawDocument.cookie = escape(name) + "=;path=" + cookiePath + ";expires=Thu, 01 Jan 1970 00:00:00 GMT"; | |
} else { | |
if (isString(value)) { | |
cookieLength = (rawDocument.cookie = escape(name) + '=' + escape(value) + ';path=' + cookiePath).length + 1; | |
// per http://www.ietf.org/rfc/rfc2109.txt browser must allow at minimum: | |
// - 300 cookies | |
// - 20 cookies per unique domain | |
// - 4096 bytes per cookie | |
if (cookieLength > 4096) { | |
$log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ | |
cookieLength + " > 4096 bytes)!"); | |
} | |
} | |
} | |
} else { | |
if (rawDocument.cookie !== lastCookieString) { | |
lastCookieString = rawDocument.cookie; | |
cookieArray = lastCookieString.split("; "); | |
lastCookies = {}; | |
for (i = 0; i < cookieArray.length; i++) { | |
cookie = cookieArray[i]; | |
index = cookie.indexOf('='); | |
if (index > 0) { //ignore nameless cookies | |
var name = unescape(cookie.substring(0, index)); | |
// the first value that is seen for a cookie is the most | |
// specific one. values for the same cookie name that | |
// follow are for less specific paths. | |
if (lastCookies[name] === undefined) { | |
lastCookies[name] = unescape(cookie.substring(index + 1)); | |
} | |
} | |
} | |
} | |
return lastCookies; | |
} | |
}; | |
/** | |
* @name ng.$browser#defer | |
* @methodOf ng.$browser | |
* @param {function()} fn A function, who's execution should be deferred. | |
* @param {number=} [delay=0] of milliseconds to defer the function execution. | |
* @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. | |
* | |
* @description | |
* Executes a fn asynchronously via `setTimeout(fn, delay)`. | |
* | |
* Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using | |
* `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed | |
* via `$browser.defer.flush()`. | |
* | |
*/ | |
self.defer = function(fn, delay) { | |
var timeoutId; | |
outstandingRequestCount++; | |
timeoutId = setTimeout(function() { | |
delete pendingDeferIds[timeoutId]; | |
completeOutstandingRequest(fn); | |
}, delay || 0); | |
pendingDeferIds[timeoutId] = true; | |
return timeoutId; | |
}; | |
/** | |
* @name ng.$browser#defer.cancel | |
* @methodOf ng.$browser.defer | |
* | |
* @description | |
* Cancels a deferred task identified with `deferId`. | |
* | |
* @param {*} deferId Token returned by the `$browser.defer` function. | |
* @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully canceled. | |
*/ | |
self.defer.cancel = function(deferId) { | |
if (pendingDeferIds[deferId]) { | |
delete pendingDeferIds[deferId]; | |
clearTimeout(deferId); | |
completeOutstandingRequest(noop); | |
return true; | |
} | |
return false; | |
}; | |
} | |
function $BrowserProvider(){ | |
this.$get = ['$window', '$log', '$sniffer', '$document', | |
function( $window, $log, $sniffer, $document){ | |
return new Browser($window, $document, $log, $sniffer); | |
}]; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$cacheFactory | |
* | |
* @description | |
* Factory that constructs cache objects and gives access to them. | |
* | |
* <pre> | |
* | |
* var cache = $cacheFactory('cacheId'); | |
* expect($cacheFactory.get('cacheId')).toBe(cache); | |
* expect($cacheFactory.get('noSuchCacheId')).not.toBeDefined(); | |
* | |
* cache.put("key", "value"); | |
* cache.put("another key", "another value"); | |
* | |
* expect(cache.info()).toEqual({id: 'cacheId', size: 2}); // Since we've specified no options on creation | |
* | |
* </pre> | |
* | |
* | |
* @param {string} cacheId Name or id of the newly created cache. | |
* @param {object=} options Options object that specifies the cache behavior. Properties: | |
* | |
* - `{number=}` `capacity` — turns the cache into LRU cache. | |
* | |
* @returns {object} Newly created cache object with the following set of methods: | |
* | |
* - `{object}` `info()` — Returns id, size, and options of cache. | |
* - `{{*}}` `put({string} key, {*} value)` — Puts a new key-value pair into the cache and returns it. | |
* - `{{*}}` `get({string} key)` — Returns cached value for `key` or undefined for cache miss. | |
* - `{void}` `remove({string} key)` — Removes a key-value pair from the cache. | |
* - `{void}` `removeAll()` — Removes all cached values. | |
* - `{void}` `destroy()` — Removes references to this cache from $cacheFactory. | |
* | |
*/ | |
function $CacheFactoryProvider() { | |
this.$get = function() { | |
var caches = {}; | |
function cacheFactory(cacheId, options) { | |
if (cacheId in caches) { | |
throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId); | |
} | |
var size = 0, | |
stats = extend({}, options, {id: cacheId}), | |
data = {}, | |
capacity = (options && options.capacity) || Number.MAX_VALUE, | |
lruHash = {}, | |
freshEnd = null, | |
staleEnd = null; | |
return caches[cacheId] = { | |
put: function(key, value) { | |
var lruEntry = lruHash[key] || (lruHash[key] = {key: key}); | |
refresh(lruEntry); | |
if (isUndefined(value)) return; | |
if (!(key in data)) size++; | |
data[key] = value; | |
if (size > capacity) { | |
this.remove(staleEnd.key); | |
} | |
return value; | |
}, | |
get: function(key) { | |
var lruEntry = lruHash[key]; | |
if (!lruEntry) return; | |
refresh(lruEntry); | |
return data[key]; | |
}, | |
remove: function(key) { | |
var lruEntry = lruHash[key]; | |
if (!lruEntry) return; | |
if (lruEntry == freshEnd) freshEnd = lruEntry.p; | |
if (lruEntry == staleEnd) staleEnd = lruEntry.n; | |
link(lruEntry.n,lruEntry.p); | |
delete lruHash[key]; | |
delete data[key]; | |
size--; | |
}, | |
removeAll: function() { | |
data = {}; | |
size = 0; | |
lruHash = {}; | |
freshEnd = staleEnd = null; | |
}, | |
destroy: function() { | |
data = null; | |
stats = null; | |
lruHash = null; | |
delete caches[cacheId]; | |
}, | |
info: function() { | |
return extend({}, stats, {size: size}); | |
} | |
}; | |
/** | |
* makes the `entry` the freshEnd of the LRU linked list | |
*/ | |
function refresh(entry) { | |
if (entry != freshEnd) { | |
if (!staleEnd) { | |
staleEnd = entry; | |
} else if (staleEnd == entry) { | |
staleEnd = entry.n; | |
} | |
link(entry.n, entry.p); | |
link(entry, freshEnd); | |
freshEnd = entry; | |
freshEnd.n = null; | |
} | |
} | |
/** | |
* bidirectionally links two entries of the LRU linked list | |
*/ | |
function link(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 | |
} | |
} | |
} | |
/** | |
* @ngdoc method | |
* @name ng.$cacheFactory#info | |
* @methodOf ng.$cacheFactory | |
* | |
* @description | |
* Get information about all the of the caches that have been created | |
* | |
* @returns {Object} - key-value map of `cacheId` to the result of calling `cache#info` | |
*/ | |
cacheFactory.info = function() { | |
var info = {}; | |
forEach(caches, function(cache, cacheId) { | |
info[cacheId] = cache.info(); | |
}); | |
return info; | |
}; | |
/** | |
* @ngdoc method | |
* @name ng.$cacheFactory#get | |
* @methodOf ng.$cacheFactory | |
* | |
* @description | |
* Get access to a cache object by the `cacheId` used when it was created. | |
* | |
* @param {string} cacheId Name or id of a cache to access. | |
* @returns {object} Cache object identified by the cacheId or undefined if no such cache. | |
*/ | |
cacheFactory.get = function(cacheId) { | |
return caches[cacheId]; | |
}; | |
return cacheFactory; | |
}; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$templateCache | |
* | |
* @description | |
* The first time a template is used, it is loaded in the template cache for quick retrieval. You can | |
* load templates directly into the cache in a `script` tag, or by consuming the `$templateCache` | |
* service directly. | |
* | |
* Adding via the `script` tag: | |
* <pre> | |
* <html ng-app> | |
* <head> | |
* <script type="text/ng-template" id="templateId.html"> | |
* This is the content of the template | |
* </script> | |
* </head> | |
* ... | |
* </html> | |
* </pre> | |
* | |
* **Note:** the `script` tag containing the template does not need to be included in the `head` of the document, but | |
* it must be below the `ng-app` definition. | |
* | |
* Adding via the $templateCache service: | |
* | |
* <pre> | |
* var myApp = angular.module('myApp', []); | |
* myApp.run(function($templateCache) { | |
* $templateCache.put('templateId.html', 'This is the content of the template'); | |
* }); | |
* </pre> | |
* | |
* To retrieve the template later, simply use it in your HTML: | |
* <pre> | |
* <div ng-include=" 'templateId.html' "></div> | |
* </pre> | |
* | |
* or get it via Javascript: | |
* <pre> | |
* $templateCache.get('templateId.html') | |
* </pre> | |
* | |
* See {@link ng.$cacheFactory $cacheFactory}. | |
* | |
*/ | |
function $TemplateCacheProvider() { | |
this.$get = ['$cacheFactory', function($cacheFactory) { | |
return $cacheFactory('templates'); | |
}]; | |
} | |
/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE! | |
* | |
* DOM-related variables: | |
* | |
* - "node" - DOM Node | |
* - "element" - DOM Element or Node | |
* - "$node" or "$element" - jqLite-wrapped node or element | |
* | |
* | |
* Compiler related stuff: | |
* | |
* - "linkFn" - linking fn of a single directive | |
* - "nodeLinkFn" - function that aggregates all linking fns for a particular node | |
* - "childLinkFn" - function that aggregates all linking fns for child nodes of a particular node | |
* - "compositeLinkFn" - function that aggregates all linking fns for a compilation root (nodeList) | |
*/ | |
/** | |
* @ngdoc function | |
* @name ng.$compile | |
* @function | |
* | |
* @description | |
* Compiles a piece of HTML string or DOM into a template and produces a template function, which | |
* can then be used to link {@link ng.$rootScope.Scope scope} and the template together. | |
* | |
* The compilation is a process of walking the DOM tree and trying to match DOM elements to | |
* {@link ng.$compileProvider#directive directives}. For each match it | |
* executes corresponding template function and collects the | |
* instance functions into a single template function which is then returned. | |
* | |
* The template function can then be used once to produce the view or as it is the case with | |
* {@link ng.directive:ngRepeat repeater} many-times, in which | |
* case each call results in a view that is a DOM clone of the original template. | |
* | |
<doc:example module="compile"> | |
<doc:source> | |
<script> | |
// declare a new module, and inject the $compileProvider | |
angular.module('compile', [], function($compileProvider) { | |
// configure new 'compile' directive by passing a directive | |
// factory function. The factory function injects the '$compile' | |
$compileProvider.directive('compile', function($compile) { | |
// directive factory creates a link function | |
return function(scope, element, attrs) { | |
scope.$watch( | |
function(scope) { | |
// watch the 'compile' expression for changes | |
return scope.$eval(attrs.compile); | |
}, | |
function(value) { | |
// when the 'compile' expression changes | |
// assign it into the current DOM | |
element.html(value); | |
// compile the new DOM and link it to the current | |
// scope. | |
// NOTE: we only compile .childNodes so that | |
// we don't get into infinite loop compiling ourselves | |
$compile(element.contents())(scope); | |
} | |
); | |
}; | |
}) | |
}); | |
function Ctrl($scope) { | |
$scope.name = 'Angular'; | |
$scope.html = 'Hello {{name}}'; | |
} | |
</script> | |
<div ng-controller="Ctrl"> | |
<input ng-model="name"> <br> | |
<textarea ng-model="html"></textarea> <br> | |
<div compile="html"></div> | |
</div> | |
</doc:source> | |
<doc:scenario> | |
it('should auto compile', function() { | |
expect(element('div[compile]').text()).toBe('Hello Angular'); | |
input('html').enter('{{name}}!'); | |
expect(element('div[compile]').text()).toBe('Angular!'); | |
}); | |
</doc:scenario> | |
</doc:example> | |
* | |
* | |
* @param {string|DOMElement} element Element or HTML string to compile into a template function. | |
* @param {function(angular.Scope[, cloneAttachFn]} transclude function available to directives. | |
* @param {number} maxPriority only apply directives lower then given priority (Only effects the | |
* root element(s), not their children) | |
* @returns {function(scope[, cloneAttachFn])} a link function which is used to bind template | |
* (a DOM element/tree) to a scope. Where: | |
* | |
* * `scope` - A {@link ng.$rootScope.Scope Scope} to bind to. | |
* * `cloneAttachFn` - If `cloneAttachFn` is provided, then the link function will clone the `template` | |
* and call the `cloneAttachFn` function allowing the caller to attach the | |
* cloned elements to the DOM document at the appropriate place. The `cloneAttachFn` is | |
* called as: <br> `cloneAttachFn(clonedElement, scope)` where: | |
* | |
* * `clonedElement` - is a clone of the original `element` passed into the compiler. | |
* * `scope` - is the current scope with which the linking function is working with. | |
* | |
* Calling the linking function returns the element of the template. It is either the original element | |
* passed in, or the clone of the element if the `cloneAttachFn` is provided. | |
* | |
* After linking the view is not updated until after a call to $digest which typically is done by | |
* Angular automatically. | |
* | |
* If you need access to the bound view, there are two ways to do it: | |
* | |
* - If you are not asking the linking function to clone the template, create the DOM element(s) | |
* before you send them to the compiler and keep this reference around. | |
* <pre> | |
* var element = $compile('<p>{{total}}</p>')(scope); | |
* </pre> | |
* | |
* - if on the other hand, you need the element to be cloned, the view reference from the original | |
* example would not point to the clone, but rather to the original template that was cloned. In | |
* this case, you can access the clone via the cloneAttachFn: | |
* <pre> | |
* var templateHTML = angular.element('<p>{{total}}</p>'), | |
* scope = ....; | |
* | |
* var clonedElement = $compile(templateHTML)(scope, function(clonedElement, scope) { | |
* //attach the clone to DOM document at the right place | |
* }); | |
* | |
* //now we have reference to the cloned DOM via `clone` | |
* </pre> | |
* | |
* | |
* For information on how the compiler works, see the | |
* {@link guide/compiler Angular HTML Compiler} section of the Developer Guide. | |
*/ | |
var $compileMinErr = minErr('$compile'); | |
/** | |
* @ngdoc service | |
* @name ng.$compileProvider | |
* @function | |
* | |
* @description | |
*/ | |
$CompileProvider.$inject = ['$provide']; | |
function $CompileProvider($provide) { | |
var hasDirectives = {}, | |
Suffix = 'Directive', | |
COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, | |
CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, | |
aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, | |
imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; | |
// Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes | |
// 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)$/; | |
/** | |
* @ngdoc function | |
* @name ng.$compileProvider#directive | |
* @methodOf ng.$compileProvider | |
* @function | |
* | |
* @description | |
* Register a new directive with the compiler. | |
* | |
* @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. | |
* @returns {ng.$compileProvider} Self for chaining. | |
*/ | |
this.directive = function registerDirective(name, directiveFactory) { | |
assertNotHasOwnProperty(name, 'directive'); | |
if (isString(name)) { | |
assertArg(directiveFactory, 'directiveFactory'); | |
if (!hasDirectives.hasOwnProperty(name)) { | |
hasDirectives[name] = []; | |
$provide.factory(name + Suffix, ['$injector', '$exceptionHandler', | |
function($injector, $exceptionHandler) { | |
var directives = []; | |
forEach(hasDirectives[name], function(directiveFactory, index) { | |
try { | |
var directive = $injector.invoke(directiveFactory); | |
if (isFunction(directive)) { | |
directive = { compile: valueFn(directive) }; | |
} else if (!directive.compile && directive.link) { | |
directive.compile = valueFn(directive.link); | |
} | |
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 || 'A'; | |
directives.push(directive); | |
} catch (e) { | |
$exceptionHandler(e); | |
} | |
}); | |
return directives; | |
}]); | |
} | |
hasDirectives[name].push(directiveFactory); | |
} else { | |
forEach(name, reverseParams(registerDirective)); | |
} | |
return this; | |
}; | |
/** | |
* @ngdoc function | |
* @name ng.$compileProvider#aHrefSanitizationWhitelist | |
* @methodOf ng.$compileProvider | |
* @function | |
* | |
* @description | |
* Retrieves or overrides the default regular expression that is used for whitelisting of safe | |
* urls during a[href] sanitization. | |
* | |
* The sanitization is a security measure aimed at prevent XSS attacks via html links. | |
* | |
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into | |
* an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` | |
* regular expression. If a match is found, the original url is written into the dom. Otherwise, | |
* the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. | |
* | |
* @param {RegExp=} regexp New regexp to whitelist urls with. | |
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for | |
* chaining otherwise. | |
*/ | |
this.aHrefSanitizationWhitelist = function(regexp) { | |
if (isDefined(regexp)) { | |
aHrefSanitizationWhitelist = regexp; | |
return this; | |
} | |
return aHrefSanitizationWhitelist; | |
}; | |
/** | |
* @ngdoc function | |
* @name ng.$compileProvider#imgSrcSanitizationWhitelist | |
* @methodOf ng.$compileProvider | |
* @function | |
* | |
* @description | |
* Retrieves or overrides the default regular expression that is used for whitelisting of safe | |
* urls during img[src] sanitization. | |
* | |
* The sanitization is a security measure aimed at prevent XSS attacks via html links. | |
* | |
* Any url about to be assigned to img[src] via data-binding is first normalized and turned into an | |
* absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` regular | |
* expression. If a match is found, the original url is written into the dom. Otherwise, the | |
* absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. | |
* | |
* @param {RegExp=} regexp New regexp to whitelist urls with. | |
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for | |
* chaining otherwise. | |
*/ | |
this.imgSrcSanitizationWhitelist = function(regexp) { | |
if (isDefined(regexp)) { | |
imgSrcSanitizationWhitelist = regexp; | |
return this; | |
} | |
return imgSrcSanitizationWhitelist; | |
}; | |
this.$get = [ | |
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', | |
'$controller', '$rootScope', '$document', '$sce', '$animate', | |
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, | |
$controller, $rootScope, $document, $sce, $animate) { | |
var Attributes = function(element, attr) { | |
this.$$element = element; | |
this.$attr = attr || {}; | |
}; | |
Attributes.prototype = { | |
$normalize: directiveNormalize, | |
/** | |
* @ngdoc function | |
* @name ng.$compile.directive.Attributes#$addClass | |
* @methodOf ng.$compile.directive.Attributes | |
* @function | |
* | |
* @description | |
* Adds the CSS class value specified by the classVal parameter to the element. If animations | |
* are enabled then an animation will be triggered for the class addition. | |
* | |
* @param {string} classVal The className value that will be added to the element | |
*/ | |
$addClass : function(classVal) { | |
if(classVal && classVal.length > 0) { | |
$animate.addClass(this.$$element, classVal); | |
} | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$compile.directive.Attributes#$removeClass | |
* @methodOf ng.$compile.directive.Attributes | |
* @function | |
* | |
* @description | |
* Removes the CSS class value specified by the classVal parameter from the element. If animations | |
* are enabled then an animation will be triggered for the class removal. | |
* | |
* @param {string} classVal The className value that will be removed from the element | |
*/ | |
$removeClass : function(classVal) { | |
if(classVal && classVal.length > 0) { | |
$animate.removeClass(this.$$element, classVal); | |
} | |
}, | |
/** | |
* Set a normalized attribute on the element in a way such that all directives | |
* can share the attribute. This function properly handles boolean attributes. | |
* @param {string} key Normalized key. (ie ngAttribute) | |
* @param {string|boolean} value The value to set. If `null` attribute will be deleted. | |
* @param {boolean=} writeAttr If false, does not write the value to DOM element attribute. | |
* Defaults to true. | |
* @param {string=} attrName Optional none normalized name. Defaults to key. | |
*/ | |
$set: function(key, value, writeAttr, attrName) { | |
//special case for class attribute addition + removal | |
//so that class changes can tap into the animation | |
//hooks provided by the $animate service | |
if(key == 'class') { | |
value = value || ''; | |
var current = this.$$element.attr('class') || ''; | |
this.$removeClass(tokenDifference(current, value).join(' ')); | |
this.$addClass(tokenDifference(value, current).join(' ')); | |
} else { | |
var booleanKey = getBooleanAttrName(this.$$element[0], key), | |
normalizedVal, | |
nodeName; | |
if (booleanKey) { | |
this.$$element.prop(key, value); | |
attrName = booleanKey; | |
} | |
this[key] = value; | |
// translate normalized key to actual key | |
if (attrName) { | |
this.$attr[key] = attrName; | |
} else { | |
attrName = this.$attr[key]; | |
if (!attrName) { | |
this.$attr[key] = attrName = snake_case(key, '-'); | |
} | |
} | |
nodeName = nodeName_(this.$$element); | |
// sanitize a[href] and img[src] values | |
if ((nodeName === 'A' && key === 'href') || | |
(nodeName === 'IMG' && key === 'src')) { | |
// NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. | |
if (!msie || msie >= 8 ) { | |
normalizedVal = urlResolve(value).href; | |
if (normalizedVal !== '') { | |
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || | |
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { | |
this[key] = value = 'unsafe:' + normalizedVal; | |
} | |
} | |
} | |
} | |
if (writeAttr !== false) { | |
if (value === null || value === undefined) { | |
this.$$element.removeAttr(attrName); | |
} else { | |
this.$$element.attr(attrName, value); | |
} | |
} | |
} | |
// fire observers | |
var $$observers = this.$$observers; | |
$$observers && forEach($$observers[key], function(fn) { | |
try { | |
fn(value); | |
} catch (e) { | |
$exceptionHandler(e); | |
} | |
}); | |
function tokenDifference(str1, str2) { | |
var values = [], | |
tokens1 = str1.split(/\s+/), | |
tokens2 = str2.split(/\s+/); | |
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); | |
} | |
return values; | |
}; | |
}, | |
/** | |
* @ngdoc function | |
* @name ng.$compile.directive.Attributes#$observe | |
* @methodOf ng.$compile.directive.Attributes | |
* @function | |
* | |
* @description | |
* Observes an interpolated attribute. | |
* | |
* The observer function will be invoked once during the next `$digest` following | |
* compilation. The observer is then invoked whenever the interpolated value | |
* changes. | |
* | |
* @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#Attributes Directives} guide for more info. | |
* @returns {function()} the `fn` parameter. | |
*/ | |
$observe: function(key, fn) { | |
var attrs = this, | |
$$observers = (attrs.$$observers || (attrs.$$observers = {})), | |
listeners = ($$observers[key] || ($$observers[key] = [])); | |
listeners.push(fn); | |
$rootScope.$evalAsync(function() { | |
if (!listeners.$$inter) { | |
// no one registered attribute interpolation function, so lets call it manually | |
fn(attrs[key]); | |
} | |
}); | |
return fn; | |
} | |
}; | |
var startSymbol = $interpolate.startSymbol(), | |
endSymbol = $interpolate.endSymbol(), | |
denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}') | |
? identity | |
: function denormalizeTemplate(template) { | |
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol); | |
}, | |
NG_ATTR_BINDING = /^ngAttr[A-Z]/; | |
return compile; | |
//================================ | |
function compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) { | |
if (!($compileNodes instanceof jqLite)) { | |
// jquery always rewraps, whereas we need to preserve the original selector so that we can 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 == 3 /* text node */ && node.nodeValue.match(/\S+/) /* non-empty */ ) { | |
$compileNodes[index] = node = jqLite(node).wrap('<span></span>').parent()[0]; | |
} | |
}); | |
var compositeLinkFn = compileNodes($compileNodes, transcludeFn, $compileNodes, maxPriority, ignoreDirective, previousCompileContext); | |
return function publicLinkFn(scope, cloneConnectFn){ | |
assertArg(scope, 'scope'); | |
// important!!: we must call our jqLite.clone() since the jQuery one is trying to be smart | |
// and sometimes changes the structure of the DOM. | |
var $linkNode = cloneConnectFn | |
? JQLitePrototype.clone.call($compileNodes) // IMPORTANT!!! | |
: $compileNodes; | |
// Attach scope only to non-text nodes. | |
for(var i = 0, ii = $linkNode.length; i<ii; i++) { | |
var node = $linkNode[i]; | |
if (node.nodeType == 1 /* element */ || node.nodeType == 9 /* document */) { | |
$linkNode.eq(i).data('$scope', scope); | |
} | |
} | |
safeAddClass($linkNode, 'ng-scope'); | |
if (cloneConnectFn) cloneConnectFn($linkNode, scope); | |
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode); | |
return $linkNode; | |
}; | |
} | |
function safeAddClass($element, className) { | |
try { | |
$element.addClass(className); | |
} catch(e) { | |
// ignore, since it means that we are trying to set class on | |
// SVG element, where class name is read-only. | |
} | |
} | |
/** | |
* Compile function matches each node in nodeList against the directives. Once all directives | |
* for a particular node are collected their compile functions are executed. The compile | |
* functions return values - the linking functions - are combined into a composite linking | |
* function, which is the a linking function for the node. | |
* | |
* @param {NodeList} nodeList an array of nodes or NodeList to compile | |
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the | |
* scope argument is auto-generated to the new child of the transcluded parent scope. | |
* @param {DOMElement=} $rootElement If the nodeList is the root of the compilation tree then the | |
* rootElement must be set the jqLite collection of the compile root. This is | |
* needed so that the jqLite collection items can be replaced with widgets. | |
* @param {number=} max directive priority | |
* @returns {?function} A composite linking function of all of the matched directives or null. | |
*/ | |
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective, previousCompileContext) { | |
var linkFns = [], | |
nodeLinkFn, childLinkFn, directives, attrs, linkFnFound; | |
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. | |
directives = collectDirectives(nodeList[i], [], attrs, i == 0 ? maxPriority : undefined, ignoreDirective); | |
nodeLinkFn = (directives.length) | |
? applyDirectivesToNode(directives, nodeList[i], attrs, transcludeFn, $rootElement, null, [], [], previousCompileContext) | |
: null; | |
childLinkFn = (nodeLinkFn && nodeLinkFn.terminal || !nodeList[i].childNodes || !nodeList[i].childNodes.length) | |
? null | |
: compileNodes(nodeList[i].childNodes, | |
nodeLinkFn ? nodeLinkFn.transclude : transcludeFn); | |
linkFns.push(nodeLinkFn); | |
linkFns.push(childLinkFn); | |
linkFnFound = (linkFnFound || nodeLinkFn || childLinkFn); | |
previousCompileContext = null; //use the previous context only for the first element in the virtual group | |
} | |
// return a linking function if we have found anything, null otherwise | |
return linkFnFound ? compositeLinkFn : null; | |
function compositeLinkFn(scope, nodeList, $rootElement, boundTranscludeFn) { | |
var nodeLinkFn, childLinkFn, node, childScope, childTranscludeFn, i, ii, n; | |
// copy nodeList so that linking doesn't break due to live list updates. | |
var stableNodeList = []; | |
for (i = 0, ii = nodeList.length; i < ii; i++) { | |
stableNodeList.push(nodeList[i]); | |
} | |
for(i = 0, n = 0, ii = linkFns.length; i < ii; n++) { | |
node = stableNodeList[n]; | |
nodeLinkFn = linkFns[i++]; | |
childLinkFn = linkFns[i++]; | |
if (nodeLinkFn) { | |
if (nodeLinkFn.scope) { | |
childScope = scope.$new(isObject(nodeLinkFn.scope)); | |
jqLite(node).data('$scope', childScope); | |
} else { | |
childScope = scope; | |
} | |
childTranscludeFn = nodeLinkFn.transclude; | |
if (childTranscludeFn || (!boundTranscludeFn && transcludeFn)) { | |
nodeLinkFn(childLinkFn, childScope, node, $rootElement, | |
(function(transcludeFn) { | |
return function(cloneFn) { | |
var transcludeScope = scope.$new(); | |
transcludeScope.$$transcluded = true; | |
return transcludeFn(transcludeScope, cloneFn). | |
on('$destroy', bind(transcludeScope, transcludeScope.$destroy)); | |
}; | |
})(childTranscludeFn || transcludeFn) | |
); | |
} else { | |
nodeLinkFn(childLinkFn, childScope, node, undefined, boundTranscludeFn); | |
} | |
} else if (childLinkFn) { | |
childLinkFn(scope, node.childNodes, undefined, boundTranscludeFn); | |
} | |
} | |
} | |
} | |
/** | |
* Looks for directives on the given node and adds them to the directive collection which is | |
* sorted. | |
* | |
* @param node Node to search. | |
* @param directives An array to which the directives are added to. This array is sorted before | |
* the function returns. | |
* @param attrs The shared attrs object which is used to populate the normalized attributes. | |
* @param {number=} maxPriority Max directive priority. | |
*/ | |
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) { | |
var nodeType = node.nodeType, | |
attrsMap = attrs.$attr, | |
match, | |
className; | |
switch(nodeType) { | |
case 1: /* Element */ | |
// use the node name: <directive> | |
addDirective(directives, | |
directiveNormalize(nodeName_(node).toLowerCase()), 'E', maxPriority, ignoreDirective); | |
// iterate over the attributes | |
for (var attr, name, nName, ngAttrName, value, nAttrs = node.attributes, | |
j = 0, jj = nAttrs && nAttrs.length; j < jj; j++) { | |
var attrStartName = false; | |
var attrEndName = false; | |
attr = nAttrs[j]; | |
if (!msie || msie >= 8 || attr.specified) { | |
name = attr.name; | |
// support ngAttr attribute binding | |
ngAttrName = directiveNormalize(name); | |
if (NG_ATTR_BINDING.test(ngAttrName)) { | |
name = snake_case(ngAttrName.substr(6), '-'); | |
} | |
var directiveNName = ngAttrName.replace(/(Start|End)$/, ''); | |
if (ngAttrName === directiveNName + 'Start') { | |
attrStartName = name; | |
attrEndName = name.substr(0, name.length - 5) + 'end'; | |
name = name.substr(0, name.length - 6); | |
} | |
nName = directiveNormalize(name.toLowerCase()); | |
attrsMap[nName] = name; | |
attrs[nName] = value = trim((msie && name == 'href') | |
? decodeURIComponent(node.getAttribute(name, 2)) | |
: attr.value); | |
if (getBooleanAttrName(node, nName)) { | |
attrs[nName] = true; // presence means true | |
} | |
addAttrInterpolateDirective(node, directives, value, nName); | |
addDirective(directives, nName, 'A', maxPriority, ignoreDirective, attrStartName, attrEndName); | |
} | |
} | |
// use class as directive | |
className = node.className; | |
if (isString(className) && className !== '') { | |
while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) { | |
nName = directiveNormalize(match[2]); | |
if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) { | |
attrs[nName] = trim(match[3]); | |
} | |
className = className.substr(match.index + match[0].length); | |
} | |
} | |
break; | |
case 3: /* Text Node */ | |
addTextInterpolateDirective(directives, node.nodeValue); | |
break; | |
case 8: /* 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.) | |
} | |
break; | |
} | |
directives.sort(byPriority); | |
return directives; | |
} | |
/** | |
* Given a node with an directive-start it collects all of the siblings until it find directive-end. | |
* @param node | |
* @param attrStart | |
* @param attrEnd | |
* @returns {*} | |
*/ | |
function groupScan(node, attrStart, attrEnd) { | |
var nodes = []; | |
var depth = 0; | |
if (attrStart && node.hasAttribute && node.hasAttribute(attrStart)) { | |
var startNode = node; | |
do { | |
if (!node) { | |
throw $compileMinErr('uterdir', "Unterminated attribute, found '{0}' but no matching '{1}' found.", attrStart, attrEnd); | |
} | |
if (node.nodeType == 1 /** Element **/) { | |
if (node.hasAttribute(attrStart)) depth++; | |
if (node.hasAttribute(attrEnd)) depth--; | |
} | |
nodes.push(node); | |
node = node.nextSibling; | |
} while (depth > 0); | |
} else { | |
nodes.push(node); | |
} | |
return jqLite(nodes); | |
} | |
/** | |
* Wrapper for linking function which converts normal linking function into a grouped | |
* linking function. | |
* @param linkFn | |
* @param attrStart | |
* @param attrEnd | |
* @returns {Function} | |
*/ | |
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) { | |
return function(scope, element, attrs, controllers) { | |
element = groupScan(element[0], attrStart, attrEnd); | |
return linkFn(scope, element, attrs, controllers); | |
} | |
} | |
/** | |
* Once the directives have been collected, their compile functions are executed. This method | |
* is responsible for inlining directive templates as well as terminating the application | |
* of the directives if the terminal directive has been reached. | |
* | |
* @param {Array} directives Array of collected directives to execute their compile function. | |
* this needs to be pre-sorted by priority order. | |
* @param {Node} compileNode The raw DOM node to apply the compile functions to | |
* @param {Object} templateAttrs The shared attribute function | |
* @param {function(angular.Scope[, cloneAttachFn]} transcludeFn A linking function, where the | |
* scope argument is auto-generated to the new child of the transcluded parent scope. | |
* @param {JQLite} jqCollection If we are working on the root of the compile tree then this | |
* argument has the root jqLite array so that we can replace nodes on it. | |
* @param {Object=} originalReplaceDirective An optional directive that will be ignored when compiling | |
* the transclusion. | |
* @param {Array.<Function>} preLinkFns | |
* @param {Array.<Function>} postLinkFns | |
* @param {Object} previousCompileContext Context used for previous compilation of the current node | |
* @returns linkFn | |
*/ | |
function applyDirectivesToNode(directives, compileNode, templateAttrs, transcludeFn, jqCollection, | |
originalReplaceDirective, preLinkFns, postLinkFns, previousCompileContext) { | |
previousCompileContext = previousCompileContext || {}; | |
var terminalPriority = -Number.MAX_VALUE, | |
newScopeDirective, | |
newIsolateScopeDirective = previousCompileContext.newIsolateScopeDirective, | |
templateDirective = previousCompileContext.templateDirective, | |
$compileNode = templateAttrs.$$element = jqLite(compileNode), | |
directive, | |
directiveName, | |
$template, | |
transcludeDirective = previousCompileContext.transcludeDirective, | |
replaceDirective = originalReplaceDirective, | |
childTranscludeFn = transcludeFn, | |
controllerDirectives, | |
linkFn, | |
directiveValue; | |
// executes all directives on the current element | |
for(var i = 0, ii = directives.length; i < ii; i++) { | |
directive = directives[i]; | |
var attrStart = directive.$$start; | |
var attrEnd = directive.$$end; | |
// collect multiblock sections | |
if (attrStart) { | |
$compileNode = groupScan(compileNode, attrStart, attrEnd) | |
} | |
$template = undefined; | |
if (terminalPriority > directive.priority) { | |
break; // prevent further processing of directives | |
} | |
if (directiveValue = directive.scope) { | |
newScopeDirective = newScopeDirective || directive; | |
// skip the check for directives with async templates, we'll check the derived sync directive when | |
// the template arrives | |
if (!directive.templateUrl) { | |
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive, $compileNode); | |
if (isObject(directiveValue)) { | |
safeAddClass($compileNode, 'ng-isolate-scope'); | |
newIsolateScopeDirective = directive; | |
} | |
safeAddClass($compileNode, 'ng-scope'); | |
} | |
} | |
directiveName = directive.name; | |
if (!directive.templateUrl && directive.controller) { | |
directiveValue = directive.controller; | |
controllerDirectives = controllerDirectives || {}; | |
assertNoDuplicate("'" + directiveName + "' controller", | |
controllerDirectives[directiveName], directive, $compileNode); | |
controllerDirectives[directiveName] = directive; | |
} | |
if (directiveValue = directive.transclude) { | |
// Special case ngRepeat so that we don't complain about duplicate transclusion, ngRepeat knows how to handle | |
// this on its own. | |
if (directiveName !== 'ngRepeat') { | |
assertNoDuplicate('transclusion', transcludeDirective, directive, $compileNode); | |
transcludeDirective = directive; | |
} | |
if (directiveValue == 'element') { | |
terminalPriority = directive.priority; | |
$template = groupScan(compileNode, attrStart, attrEnd); | |
$compileNode = templateAttrs.$$element = | |
jqLite(document.createComment(' ' + directiveName + ': ' + templateAttrs[directiveName] + ' ')); | |
compileNode = $compileNode[0]; | |
replaceWith(jqCollection, jqLite(sliceArgs($template)), compileNode); | |
childTranscludeFn = compile($template, transcludeFn, terminalPriority, | |
replaceDirective && replaceDirective.name, { | |
newIsolateScopeDirective: newIsolateScopeDirective, | |
transcludeDirective: transcludeDirective, | |
templateDirective: templateDirective | |
}); | |
} else { | |
$template = jqLite(JQLiteClone(compileNode)).contents(); | |
$compileNode.html(''); // clear contents | |
childTranscludeFn = compile($template, transcludeFn); | |
} | |
} | |
if (directive.template) { | |
assertNoDuplicate('template', templateDirective, directive, $compileNode); | |
templateDirective = directive; | |
directiveValue = (isFunction(directive.template)) | |
? directive.template($compileNode, templateAttrs) | |
: directive.template; | |
directiveValue = denormalizeTemplate(directiveValue); | |
if (directive.replace) { | |
replaceDirective = directive; | |
$template = jqLite('<div>' + | |
trim(directiveValue) + | |
'</div>').contents(); | |
compileNode = $template[0]; | |
if ($template.length != 1 || compileNode.nodeType !== 1) { | |
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", directiveName, ''); | |
} | |
replaceWith(jqCollection, $compileNode, compileNode); | |
var newTemplateAttrs = {$attr: {}}; | |
// combine directives from the original node and from the template: | |
// - take the array of directives for this element | |
// - split it into two parts, those that were already applied and those that weren't | |
// - collect directives from the template, add them to the second group and sort them | |
// - append the second group with new directives to the first group | |
directives = directives.concat( | |
collectDirectives( | |
compileNode, | |
directives.splice(i + 1, directives.length - (i + 1)), | |
newTemplateAttrs | |
) | |
); | |
mergeTemplateAttributes(templateAttrs, newTemplateAttrs); | |
ii = directives.length; | |
} else { | |
$compileNode.html(directiveValue); | |
} | |
} | |
if (directive.templateUrl) { | |
assertNoDuplicate('template', templateDirective, directive, $compileNode); | |
templateDirective = directive; | |
if (directive.replace) { | |
replaceDirective = directive; | |
} | |
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode, | |
templateAttrs, jqCollection, childTranscludeFn, preLinkFns, postLinkFns, { | |
newIsolateScopeDirective: newIsolateScopeDirective, | |
transcludeDirective: transcludeDirective, | |
templateDirective: templateDirective | |
}); | |
ii = directives.length; | |
} else if (directive.compile) { | |
try { | |
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn); | |
if (isFunction(linkFn)) { | |
addLinkFns(null, linkFn, attrStart, attrEnd); | |
} else if (linkFn) { | |
addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd); | |
} | |
} catch (e) { | |
$exceptionHandler(e, startingTag($compileNode)); | |
} | |
} | |
if (directive.terminal) { | |
nodeLinkFn.terminal = true; | |
terminalPriority = Math.max(terminalPriority, directive.priority); | |
} | |
} | |
nodeLinkFn.scope = newScopeDirective && newScopeDirective.scope; | |
nodeLinkFn.transclude = transcludeDirective && childTranscludeFn; | |
// might be normal or delayed nodeLinkFn depending on if templateUrl is present | |
return nodeLinkFn; | |
//////////////////// | |
function addLinkFns(pre, post, attrStart, attrEnd) { | |
if (pre) { | |
if (attrStart) pre = groupElementsLinkFnWrapper(pre, attrStart, attrEnd); | |
pre.require = directive.require; | |
preLinkFns.push(pre); | |
} | |
if (post) { | |
if (attrStart) post = groupElementsLinkFnWrapper(post, attrStart, attrEnd); | |
post.require = directive.require; | |
postLinkFns.push(post); | |
} | |
} | |
function getControllers(require, $element) { | |
var value, retrievalMethod = 'data', optional = false; | |
if (isString(require)) { | |
while((value = require.charAt(0)) == '^' || value == '?') { | |
require = require.substr(1); | |
if (value == '^') { | |
retrievalMethod = 'inheritedData'; | |
} | |
optional = optional || value == '?'; | |
} | |
value = $element[retrievalMethod]('$' + require + 'Controller'); | |
if ($element[0].nodeType == 8 && $element[0].$$controller) { // Transclusion comment node | |
value = value || $element[0].$$controller; | |
$element[0].$$controller = null; | |
} | |
if (!value && !optional) { | |
throw $compileMinErr('ctreq', "Controller '{0}', required by directive '{1}', can't be found!", require, directiveName); | |
} | |
return value; | |
} else if (isArray(require)) { | |
value = []; | |
forEach(require, function(require) { | |
value.push(getControllers(require, $element)); | |
}); | |
} | |
return value; | |
} | |
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) { | |
var attrs, $element, i, ii, linkFn, controller; | |
if (compileNode === linkNode) { | |
attrs = templateAttrs; | |
} else { | |
attrs = shallowCopy(templateAttrs, new Attributes(jqLite(linkNode), templateAttrs.$attr)); | |
} | |
$element = attrs.$$element; | |
if (newIsolateScopeDirective) { | |
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/; | |
var parentScope = scope.$parent || scope; | |
forEach(newIsolateScopeDirective.scope, function(definition, scopeName) { | |
var match = definition.match(LOCAL_REGEXP) || [], | |
attrName = match[3] || scopeName, | |
optional = (match[2] == '?'), | |
mode = match[1], // @, =, or & | |
lastValue, | |
parentGet, parentSet; | |
scope.$$isolateBindings[scopeName] = mode + attrName; | |
switch (mode) { | |
case '@': { | |
attrs.$observe(attrName, function(value) { | |
scope[scopeName] = value; | |
}); | |
attrs.$$observers[attrName].$$scope = parentScope; | |
if( attrs[attrName] ) { | |
// If the attribute has been provided then we trigger an interpolation to ensure the value is there for use in the link fn | |
scope[scopeName] = $interpolate(attrs[attrName])(parentScope); | |
} | |
break; | |
} | |
case '=': { | |
if (optional && !attrs[attrName]) { | |
return; | |
} | |
parentGet = $parse(attrs[attrName]); | |
parentSet = parentGet.assign || function() { | |
// reset the change, or we will throw this exception on every $digest | |
lastValue = scope[scopeName] = parentGet(parentScope); | |
throw $compileMinErr('nonassign', "Expression '{0}' used with directive '{1}' is non-assignable!", | |
attrs[attrName], newIsolateScopeDirective.name); | |
}; | |
lastValue = scope[scopeName] = parentGet(parentScope); | |
scope.$watch(function parentValueWatch() { | |
var parentValue = parentGet(parentScope); | |
if (parentValue !== scope[scopeName]) { | |
// we are out of sync and need to copy | |
if (parentValue !== lastValue) { | |
// parent changed and it has precedence | |
lastValue = scope[scopeName] = parentValue; | |
} else { | |
// if the parent can be assigned then do so | |
parentSet(parentScope, parentValue = lastValue = scope[scopeName]); | |
} | |
} | |
return parentValue; | |
}); | |
break; | |
} | |
case '&': { | |
parentGet = $parse(attrs[attrName]); | |
scope[scopeName] = function(locals) { | |
return parentGet(parentScope, locals); | |
}; | |
break; | |
} | |
default: { | |
throw $compileMinErr('iscp', "Invalid isolate scope definition for directive '{0}'. Definition: {... {1}: '{2}' ...}", | |
newIsolateScopeDirective.name, scopeName, definition); | |
} | |
} | |
}); | |
} | |
if (controllerDirectives) { | |
forEach(controllerDirectives, function(directive) { | |
var locals = { | |
$scope: scope, | |
$element: $element, | |
$attrs: attrs, | |
$transclude: boundTranscludeFn | |
}, controllerInstance; | |
controller = directive.controller; | |
if (controller == '@') { | |
controller = attrs[directive.name]; | |
} | |
controllerInstance = $controller(controller, locals); | |
// Directives with element transclusion and a controller need to attach controller | |
// to the comment node created by the compiler, but jQuery .data doesn't support | |
// attaching data to comment nodes so instead we set it directly on the element and | |
// remove it after we read it later. | |
if ($element[0].nodeType == 8) { // Transclusion comment node | |
$element[0].$$controller = controllerInstance; | |
} else { | |
$element.data('$' + directive.name + 'Controller', controllerInstance); | |
} | |
if (directive.controllerAs) { | |
locals.$scope[directive.controllerAs] = controllerInstance; | |
} | |
}); | |
} | |
// PRELINKING | |
for(i = 0, ii = preLinkFns.length; i < ii; i++) { | |
try { | |
linkFn = preLinkFns[i]; | |
linkFn(scope, $element, attrs, | |
linkFn.require && getControllers(linkFn.require, $element)); | |
} catch (e) { | |
$exceptionHandler(e, startingTag($element)); | |
} | |
} | |
// RECURSION | |
childLinkFn && childLinkFn(scope, linkNode.childNodes, undefined, boundTranscludeFn); | |
// POSTLINKING | |
for(i = postLinkFns.length - 1; i >= 0; i--) { | |
try { | |
linkFn = postLinkFns[i]; | |
linkFn(scope, $element, attrs, | |
linkFn.require && getControllers(linkFn.require, $element)); | |
} catch (e) { | |
$exceptionHandler(e, startingTag($element)); | |
} | |
} | |
} | |
} | |
/** | |
* looks up the directive and decorates it with exception handling and proper parameters. We | |
* call this the boundDirective. | |
* | |
* @param {string} name name of the directive to look up. | |
* @param {string} location The directive must be found in specific format. | |
* String containing any of theses characters: | |
* | |
* * `E`: element name | |
* * `A': attribute | |
* * `C`: class | |
* * `M`: comment | |
* @returns true if directive was added. | |
*/ | |
function addDirective(tDirectives, name, location, maxPriority, ignoreDirective, startAttrName, endAttrName) { | |
if (name === ignoreDirective) return null; | |
var match = null; | |
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 ( (maxPriority === undefined || maxPriority > directive.priority) && | |
directive.restrict.indexOf(location) != -1) { | |
if (startAttrName) { | |
directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName}); | |
} | |
tDirectives.push(directive); | |
match = directive; | |
} | |
} catch(e) { $exceptionHandler(e); } | |
} | |
} | |
return match; | |
} | |
/** | |
* When the element is replaced with HTML template then the new attributes | |
* on the template need to be merged with the existing attributes in the DOM. | |
* The desired effect is to have both of the attributes present. | |
* | |
* @param {object} dst destination attributes (original DOM) | |
* @param {object} src source attributes (from the directive template) | |
*/ | |
function mergeTemplateAttributes(dst, src) { | |
var srcAttr = src.$attr, | |
dstAttr = dst.$attr, | |
$element = dst.$$element; | |
// reapply the old attributes to the new element | |
forEach(dst, function(value, key) { | |
if (key.charAt(0) != '$') { | |
if (src[key]) { | |
value += (key === 'style' ? ';' : ' ') + src[key]; | |
} | |
dst.$set(key, value, true, srcAttr[key]); | |
} | |
}); | |
// 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` 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)) { | |
dst[key] = value; | |
dstAttr[key] = srcAttr[key]; | |
} | |
}); | |
} | |
function compileTemplateUrl(directives, $compileNode, tAttrs, | |
$rootElement, childTranscludeFn, preLinkFns, postLinkFns, previousCompileContext) { | |
var linkQueue = [], | |
afterTemplateNodeLinkFn, | |
afterTemplateChildLinkFn, | |
beforeTemplateCompileNode = $compileNode[0], | |
origAsyncDirective = directives.shift(), | |
// The fact that we have to copy and patch the directive seems wrong! | |
derivedSyncDirective = extend({}, origAsyncDirective, { | |
templateUrl: null, transclude: null, replace: null | |
}), | |
templateUrl = (isFunction(origAsyncDirective.templateUrl)) | |
? origAsyncDirective.templateUrl($compileNode, tAttrs) | |
: origAsyncDirective.templateUrl; | |
$compileNode.html(''); | |
$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}). | |
success(function(content) { | |
var compileNode, tempTemplateAttrs, $template; | |
content = denormalizeTemplate(content); | |
if (origAsyncDirective.replace) { | |
$template = jqLite('<div>' + trim(content) + '</div>').contents(); | |
compileNode = $template[0]; | |
if ($template.length != 1 || compileNode.nodeType !== 1) { | |
throw $compileMinErr('tplrt', "Template for directive '{0}' must have exactly one root element. {1}", | |
origAsyncDirective.name, templateUrl); | |
} | |
tempTemplateAttrs = {$attr: {}}; | |
replaceWith($rootElement, $compileNode, compileNode); | |
collectDirectives(compileNode, directives, tempTemplateAttrs); | |
mergeTemplateAttributes(tAttrs, tempTemplateAttrs); | |
} else { | |
compileNode = beforeTemplateCompileNode; | |
$compileNode.html(content); | |
} | |
directives.unshift(derivedSyncDirective); | |
afterTemplateNodeLinkFn = applyDirectivesToNode(directives, compileNode, tAttrs, | |
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns, previousCompileContext); | |
forEach($rootElement, function(node, i) { | |
if (node == compileNode) { | |
$rootElement[i] = $compileNode[0]; | |
} | |
}); | |
afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn); | |
while(linkQueue.length) { | |
var scope = linkQueue.shift(), | |
beforeTemplateLinkNode = linkQueue.shift(), | |
linkRootElement = linkQueue.shift(), | |
controller = linkQueue.shift(), | |
linkNode = $compileNode[0]; | |
if (beforeTemplateLinkNode !== beforeTemplateCompileNode) { | |
// it was cloned therefore we have to clone as well. | |
linkNode = JQLiteClone(compileNode); | |
replaceWith(linkRootElement, jqLite(beforeTemplateLinkNode), linkNode); | |
} | |
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement, controller); | |
} | |
linkQueue = null; | |
}). | |
error(function(response, code, headers, config) { | |
throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url); | |
}); | |
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, controller) { | |
if (linkQueue) { | |
linkQueue.push(scope); | |
linkQueue.push(node); | |
linkQueue.push(rootElement); | |
linkQueue.push(controller); | |
} else { | |
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, controller); | |
} | |
}; | |
} | |
/** | |
* Sorting function for bound directives. | |
*/ | |
function byPriority(a, b) { | |
var diff = b.priority - a.priority; | |
if (diff !== 0) return diff; | |
if (a.name !== b.name) return (a.name < b.name) ? -1 : 1; | |
return a.index - b.index; | |
} | |
function assertNoDuplicate(what, previousDirective, directive, element) { | |
if (previousDirective) { | |
throw $compileMinErr('multidir', 'Multiple directives [{0}, {1}] asking for {2} on: {3}', | |
previousDirective.name, directive.name, what, startingTag(element)); | |
} | |
} | |
function addTextInterpolateDirective(directives, text) { | |
var interpolateFn = $interpolate(text, true); | |
if (interpolateFn) { | |
directives.push({ | |
priority: 0, | |
compile: valueFn(function textInterpolateLinkFn(scope, node) { | |
var parent = node.parent(), | |
bindings = parent.data('$binding') || []; | |
bindings.push(interpolateFn); | |
safeAddClass(parent.data('$binding', bindings), 'ng-binding'); | |
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) { | |
node[0].nodeValue = value; | |
}); | |
}) | |
}); | |
} | |
} | |
function getTrustedContext(node, attrNormalizedName) { | |
// maction[xlink:href] can source SVG. It's not limited to <maction>. | |
if (attrNormalizedName == "xlinkHref" || | |
(nodeName_(node) != "IMG" && (attrNormalizedName == "src" || | |
attrNormalizedName == "ngSrc"))) { | |
return $sce.RESOURCE_URL; | |
} | |
} | |
function addAttrInterpolateDirective(node, directives, value, name) { | |
var interpolateFn = $interpolate(value, true); | |
// 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}", | |
startingTag(node)); | |
} | |
directives.push({ | |
priority: -100, | |
compile: valueFn(function attrInterpolateLinkFn(scope, element, attr) { | |
var $$observers = (attr.$$observers || (attr.$$observers = {})); | |
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."); | |
} | |
// we need to interpolate again, in case the attribute value has been updated | |
// (e.g. by another directive's compile function) | |
interpolateFn = $interpolate(attr[name], true, getTrustedContext(node, name)); | |
// if attribute was updated so that there is no interpolation going on we don't want to | |
// register any observers | |
if (!interpolateFn) return; | |
// TODO(i): this should likely be attr.$set(name, iterpolateFn(scope) so that we reset the actual attr value | |
attr[name] = interpolateFn(scope); | |
($$observers[name] || ($$observers[name] = [])).$$inter = true; | |
(attr.$$observers && attr.$$observers[name].$$scope || scope). | |
$watch(interpolateFn, function interpolateFnWatchAction(value) { | |
attr.$set(name, value); | |
}); | |
}) | |
}); | |
} | |
/** | |
* This is a special jqLite.replaceWith, which can replace items which | |
* have no parents, provided that the containing jqLite collection is provided. | |
* | |
* @param {JqLite=} $rootElement The root of the compile tree. Used so that we can replace nodes | |
* in the root of the tree. | |
* @param {JqLite} elementsToRemove The jqLite element which we are going to replace. We keep the shell, | |
* but replace its DOM node reference. | |
* @param {Node} newNode The new DOM node. | |
*/ | |
function replaceWith($rootElement, elementsToRemove, newNode) { | |
var firstElementToRemove = elementsToRemove[0], | |
removeCount = elementsToRemove.length, | |
parent = firstElementToRemove.parentNode, | |
i, ii; | |
if ($rootElement) { | |
for(i = 0, ii = $rootElement.length; i < ii; i++) { | |
if ($rootElement[i] == firstElementToRemove) { | |
$rootElement[i++] = newNode; | |
for (var j = i, j2 = j + removeCount - 1, | |
jj = $rootElement.length; | |
j < jj; j++, j2++) { | |
if (j2 < jj) { | |
$rootElement[j] = $rootElement[j2]; | |
} else { | |
delete $rootElement[j]; | |
} | |
} | |
$rootElement.length -= removeCount - 1; | |
break; | |
} | |
} | |
} | |
if (parent) { | |
parent.replaceChild(newNode, firstElementToRemove); | |
} | |
var fragment = document.createDocumentFragment(); | |
fragment.appendChild(firstElementToRemove); | |
newNode[jqLite.expando] = firstElementToRemove[jqLite.expando]; | |
for (var k = 1, kk = elementsToRemove.length; k < kk; k++) { | |
var element = elementsToRemove[k]; | |
jqLite(element).remove(); // must do this way to clean up expando | |
fragment.appendChild(element); | |
delete elementsToRemove[k]; | |
} | |
elementsToRemove[0] = newNode; | |
elementsToRemove.length = 1 | |
} | |
}]; | |
} | |
var PREFIX_REGEXP = /^(x[\:\-_]|data[\:\-_])/i; | |
/** | |
* Converts all accepted directives format into proper directive name. | |
* All of these will become 'myDirective': | |
* my:Directive | |
* my-directive | |
* x-my-directive | |
* data-my:directive | |
* | |
* Also there is special case for Moz prefix starting with upper case letter. | |
* @param name Name to normalize | |
*/ | |
function directiveNormalize(name) { | |
return camelCase(name.replace(PREFIX_REGEXP, '')); | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$compile.directive.Attributes | |
* @description | |
* | |
* A shared object between directive compile / linking functions which contains normalized DOM element | |
* attributes. The the values reflect current binding state `{{ }}`. The normalization is needed | |
* since all of these are treated as equivalent in Angular: | |
* | |
* <span ng:bind="a" ng-bind="a" data-ng-bind="a" x-ng-bind="a"> | |
*/ | |
/** | |
* @ngdoc property | |
* @name ng.$compile.directive.Attributes#$attr | |
* @propertyOf ng.$compile.directive.Attributes | |
* @returns {object} A map of DOM element attribute names to the normalized name. This is | |
* needed to do reverse lookup from normalized name back to actual name. | |
*/ | |
/** | |
* @ngdoc function | |
* @name ng.$compile.directive.Attributes#$set | |
* @methodOf ng.$compile.directive.Attributes | |
* @function | |
* | |
* @description | |
* Set DOM element attribute value. | |
* | |
* | |
* @param {string} name Normalized element attribute name of the property to modify. The name is | |
* revers translated using the {@link ng.$compile.directive.Attributes#$attr $attr} | |
* property to the original name. | |
* @param {string} value Value to set the attribute to. The value can be an interpolated string. | |
*/ | |
/** | |
* Closure compiler type information | |
*/ | |
function nodesetLinkingFn( | |
/* angular.Scope */ scope, | |
/* NodeList */ nodeList, | |
/* Element */ rootElement, | |
/* function(Function) */ boundTranscludeFn | |
){} | |
function directiveLinkingFn( | |
/* nodesetLinkingFn */ nodesetLinkingFn, | |
/* angular.Scope */ scope, | |
/* Node */ node, | |
/* Element */ rootElement, | |
/* function(Function) */ boundTranscludeFn | |
){} | |
/** | |
* @ngdoc object | |
* @name ng.$controllerProvider | |
* @description | |
* The {@link ng.$controller $controller service} is used by Angular to create new | |
* controllers. | |
* | |
* This provider allows controller registration via the | |
* {@link ng.$controllerProvider#register register} method. | |
*/ | |
function $ControllerProvider() { | |
var controllers = {}, | |
CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/; | |
/** | |
* @ngdoc function | |
* @name ng.$controllerProvider#register | |
* @methodOf ng.$controllerProvider | |
* @param {string|Object} name Controller name, or an object map of controllers where the keys are | |
* the names and the values are the constructors. | |
* @param {Function|Array} constructor Controller constructor fn (optionally decorated with DI | |
* annotations in the array notation). | |
*/ | |
this.register = function(name, constructor) { | |
assertNotHasOwnProperty(name, 'controller'); | |
if (isObject(name)) { | |
extend(controllers, name) | |
} else { | |
controllers[name] = constructor; | |
} | |
}; | |
this.$get = ['$injector', '$window', function($injector, $window) { | |
/** | |
* @ngdoc function | |
* @name ng.$controller | |
* @requires $injector | |
* | |
* @param {Function|string} constructor If called with a function then it's considered to be the | |
* controller constructor function. Otherwise it's considered to be a string which is used | |
* to retrieve the controller constructor using the following steps: | |
* | |
* * check if a controller with given name is registered via `$controllerProvider` | |
* * check if evaluating the string on the current scope returns a constructor | |
* * check `window[constructor]` on the global `window` object | |
* | |
* @param {Object} locals Injection locals for Controller. | |
* @return {Object} Instance of given controller. | |
* | |
* @description | |
* `$controller` service is responsible for instantiating controllers. | |
* | |
* It's just a simple call to {@link AUTO.$injector $injector}, but extracted into | |
* a service, so that one can override this service with {@link https://gist.github.com/1649788 | |
* BC version}. | |
*/ | |
return function(expression, locals) { | |
var instance, match, constructor, identifier; | |
if(isString(expression)) { | |
match = expression.match(CNTRL_REG), | |
constructor = match[1], | |
identifier = match[3]; | |
expression = controllers.hasOwnProperty(constructor) | |
? controllers[constructor] | |
: getter(locals.$scope, constructor, true) || getter($window, constructor, true); | |
assertArgFn(expression, constructor, true); | |
} | |
instance = $injector.instantiate(expression, locals); | |
if (identifier) { | |
if (!(locals && typeof locals.$scope == 'object')) { | |
throw minErr('$controller')('noscp', "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.", constructor || expression.name, identifier); | |
} | |
locals.$scope[identifier] = instance; | |
} | |
return instance; | |
}; | |
}]; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$document | |
* @requires $window | |
* | |
* @description | |
* A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document` | |
* element. | |
*/ | |
function $DocumentProvider(){ | |
this.$get = ['$window', function(window){ | |
return jqLite(window.document); | |
}]; | |
} | |
/** | |
* @ngdoc function | |
* @name ng.$exceptionHandler | |
* @requires $log | |
* | |
* @description | |
* Any uncaught exception in angular expressions is delegated to this service. | |
* The default implementation simply delegates to `$log.error` which logs it into | |
* the browser console. | |
* | |
* In unit tests, if `angular-mocks.js` is loaded, this service is overridden by | |
* {@link ngMock.$exceptionHandler mock $exceptionHandler} which aids in testing. | |
* | |
* ## Example: | |
* | |
* <pre> | |
* angular.module('exceptionOverride', []).factory('$exceptionHandler', function () { | |
* return function (exception, cause) { | |
* exception.message += ' (caused by "' + cause + '")'; | |
* throw exception; | |
* }; | |
* }); | |
* </pre> | |
* | |
* 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. | |
* | |
* @param {Error} exception Exception associated with the error. | |
* @param {string=} cause optional information about the context in which | |
* the error was thrown. | |
* | |
*/ | |
function $ExceptionHandlerProvider() { | |
this.$get = ['$log', function($log) { | |
return function(exception, cause) { | |
$log.error.apply($log, arguments); | |
}; | |
}]; | |
} | |
/** | |
* Parse headers into key value object | |
* | |
* @param {string} headers Raw headers as a string | |
* @returns {Object} Parsed headers as key value object | |
*/ | |
function parseHeaders(headers) { | |
var parsed = {}, key, val, i; | |
if (!headers) return parsed; | |
forEach(headers.split('\n'), function(line) { | |
i = line.indexOf(':'); | |
key = lowercase(trim(line.substr(0, i))); | |
val = trim(line.substr(i + 1)); | |
if (key) { | |
if (parsed[key]) { | |
parsed[key] += ', ' + val; | |
} else { | |
parsed[key] = val; | |
} | |
} | |
}); | |
return parsed; | |
} | |
/** | |
* Returns a function that provides access to parsed headers. | |
* | |
* Headers are lazy parsed when first requested. | |
* @see parseHeaders | |
* | |
* @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 no arguments returns an object containing all headers. | |
*/ | |
function headersGetter(headers) { | |
var headersObj = isObject(headers) ? headers : undefined; | |
return function(name) { | |
if (!headersObj) headersObj = parseHeaders(headers); | |
if (name) { | |
return headersObj[lowercase(name)] || null; | |
} | |
return headersObj; | |
}; | |
} | |
/** | |
* Chain all given functions | |
* | |
* This function is used for both request and response transforming | |
* | |
* @param {*} data Data to transform. | |
* @param {function(string=)} headers Http headers getter fn. | |
* @param {(function|Array.<function>)} fns Function or an array of functions. | |
* @returns {*} Transformed data. | |
*/ | |
function transformData(data, headers, fns) { | |
if (isFunction(fns)) | |
return fns(data, headers); | |
forEach(fns, function(fn) { | |
data = fn(data, headers); | |
}); | |
return data; | |
} | |
function isSuccess(status) { | |
return 200 <= status && status < 300; | |
} | |
function $HttpProvider() { | |
var JSON_START = /^\s*(\[|\{[^\{])/, | |
JSON_END = /[\}\]]\s*$/, | |
PROTECTION_PREFIX = /^\)\]\}',?\n/, | |
CONTENT_TYPE_APPLICATION_JSON = {'Content-Type': 'application/json;charset=utf-8'}; | |
var defaults = this.defaults = { | |
// transform incoming response data | |
transformResponse: [function(data) { | |
if (isString(data)) { | |
// strip json vulnerability protection prefix | |
data = data.replace(PROTECTION_PREFIX, ''); | |
if (JSON_START.test(data) && JSON_END.test(data)) | |
data = fromJson(data); | |
} | |
return data; | |
}], | |
// transform outgoing request data | |
transformRequest: [function(d) { | |
return isObject(d) && !isFile(d) ? toJson(d) : d; | |
}], | |
// default headers | |
headers: { | |
common: { | |
'Accept': 'application/json, text/plain, */*' | |
}, | |
post: CONTENT_TYPE_APPLICATION_JSON, | |
put: CONTENT_TYPE_APPLICATION_JSON, | |
patch: CONTENT_TYPE_APPLICATION_JSON | |
}, | |
xsrfCookieName: 'XSRF-TOKEN', | |
xsrfHeaderName: 'X-XSRF-TOKEN' | |
}; | |
/** | |
* Are ordered by request, i.e. they are applied in the same order as the | |
* array, on request, but reverse order, on response. | |
*/ | |
var interceptorFactories = this.interceptors = []; | |
/** | |
* For historical reasons, response interceptors are ordered by the order in which | |
* they are applied to the response. (This is the opposite of interceptorFactories) | |
*/ | |
var responseInterceptorFactories = this.responseInterceptors = []; | |
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', | |
function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { | |
var defaultCache = $cacheFactory('$http'); | |
/** | |
* Interceptors stored in reverse order. Inner interceptors before outer interceptors. | |
* The reversal is needed so that we can build up the interception chain around the | |
* server request. | |
*/ | |
var reversedInterceptors = []; | |
forEach(interceptorFactories, function(interceptorFactory) { | |
reversedInterceptors.unshift(isString(interceptorFactory) | |
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory)); | |
}); | |
forEach(responseInterceptorFactories, function(interceptorFactory, index) { | |
var responseFn = isString(interceptorFactory) | |
? $injector.get(interceptorFactory) | |
: $injector.invoke(interceptorFactory); | |
/** | |
* Response interceptors go before "around" interceptors (no real reason, just | |
* had to pick one.) But they are already reversed, so we can't use unshift, hence | |
* the splice. | |
*/ | |
reversedInterceptors.splice(index, 0, { | |
response: function(response) { | |
return responseFn($q.when(response)); | |
}, | |
responseError: function(response) { | |
return responseFn($q.reject(response)); | |
} | |
}); | |
}); | |
/** | |
* @ngdoc function | |
* @name ng.$http | |
* @requires $httpBackend | |
* @requires $browser | |
* @requires $cacheFactory | |
* @requires $rootScope | |
* @requires $q | |
* @requires $injector | |
* | |
* @description | |
* The `$http` service is a core Angular service that facilitates communication with the remote | |
* HTTP servers via the browser's {@link https://developer.mozilla.org/en/xmlhttprequest | |
* XMLHttpRequest} object or via {@link http://en.wikipedia.org/wiki/JSONP JSONP}. | |
* | |
* For unit testing applications that use `$http` service, see | |
* {@link ngMock.$httpBackend $httpBackend mock}. | |
* | |
* For a higher level of abstraction, please check out the {@link ngResource.$resource | |
* $resource} service. | |
* | |
* The $http API is based on the {@link ng.$q deferred/promise APIs} exposed by | |
* the $q service. While for simple usage patterns this doesn't matter much, for advanced usage | |
* it is important to familiarize yourself with these APIs and the guarantees they provide. | |
* | |
* | |
* # General usage | |
* The `$http` service is a function which takes a single argument — a configuration object — | |
* that is used to generate an HTTP request and returns a {@link ng.$q promise} | |
* with two $http specific methods: `success` and `error`. | |
* | |
* <pre> | |
* $http({method: 'GET', url: '/someUrl'}). | |
* success(function(data, status, headers, config) { | |
* // this callback will be called asynchronously | |
* // when the response is available | |
* }). | |
* error(function(data, status, headers, config) { | |
* // called asynchronously if an error occurs | |
* // or server returns response with an error status. | |
* }); | |
* </pre> | |
* | |
* Since the returned value of calling the $http function is a `promise`, you can also use | |
* the `then` method to register callbacks, and these callbacks will receive a single argument – | |
* an object representing the response. See the API signature and type info below for more | |
* details. | |
* | |
* 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. | |
* | |
* # Calling $http from outside AngularJS | |
* The `$http` service will not actually send the request until the next `$digest()` is executed. | |
* Normally this is not an issue, since almost all the time your call to `$http` will be from within | |
* a `$apply()` block. | |
* If you are calling `$http` from outside Angular, then you should wrap it in a call to `$apply` | |
* to cause a $digest to occur and also to handle errors in the block correctly. | |
* | |
* ``` | |
* $scope.$apply(function() { | |
* $http(...); | |
* }); | |
* ``` | |
* | |
* # Writing Unit Tests that use $http | |
* When unit testing you are mostly responsible for scheduling the `$digest` cycle. If you do not | |
* trigger a `$digest` before calling `$httpBackend.flush()` then the request will not have been | |
* made and `$httpBackend.expect(...)` expectations will fail. The solution is to run the code | |
* that calls the `$http()` method inside a $apply block as explained in the previous section. | |
* | |
* ``` | |
* $httpBackend.expectGET(...); | |
* $scope.$apply(function() { | |
* $http.get(...); | |
* }); | |
* $httpBackend.flush(); | |
* ``` | |
* | |
* # Shortcut methods | |
* | |
* Since all invocations of the $http service require passing in an HTTP method and URL, and | |
* POST/PUT requests require request data to be provided as well, shortcut methods | |
* were created: | |
* | |
* <pre> | |
* $http.get('/someUrl').success(successCallback); | |
* $http.post('/someUrl', data).success(successCallback); | |
* </pre> | |
* | |
* Complete list of shortcut methods: | |
* | |
* - {@link ng.$http#get $http.get} | |
* - {@link ng.$http#head $http.head} | |
* - {@link ng.$http#post $http.post} | |
* - {@link ng.$http#put $http.put} | |
* - {@link ng.$http#delete $http.delete} | |
* - {@link ng.$http#jsonp $http.jsonp} | |
* | |
* | |
* # Setting HTTP Headers | |
* | |
* The $http service will automatically add certain HTTP headers to all requests. These defaults | |
* can be fully configured by accessing the `$httpProvider.defaults.headers` configuration | |
* object, which currently contains this default configuration: | |
* | |
* - `$httpProvider.defaults.headers.common` (headers that are common for all requests): | |
* - `Accept: application/json, text/plain, * / *` | |
* - `$httpProvider.defaults.headers.post`: (header defaults for POST requests) | |
* - `Content-Type: application/json` | |
* - `$httpProvider.defaults.headers.put` (header defaults for PUT requests) | |
* - `Content-Type: application/json` | |
* | |
* To add or overwrite these defaults, simply add or remove a property from these configuration | |
* objects. To add headers for an HTTP method other than POST or PUT, simply add a new object | |
* with the lowercased HTTP method name as the key, e.g. | |
* `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }. | |
* | |
* Additionally, the defaults can be set at runtime via the `$http.defaults` object in the same | |
* fashion. | |
* | |
* | |
* # Transforming Requests and Responses | |
* | |
* Both requests and responses can be transformed using transform functions. By default, Angular | |
* applies these transformations: | |
* | |
* Request transformations: | |
* | |
* - If the `data` property of the request configuration object contains an object, serialize it into | |
* JSON format. | |
* | |
* Response transformations: | |
* | |
* - If XSRF prefix is detected, strip it (see Security Considerations section below). | |
* - If JSON response is detected, deserialize it using a JSON parser. | |
* | |
* To globally augment or override the default transforms, modify the `$httpProvider.defaults.transformRequest` and | |
* `$httpProvider.defaults.transformResponse` properties. These properties are by default an | |
* array of transform functions, which allows you to `push` or `unshift` a new transformation function into the | |
* transformation chain. You can also decide to completely override any default transformations by assigning your | |
* transformation functions to these properties directly without the array wrapper. | |
* | |
* Similarly, to locally override the request/response transforms, augment the `transformRequest` and/or | |
* `transformResponse` properties of the configuration object passed into `$http`. | |
* | |
* | |
* # Caching | |
* | |
* To enable caching, set the configuration property `cache` to `true`. When the cache is | |
* enabled, `$http` stores the response from the server in local cache. Next time the | |
* response is served from the cache without sending a request to the server. | |
* | |
* 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 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. | |
* | |
* A custom default cache built with $cacheFactory can be provided in $http.defaults.cache. | |
* To skip it, set configuration property `cache` to `false`. | |
* | |
* | |
* # Interceptors | |
* | |
* Before you start creating interceptors, be sure to understand the | |
* {@link ng.$q $q and deferred/promise APIs}. | |
* | |
* For purposes of global error handling, authentication, or any kind of synchronous or | |
* asynchronous pre-processing of request or postprocessing of responses, it is desirable to be | |
* able to intercept requests before they are handed to the server and | |
* responses before they are handed over to the application code that | |
* initiated these requests. The interceptors leverage the {@link ng.$q | |
* promise APIs} to fulfill this need for both synchronous and asynchronous pre-processing. | |
* | |
* The interceptors are service factories that are registered with the `$httpProvider` by | |
* adding them to the `$httpProvider.interceptors` array. The factory is called and | |
* injected with dependencies (if specified) and returns the interceptor. | |
* | |
* There are two kinds of interceptors (and two kinds of rejection interceptors): | |
* | |
* * `request`: interceptors get called with http `config` object. The function is free to modify | |
* the `config` or create a new one. The function needs to return the `config` directly or as a | |
* promise. | |
* * `requestError`: interceptor gets called when a previous interceptor threw an error or resolved | |
* with a rejection. | |
* * `response`: interceptors get called with http `response` object. The function is free to modify | |
* the `response` or create a new one. The function needs to return the `response` directly or as a | |
* promise. | |
* * `responseError`: interceptor gets called when a previous interceptor threw an error or resolved | |
* with a rejection. | |
* | |
* | |
* <pre> | |
* // register the interceptor as a service | |
* $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { | |
* return { | |
* // optional method | |
* 'request': function(config) { | |
* // do something on success | |
* return config || $q.when(config); | |
* }, | |
* | |
* // optional method | |
* 'requestError': function(rejection) { | |
* // do something on error | |
* if (canRecover(rejection)) { | |
* return responseOrNewPromise | |
* } | |
* return $q.reject(rejection); | |
* }, | |
* | |
* | |
* | |
* // optional method | |
* 'response': function(response) { | |
* // do something on success | |
* return response || $q.when(response); | |
* }, | |
* | |
* // optional method | |
* 'responseError': function(rejection) { | |
* // do something on error | |
* if (canRecover(rejection)) { | |
* return responseOrNewPromise | |
* } | |
* return $q.reject(rejection); | |
* }; | |
* } | |
* }); | |
* | |
* $httpProvider.interceptors.push('myHttpInterceptor'); | |
* | |
* | |
* // register the interceptor via an anonymous factory | |
* $httpProvider.interceptors.push(function($q, dependency1, dependency2) { | |
* return { | |
* 'request': function(config) { | |
* // same as above | |
* }, | |
* 'response': function(response) { | |
* // same as above | |
* } | |
* }); | |
* </pre> | |
* | |
* # Response interceptors (DEPRECATED) | |
* | |
* Before you start creating interceptors, be sure to understand the | |
* {@link ng.$q $q and deferred/promise APIs}. | |
* | |
* For purposes of global error handling, authentication or any kind of synchronous or | |
* asynchronous preprocessing of received responses, it is desirable to be able to intercept | |
* responses for http requests before they are handed over to the application code that | |
* initiated these requests. The response interceptors leverage the {@link ng.$q | |
* promise apis} to fulfil this need for both synchronous and asynchronous preprocessing. | |
* | |
* The interceptors are service factories that are registered with the $httpProvider by | |
* adding them to the `$httpProvider.responseInterceptors` array. The factory is called and | |
* injected with dependencies (if specified) and returns the interceptor — a function that | |
* takes a {@link ng.$q promise} and returns the original or a new promise. | |
* | |
* <pre> | |
* // register the interceptor as a service | |
* $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { | |
* return function(promise) { | |
* return promise.then(function(response) { | |
* // do something on success | |
* return response; | |
* }, function(response) { | |
* // do something on error | |
* if (canRecover(response)) { | |
* return responseOrNewPromise | |
* } | |
* return $q.reject(response); | |
* }); | |
* } | |
* }); | |
* | |
* $httpProvider.responseInterceptors.push('myHttpInterceptor'); | |
* | |
* | |
* // register the interceptor via an anonymous factory | |
* $httpProvider.responseInterceptors.push(function($q, dependency1, dependency2) { | |
* return function(promise) { | |
* // same as above | |
* } | |
* }); | |
* </pre> | |
* | |
* | |
* # Security Considerations | |
* | |
* When designing web applications, consider security threats from: | |
* | |
* - {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx | |
* JSON vulnerability} | |
* - {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} | |
* | |
* Both server and the client must cooperate in order to eliminate these threats. Angular comes | |
* pre-configured with strategies that address these issues, but for this to work backend server | |
* cooperation is required. | |
* | |
* ## JSON Vulnerability Protection | |
* | |
* A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx | |
* JSON vulnerability} allows third party website to turn your JSON resource URL into | |
* {@link http://en.wikipedia.org/wiki/JSONP JSONP} request under some conditions. To | |
* counter this your server can prefix all JSON requests with following string `")]}',\n"`. | |
* Angular will automatically strip the prefix before processing it as JSON. | |
* | |
* For example if your server needs to return: | |
* <pre> | |
* ['one','two'] | |
* </pre> | |
* | |
* which is vulnerable to attack, your server can return: | |
* <pre> | |
* )]}', | |
* ['one','two'] | |
* </pre> | |
* | |
* Angular will strip the prefix, before processing the JSON. | |
* | |
* | |
* ## Cross Site Request Forgery (XSRF) Protection | |
* | |
* {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} 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. | |
* | |
* 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 | |
* server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure | |
* that only JavaScript running on your domain could have sent the request. The token must be | |
* unique for each user and must be verifiable by the server (to prevent the JavaScript from making | |
* up its own tokens). We recommend that the token is a digest of your site's authentication | |
* cookie with a {@link https://en.wikipedia.org/wiki/Salt_(cryptography) salt} for added security. | |
* | |
* The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName | |
* properties of either $httpProvider.defaults, or the per-request config object. | |
* | |
* | |
* @param {object} config Object describing the request to be made and how it should be | |
* processed. The object has following properties: | |
* | |
* - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) | |
* - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. | |
* - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be turned to | |
* `?key1=value1&key2=value2` after the url. If the value is not a string, it will be JSONified. | |
* - **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. | |
* - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token. | |
* - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token. | |
* - **transformRequest** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – | |
* transform function or an array of such functions. The transform function takes the http | |
* request body and headers and returns its transformed (typically serialized) version. | |
* - **transformResponse** – `{function(data, headersGetter)|Array.<function(data, headersGetter)>}` – | |
* transform function or an array of such functions. The transform function takes the http | |
* response body and headers and returns its transformed (typically deserialized) version. | |
* - **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. | |
* - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise} | |
* that should abort the request when resolved. | |
* - **withCredentials** - `{boolean}` - whether to to set the `withCredentials` flag on the | |
* XHR object. See {@link https://developer.mozilla.org/en/http_access_control#section_5 | |
* requests with credentials} for more information. | |
* - **responseType** - `{string}` - see {@link | |
* https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType requestType}. | |
* | |
* @returns {HttpPromise} Returns a {@link ng.$q promise} object with the | |
* standard `then` method and two http specific methods: `success` and `error`. The `then` | |
* method takes two arguments a success and an error callback which will be called with a | |
* response object. The `success` and `error` methods take a single argument - a function that | |
* will be called when the request succeeds or fails respectively. The arguments passed into | |
* these functions are destructured representation of the response object passed into the | |
* `then` method. The response object has these properties: | |
* | |
* - **data** – `{string|Object}` – The response body transformed with the transform functions. | |
* - **status** – `{number}` – HTTP status code of the response. | |
* - **headers** – `{function([headerName])}` – Header getter function. | |
* - **config** – `{Object}` – The configuration object that was used to generate the request. | |
* | |
* @property {Array.<Object>} pendingRequests Array of config objects for currently pending | |
* requests. This is primarily meant to be used for debugging purposes. | |
* | |
* | |
* @example | |
<example> | |
<file name="index.html"> | |
<div ng-controller="FetchCtrl"> | |
<select ng-model="method"> | |
<option>GET</option> | |
<option>JSONP</option> | |
</select> | |
<input type="text" ng-model="url" size="80"/> | |
<button ng-click="fetch()">fetch</button><br> | |
<button ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button> | |
<button ng-click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button> | |
<button ng-click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button> | |
<pre>http status code: {{status}}</pre> | |
<pre>http response data: {{data}}</pre> | |
</div> | |
</file> | |
<file name="script.js"> | |
function FetchCtrl($scope, $http, $templateCache) { | |
$scope.method = 'GET'; | |
$scope.url = 'http-hello.html'; | |
$scope.fetch = function() { | |
$scope.code = null; | |
$scope.response = null; | |
$http({method: $scope.method, url: $scope.url, cache: $templateCache}). | |
success(function(data, status) { | |
$scope.status = status; | |
$scope.data = data; | |
}). | |
error(function(data, status) { | |
$scope.data = data || "Request failed"; | |
$scope.status = status; | |
}); | |
}; | |
$scope.updateModel = function(method, url) { | |
$scope.method = method; | |
$scope.url = url; | |
}; | |
} | |
</file> | |
<file name="http-hello.html"> | |
Hello, $http! | |
</file> | |
<file name="scenario.js"> | |
it('should make an xhr GET request', function() { | |
element(':button:contains("Sample GET")').click(); | |
element(':button:contains("fetch")').click(); | |
expect(binding('status')).toBe('200'); | |
expect(binding('data')).toMatch(/Hello, \$http!/); | |
}); | |
it('should make a JSONP request to angularjs.org', function() { | |
element(':button:contains("Sample JSONP")').click(); | |
element(':button:contains("fetch")').click(); | |
expect(binding('status')).toBe('200'); | |
expect(binding('data')).toMatch(/Super Hero!/); | |
}); | |
it('should make JSONP request to invalid URL and invoke the error handler', | |
function() { | |
element(':button:contains("Invalid JSONP")').click(); | |
element(':button:contains("fetch")').click(); | |
expect(binding('status')).toBe('0'); | |
expect(binding('data')).toBe('Request failed'); | |
}); | |
</file> | |
</example> | |
*/ | |
function $http(requestConfig) { | |
var config = { | |
transformRequest: defaults.transformRequest, | |
transformResponse: defaults.transformResponse | |
}; | |
var headers = mergeHeaders(requestConfig); | |
extend(config, requestConfig); | |
config.headers = headers; | |
config.method = uppercase(config.method); | |
var xsrfValue = urlIsSameOrigin(config.url) | |
? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] | |
: undefined; | |
if (xsrfValue) { | |
headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue; | |
} | |
var serverRequest = function(config) { | |
headers = config.headers; | |
var reqData = transformData(config.data, headersGetter(headers), config.transformRequest); | |
// strip content-type if data is undefined | |
if (isUndefined(config.data)) { | |
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, headers).then(transformResponse, transformResponse); | |
}; | |
var chain = [serverRequest, undefined]; | |
var promise = $q.when(config); | |
// apply interceptors | |
forEach(reversedInterceptors, function(interceptor) { | |
if (interceptor.request || interceptor.requestError) { | |
chain.unshift(interceptor.request, interceptor.requestError); | |
} | |
if (interceptor.response || interceptor.responseError) { | |
chain.push(interceptor.response, interceptor.responseError); | |
} | |
}); | |
while(chain.length) { | |
var thenFn = chain.shift(); | |
var rejectFn = chain.shift(); | |
promise = promise.then(thenFn, rejectFn); | |
} | |
promise.success = function(fn) { | |
promise.then(function(response) { | |
fn(response.data, response.status, response.headers, config); | |
}); | |
return promise; | |
}; | |
promise.error = function(fn) { | |
promise.then(null, function(response) { | |
fn(response.data, response.status, response.headers, config); | |
}); | |
return promise; | |
}; | |
return promise; | |
function transformResponse(response) { | |
// make a copy since the response must be cacheable | |
var resp = extend({}, response, { | |
data: transformData(response.data, response.headers, config.transformResponse) | |
}); | |
return (isSuccess(response.status)) | |
? resp | |
: $q.reject(resp); | |
} | |
function mergeHeaders(config) { | |
var defHeaders = defaults.headers, | |
reqHeaders = extend({}, config.headers), | |
defHeaderName, lowercaseDefHeaderName, reqHeaderName; | |
defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]); | |
// execute if header value is function | |
execHeaders(defHeaders); | |
execHeaders(reqHeaders); | |
// using for-in instead of forEach to avoid unecessary iteration after header has been found | |
defaultHeadersIteration: | |
for (defHeaderName in defHeaders) { | |
lowercaseDefHeaderName = lowercase(defHeaderName); | |
for (reqHeaderName in reqHeaders) { | |
if (lowercase(reqHeaderName) === lowercaseDefHeaderName) { | |
continue defaultHeadersIteration; | |
} | |
} | |
reqHeaders[defHeaderName] = defHeaders[defHeaderName]; | |
} | |
return reqHeaders; | |
function execHeaders(headers) { | |
var headerContent; | |
forEach(headers, function(headerFn, header) { | |
if (isFunction(headerFn)) { | |
headerContent = headerFn(); | |
if (headerContent != null) { | |
headers[header] = headerContent; | |
} else { | |
delete headers[header]; | |
} | |
} | |
}); | |
} | |
} | |
} | |
$http.pendingRequests = []; | |
/** | |
* @ngdoc method | |
* @name ng.$http#get | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `GET` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
/** | |
* @ngdoc method | |
* @name ng.$http#delete | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `DELETE` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
/** | |
* @ngdoc method | |
* @name ng.$http#head | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `HEAD` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
/** | |
* @ngdoc method | |
* @name ng.$http#jsonp | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `JSONP` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request. | |
* Should contain `JSON_CALLBACK` string. | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
createShortMethods('get', 'delete', 'head', 'jsonp'); | |
/** | |
* @ngdoc method | |
* @name ng.$http#post | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `POST` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request | |
* @param {*} data Request content | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
/** | |
* @ngdoc method | |
* @name ng.$http#put | |
* @methodOf ng.$http | |
* | |
* @description | |
* Shortcut method to perform `PUT` request. | |
* | |
* @param {string} url Relative or absolute URL specifying the destination of the request | |
* @param {*} data Request content | |
* @param {Object=} config Optional configuration object | |
* @returns {HttpPromise} Future object | |
*/ | |
createShortMethodsWithData('post', 'put'); | |
/** | |
* @ngdoc property | |
* @name ng.$http#defaults | |
* @propertyOf ng.$http | |
* | |
* @description | |
* Runtime equivalent of the `$httpProvider.defaults` property. Allows configuration of | |
* default headers, withCredentials as well as request and response transformations. | |
* | |
* See "Setting HTTP Headers" and "Transforming Requests and Responses" sections above. | |
*/ | |
$http.defaults = defaults; | |
return $http; | |
function createShortMethods(names) { | |
forEach(arguments, function(name) { | |
$http[name] = function(url, config) { | |
return $http(extend(config || {}, { | |
method: name, | |
url: url | |
})); | |
}; | |
}); | |
} | |
function createShortMethodsWithData(name) { | |
forEach(arguments, function(name) { | |
$http[name] = function(url, data, config) { | |
return $http(extend(config || {}, { | |
method: name, | |
url: url, | |
data: data | |
})); | |
}; | |
}); | |
} | |
/** | |
* Makes the request. | |
* | |
* !!! ACCESSES CLOSURE VARS: | |
* $httpBackend, defaults, $log, $rootScope, defaultCache, $http.pendingRequests | |
*/ | |
function sendReq(config, reqData, reqHeaders) { | |
var deferred = $q.defer(), | |
promise = deferred.promise, | |
cache, | |
cachedResp, | |
url = buildUrl(config.url, config.params); | |
$http.pendingRequests.push(config); | |
promise.then(removePendingReq, removePendingReq); | |
if ((config.cache || defaults.cache) && config.cache !== false && config.method == 'GET') { | |
cache = isObject(config.cache) ? config.cache | |
: isObject(defaults.cache) ? defaults.cache | |
: defaultCache; | |
} | |
if (cache) { | |
cachedResp = cache.get(url); | |
if (isDefined(cachedResp)) { | |
if (cachedResp.then) { | |
// cached request has already been sent, but there is no response yet | |
cachedResp.then(removePendingReq, removePendingReq); | |
return cachedResp; | |
} else { | |
// serving from cache | |
if (isArray(cachedResp)) { | |
resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); | |
} else { | |
resolvePromise(cachedResp, 200, {}); | |
} | |
} | |
} else { | |
// put the promise for the non-transformed response into cache as a placeholder | |
cache.put(url, promise); | |
} | |
} | |
// if we won't have the response in cache, send the request to the backend | |
if (isUndefined(cachedResp)) { | |
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout, | |
config.withCredentials, config.responseType); | |
} | |
return promise; | |
/** | |
* Callback registered to $httpBackend(): | |
* - caches the response if desired | |
* - resolves the raw $http promise | |
* - calls $apply | |
*/ | |
function done(status, response, headersString) { | |
if (cache) { | |
if (isSuccess(status)) { | |
cache.put(url, [status, response, parseHeaders(headersString)]); | |
} else { | |
// remove promise from the cache | |
cache.remove(url); | |
} | |
} | |
resolvePromise(response, status, headersString); | |
if (!$rootScope.$$phase) $rootScope.$apply(); | |
} | |
/** | |
* Resolves the raw $http promise. | |
*/ | |
function resolvePromise(response, status, headers) { | |
// normalize internal statuses to 0 | |
status = Math.max(status, 0); | |
(isSuccess(status) ? deferred.resolve : deferred.reject)({ | |
data: response, | |
status: status, | |
headers: headersGetter(headers), | |
config: config | |
}); | |
} | |
function removePendingReq() { | |
var idx = indexOf($http.pendingRequests, config); | |
if (idx !== -1) $http.pendingRequests.splice(idx, 1); | |
} | |
} | |
function buildUrl(url, params) { | |
if (!params) return url; | |
var parts = []; | |
forEachSorted(params, function(value, key) { | |
if (value == null || value == undefined) return; | |
if (!isArray(value)) value = [value]; | |
forEach(value, function(v) { | |
if (isObject(v)) { | |
v = toJson(v); | |
} | |
parts.push(encodeUriQuery(key) + '=' + | |
encodeUriQuery(v)); | |
}); | |
}); | |
return url + ((url.indexOf('?') == -1) ? '?' : '&') + parts.join('&'); | |
} | |
}]; | |
} | |
var XHR = window.XMLHttpRequest || function() { | |
try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} | |
try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} | |
try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} | |
throw minErr('$httpBackend')('noxhr', "This browser does not support XMLHttpRequest."); | |
}; | |
/** | |
* @ngdoc object | |
* @name ng.$httpBackend | |
* @requires $browser | |
* @requires $window | |
* @requires $document | |
* | |
* @description | |
* HTTP backend used by the {@link ng.$http service} that delegates to | |
* XMLHttpRequest object or JSONP and deals with browser incompatibilities. | |
* | |
* You should never need to use this service directly, instead use the higher-level abstractions: | |
* {@link ng.$http $http} or {@link ngResource.$resource $resource}. | |
* | |
* During testing this implementation is swapped with {@link ngMock.$httpBackend mock | |
* $httpBackend} which can be trained with responses. | |
*/ | |
function $HttpBackendProvider() { | |
this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { | |
return createHttpBackend($browser, XHR, $browser.defer, $window.angular.callbacks, | |
$document[0], $window.location.protocol.replace(':', '')); | |
}]; | |
} | |
function createHttpBackend($browser, XHR, $browserDefer, callbacks, rawDocument, locationProtocol) { | |
// TODO(vojta): fix the signature | |
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) { | |
var status; | |
$browser.$$incOutstandingRequestCount(); | |
url = url || $browser.url(); | |
if (lowercase(method) == 'jsonp') { | |
var callbackId = '_' + (callbacks.counter++).toString(36); | |
callbacks[callbackId] = function(data) { | |
callbacks[callbackId].data = data; | |
}; | |
var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId), | |
function() { | |
if (callbacks[callbackId].data) { | |
completeRequest(callback, 200, callbacks[callbackId].data); | |
} else { | |
completeRequest(callback, status || -2); | |
} | |
delete callbacks[callbackId]; | |
}); | |
} else { | |
var xhr = new XHR(); | |
xhr.open(method, url, true); | |
forEach(headers, function(value, key) { | |
if (isDefined(value)) { | |
xhr.setRequestHeader(key, value); | |
} | |
}); | |
// In IE6 and 7, this might be called synchronously when xhr.send below is called and the | |
// response is in the cache. the promise api will ensure that to the app code the api is | |
// always async | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState == 4) { | |
var responseHeaders = xhr.getAllResponseHeaders(); | |
// responseText is the old-school way of retrieving response (supported by IE8 & 9) | |
// response and responseType properties were introduced in XHR Level2 spec (supported by IE10) | |
completeRequest(callback, | |
status || xhr.status, | |
(xhr.responseType ? xhr.response : xhr.responseText), | |
responseHeaders); | |
} | |
}; | |
if (withCredentials) { | |
xhr.withCredentials = true; | |
} | |
if (responseType) { | |
xhr.responseType = responseType; | |
} | |
xhr.send(post || null); | |
} | |
if (timeout > 0) { | |
var timeoutId = $browserDefer(timeoutRequest, timeout); | |
} else if (timeout && timeout.then) { | |
timeout.then(timeoutRequest); | |
} | |
function timeoutRequest() { | |
status = -1; | |
jsonpDone && jsonpDone(); | |
xhr && xhr.abort(); | |
} | |
function completeRequest(callback, status, response, headersString) { | |
var protocol = locationProtocol || urlResolve(url).protocol; | |
// cancel timeout and subsequent timeout promise resolution | |
timeoutId && $browserDefer.cancel(timeoutId); | |
jsonpDone = xhr = null; | |
// fix status code for file protocol (it's always 0) | |
status = (protocol == 'file') ? (response ? 200 : 404) : status; | |
// normalize IE bug (http://bugs.jquery.com/ticket/1450) | |
status = status == 1223 ? 204 : status; | |
callback(status, response, headersString); | |
$browser.$$completeOutstandingRequest(noop); | |
} | |
}; | |
function jsonpReq(url, done) { | |
// we can't use jQuery/jqLite here because jQuery does crazy shit 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'), | |
doneWrapper = function() { | |
rawDocument.body.removeChild(script); | |
if (done) done(); | |
}; | |
script.type = 'text/javascript'; | |
script.src = url; | |
if (msie) { | |
script.onreadystatechange = function() { | |
if (/loaded|complete/.test(script.readyState)) doneWrapper(); | |
}; | |
} else { | |
script.onload = script.onerror = doneWrapper; | |
} | |
rawDocument.body.appendChild(script); | |
return doneWrapper; | |
} | |
} | |
var $interpolateMinErr = minErr('$interpolate'); | |
/** | |
* @ngdoc object | |
* @name ng.$interpolateProvider | |
* @function | |
* | |
* @description | |
* | |
* Used for configuring the interpolation markup. Defaults to `{{` and `}}`. | |
* | |
* @example | |
<doc:example module="customInterpolationApp"> | |
<doc:source> | |
<script> | |
var customInterpolationApp = angular.module('customInterpolationApp', []); | |
customInterpolationApp.config(function($interpolateProvider) { | |
$interpolateProvider.startSymbol('//'); | |
$interpolateProvider.endSymbol('//'); | |
}); | |
customInterpolationApp.controller('DemoController', function DemoController() { | |
this.label = "This bindings is brought you you by // interpolation symbols."; | |
}); | |
</script> | |
<div ng-app="App" ng-controller="DemoController as demo"> | |
//demo.label// | |
</div> | |
</doc:source> | |
<doc:scenario> | |
it('should interpolate binding with custom symbols', function() { | |
expect(binding('demo.label')).toBe('This bindings is brought you you by // interpolation symbols.'); | |
}); | |
</doc:scenario> | |
</doc:example> | |
*/ | |
function $InterpolateProvider() { | |
var startSymbol = '{{'; | |
var endSymbol = '}}'; | |
/** | |
* @ngdoc method | |
* @name ng.$interpolateProvider#startSymbol | |
* @methodOf ng.$interpolateProvider | |
* @description | |
* Symbol to denote start of expression in the interpolated string. Defaults to `{{`. | |
* | |
* @param {string=} value new value to set the starting symbol to. | |
* @returns {string|self} Returns the symbol when used as getter and self if used as setter. | |
*/ | |
this.startSymbol = function(value){ | |
if (value) { | |
startSymbol = value; | |
return this; | |
} else { | |
return startSymbol; | |
} | |
}; | |
/** | |
* @ngdoc method | |
* @name ng.$interpolateProvider#endSymbol | |
* @methodOf ng.$interpolateProvider | |
* @description | |
* Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. | |
* | |
* @param {string=} value new value to set the ending symbol to. | |
* @returns {string|self} Returns the symbol when used as getter and self if used as setter. | |
*/ | |
this.endSymbol = function(value){ | |
if (value) { | |
endSymbol = value; | |
return this; | |
} else { | |
return endSymbol; | |
} | |
}; | |
this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { | |
var startSymbolLength = startSymbol.length, | |
endSymbolLength = endSymbol.length; | |
/** | |
* @ngdoc function | |
* @name ng.$interpolate | |
* @function | |
* | |
* @requires $parse | |
* @requires $sce | |
* | |
* @description | |
* | |
* Compiles a string with markup into an interpolation function. This service is used by the | |
* HTML {@link ng.$compile $compile} service for data binding. See | |
* {@link ng.$interpolateProvider $interpolateProvider} for configuring the | |
* interpolation markup. | |
* | |
* | |
<pre> | |
var $interpolate = ...; // injected | |
var exp = $interpolate('Hello {{name}}!'); | |
expect(exp({name:'Angular'}).toEqual('Hello Angular!'); | |
</pre> | |
* | |
* | |
* @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 | |
* embedded expression will return null for the interpolation function. | |
* @param {string=} trustedContext when provided, the returned function passes the interpolated | |
* result through {@link ng.$sce#getTrusted $sce.getTrusted(interpolatedResult, | |
* trustedContext)} before returning it. Refer to the {@link ng.$sce $sce} service that | |
* provides Strict Contextual Escaping for details. | |
* @returns {function(context)} an interpolation function which is used to compute the interpolated | |
* string. The function has these parameters: | |
* | |
* * `context`: an object against which any expressions embedded in the strings are evaluated | |
* against. | |
* | |
*/ | |
function $interpolate(text, mustHaveExpression, trustedContext) { | |
var startIndex, | |
endIndex, | |
index = 0, | |
parts = [], | |
length = text.length, | |
hasInterpolation = false, | |
fn, | |
exp, | |
concat = []; | |
while(index < length) { | |
if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && | |
((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { | |
(index != startIndex) && parts.push(text.substring(index, startIndex)); | |
parts.push(fn = $parse(exp = text.substring(startIndex + startSymbolLength, endIndex))); | |
fn.exp = exp; | |
index = endIndex + endSymbolLength; | |
hasInterpolation = true; | |
} else { | |
// we did not find anything, so we have to add the remainder to the parts array | |
(index != length) && parts.push(text.substring(index)); | |
index = length; | |
} | |
} | |
if (!(length = parts.length)) { | |
// we added, nothing, must have been an empty string. | |
parts.push(''); | |
length = 1; | |
} | |
// Concatenating expressions makes it hard to reason about whether some combination of concatenated | |
// values are unsafe to use and could easily lead to XSS. By requiring that a single | |
// expression be used for iframe[src], object[src], etc., we ensure that the value that's used | |
// is assigned or constructed by some JS code somewhere that is more testable or make it | |
// obvious that you bound the value to some user controlled value. This helps reduce the load | |
// when auditing for XSS issues. | |
if (trustedContext && parts.length > 1) { | |
throw $interpolateMinErr('noconcat', | |
"Error while interpolating: {0}\nStrict Contextual Escaping disallows " + | |
"interpolations that concatenate multiple expressions when a trusted value is " + | |
"required. See http://docs.angularjs.org/api/ng.$sce", text); | |
} | |
if (!mustHaveExpression || hasInterpolation) { | |
concat.length = length; | |
fn = function(context) { | |
try { | |
for(var i = 0, ii = length, part; i<ii; i++) { | |
if (typeof (part = parts[i]) == 'function') { | |
part = part(context); | |
if (trustedContext) { | |
part = $sce.getTrusted(trustedContext, part); | |
} else { | |
part = $sce.valueOf(part); | |
} | |
if (part == null || part == undefined) { | |
part = ''; | |
} else if (typeof part != 'string') { | |
part = toJson(part); | |
} | |
} | |
concat[i] = part; | |
} | |
return concat.join(''); | |
} | |
catch(err) { | |
var newErr = $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString()); | |
$exceptionHandler(newErr); | |
} | |
}; | |
fn.exp = text; | |
fn.parts = parts; | |
return fn; | |
} | |
} | |
/** | |
* @ngdoc method | |
* @name ng.$interpolate#startSymbol | |
* @methodOf ng.$interpolate | |
* @description | |
* Symbol to denote the start of expression in the interpolated string. Defaults to `{{`. | |
* | |
* Use {@link ng.$interpolateProvider#startSymbol $interpolateProvider#startSymbol} to change | |
* the symbol. | |
* | |
* @returns {string} start symbol. | |
*/ | |
$interpolate.startSymbol = function() { | |
return startSymbol; | |
} | |
/** | |
* @ngdoc method | |
* @name ng.$interpolate#endSymbol | |
* @methodOf ng.$interpolate | |
* @description | |
* Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. | |
* | |
* Use {@link ng.$interpolateProvider#endSymbol $interpolateProvider#endSymbol} to change | |
* the symbol. | |
* | |
* @returns {string} start symbol. | |
*/ | |
$interpolate.endSymbol = function() { | |
return endSymbol; | |
} | |
return $interpolate; | |
}]; | |
} | |
function $IntervalProvider() { | |
this.$get = ['$rootScope', '$window', '$q', | |
function($rootScope, $window, $q) { | |
var intervals = {}; | |
/** | |
* @ngdoc function | |
* @name ng.$interval | |
* | |
* @description | |
* Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` | |
* milliseconds. | |
* | |
* The return value of registering an interval function is a promise. This promise will be | |
* notified upon each tick of the interval, and will be resolved after `count` iterations, or | |
* run indefinitely if `count` is not defined. The value of the notification will be the | |
* number of iterations that have run. | |
* To cancel an interval, call `$interval.cancel(promise)`. | |
* | |
* In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to | |
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that | |
* time. | |
* | |
* @param {function()} fn A function that should be called repeatedly. | |
* @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. | |
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise | |
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. | |
* @returns {promise} A promise which will be notified on each iteration. | |
*/ | |
function interval(fn, delay, count, invokeApply) { | |
var setInterval = $window.setInterval, | |
clearInterval = $window.clearInterval; | |
var deferred = $q.defer(), | |
promise = deferred.promise, | |
count = (isDefined(count)) ? count : 0, | |
iteration = 0, | |
skipApply = (isDefined(invokeApply) && !invokeApply); | |
promise.then(null, null, fn); | |
promise.$$intervalId = setInterval(function tick() { | |
deferred.notify(iteration++); | |
if (count > 0 && iteration >= count) { | |
deferred.resolve(iteration); | |
clearInterval(promise.$$intervalId); | |
delete intervals[promise.$$intervalId]; | |
} | |
if (!skipApply) $rootScope.$apply(); | |
}, delay); | |
intervals[promise.$$intervalId] = deferred; | |
return promise; | |
} | |
/** | |
* @ngdoc function | |
* @name ng.$interval#cancel | |
* @methodOf ng.$interval | |
* | |
* @description | |
* Cancels a task associated with the `promise`. | |
* | |
* @param {number} promise Promise returned by the `$interval` function. | |
* @returns {boolean} Returns `true` if the task was successfully canceled. | |
*/ | |
interval.cancel = function(promise) { | |
if (promise && promise.$$intervalId in intervals) { | |
intervals[promise.$$intervalId].reject('canceled'); | |
clearInterval(promise.$$intervalId); | |
delete intervals[promise.$$intervalId]; | |
return true; | |
} | |
return false; | |
}; | |
return interval; | |
}]; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$locale | |
* | |
* @description | |
* $locale service provides localization rules for various Angular components. As of right now the | |
* only public api is: | |
* | |
* * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`) | |
*/ | |
function $LocaleProvider(){ | |
this.$get = function() { | |
return { | |
id: 'en-us', | |
NUMBER_FORMATS: { | |
DECIMAL_SEP: '.', | |
GROUP_SEP: ',', | |
PATTERNS: [ | |
{ // Decimal Pattern | |
minInt: 1, | |
minFrac: 0, | |
maxFrac: 3, | |
posPre: '', | |
posSuf: '', | |
negPre: '-', | |
negSuf: '', | |
gSize: 3, | |
lgSize: 3 | |
},{ //Currency Pattern | |
minInt: 1, | |
minFrac: 2, | |
maxFrac: 2, | |
posPre: '\u00A4', | |
posSuf: '', | |
negPre: '(\u00A4', | |
negSuf: ')', | |
gSize: 3, | |
lgSize: 3 | |
} | |
], | |
CURRENCY_SYM: '$' | |
}, | |
DATETIME_FORMATS: { | |
MONTH: 'January,February,March,April,May,June,July,August,September,October,November,December' | |
.split(','), | |
SHORTMONTH: 'Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec'.split(','), | |
DAY: 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(','), | |
SHORTDAY: 'Sun,Mon,Tue,Wed,Thu,Fri,Sat'.split(','), | |
AMPMS: ['AM','PM'], | |
medium: 'MMM d, y h:mm:ss a', | |
short: 'M/d/yy h:mm a', | |
fullDate: 'EEEE, MMMM d, y', | |
longDate: 'MMMM d, y', | |
mediumDate: 'MMM d, y', | |
shortDate: 'M/d/yy', | |
mediumTime: 'h:mm:ss a', | |
shortTime: 'h:mm a' | |
}, | |
pluralCat: function(num) { | |
if (num === 1) { | |
return 'one'; | |
} | |
return 'other'; | |
} | |
}; | |
}; | |
} | |
var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/, | |
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; | |
var $locationMinErr = minErr('$location'); | |
/** | |
* Encode path using encodeUriSegment, ignoring forward slashes | |
* | |
* @param {string} path Path to encode | |
* @returns {string} | |
*/ | |
function encodePath(path) { | |
var segments = path.split('/'), | |
i = segments.length; | |
while (i--) { | |
segments[i] = encodeUriSegment(segments[i]); | |
} | |
return segments.join('/'); | |
} | |
function parseAbsoluteUrl(absoluteUrl, locationObj) { | |
var parsedUrl = urlResolve(absoluteUrl); | |
locationObj.$$protocol = parsedUrl.protocol; | |
locationObj.$$host = parsedUrl.hostname; | |
locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null; | |
} | |
function parseAppUrl(relativeUrl, locationObj) { | |
var prefixed = (relativeUrl.charAt(0) !== '/'); | |
if (prefixed) { | |
relativeUrl = '/' + relativeUrl; | |
} | |
var match = urlResolve(relativeUrl); | |
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) != '/') locationObj.$$path = '/' + locationObj.$$path; | |
} | |
/** | |
* | |
* @param {string} begin | |
* @param {string} whole | |
* @returns {string} returns text from whole after begin or undefined if it does not begin with expected string. | |
*/ | |
function beginsWith(begin, whole) { | |
if (whole.indexOf(begin) == 0) { | |
return whole.substr(begin.length); | |
} | |
} | |
function stripHash(url) { | |
var index = url.indexOf('#'); | |
return index == -1 ? url : url.substr(0, index); | |
} | |
function stripFile(url) { | |
return url.substr(0, stripHash(url).lastIndexOf('/') + 1); | |
} | |
/* return the server only (scheme://host:port) */ | |
function serverBase(url) { | |
return url.substring(0, url.indexOf('/', url.indexOf('//') + 2)); | |
} | |
/** | |
* LocationHtml5Url represents an url | |
* This object is exposed as $location service when HTML5 mode is enabled and supported | |
* | |
* @constructor | |
* @param {string} appBase application base URL | |
* @param {string} basePrefix url path prefix | |
*/ | |
function LocationHtml5Url(appBase, basePrefix) { | |
this.$$html5 = true; | |
basePrefix = basePrefix || ''; | |
var appBaseNoFile = stripFile(appBase); | |
parseAbsoluteUrl(appBase, this); | |
/** | |
* Parse given html5 (regular) url string into properties | |
* @param {string} newAbsoluteUrl HTML5 url | |
* @private | |
*/ | |
this.$$parse = function(url) { | |
var pathUrl = beginsWith(appBaseNoFile, url); | |
if (!isString(pathUrl)) { | |
throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url, appBaseNoFile); | |
} | |
parseAppUrl(pathUrl, this); | |
if (!this.$$path) { | |
this.$$path = '/'; | |
} | |
this.$$compose(); | |
}; | |
/** | |
* Compose url and update `absUrl` property | |
* @private | |
*/ | |
this.$$compose = function() { | |
var search = toKeyValue(this.$$search), | |
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; | |
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; | |
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/' | |
}; | |
this.$$rewrite = function(url) { | |
var appUrl, prevAppUrl; | |
if ( (appUrl = beginsWith(appBase, url)) !== undefined ) { | |
prevAppUrl = appUrl; | |
if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) { | |
return appBaseNoFile + (beginsWith('/', appUrl) || appUrl); | |
} else { | |
return appBase + prevAppUrl; | |
} | |
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) { | |
return appBaseNoFile + appUrl; | |
} else if (appBaseNoFile == url + '/') { | |
return appBaseNoFile; | |
} | |
} | |
} | |
/** | |
* 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. | |
* | |
* @constructor | |
* @param {string} appBase application base URL | |
* @param {string} hashPrefix hashbang prefix | |
*/ | |
function LocationHashbangUrl(appBase, hashPrefix) { | |
var appBaseNoFile = stripFile(appBase); | |
parseAbsoluteUrl(appBase, this); | |
/** | |
* 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 withoutHashUrl = withoutBaseUrl.charAt(0) == '#' | |
? beginsWith(hashPrefix, withoutBaseUrl) | |
: (this.$$html5) | |
? withoutBaseUrl | |
: ''; | |
if (!isString(withoutHashUrl)) { | |
throw $locationMinErr('ihshprfx', 'Invalid url "{0}", missing hash prefix "{1}".', url, hashPrefix); | |
} | |
parseAppUrl(withoutHashUrl, this); | |
this.$$compose(); | |
}; | |
/** | |
* Compose hashbang url and update `absUrl` property | |
* @private | |
*/ | |
this.$$compose = function() { | |
var search = toKeyValue(this.$$search), | |
hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; | |
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; | |
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : ''); | |
}; | |
this.$$rewrite = function(url) { | |
if(stripHash(appBase) == stripHash(url)) { | |
return url; | |
} | |
} | |
} | |
/** | |
* LocationHashbangUrl represents url | |
* This object is exposed as $location service when html5 history api is enabled but the browser | |
* does not support it. | |
* | |
* @constructor | |
* @param {string} appBase application base URL | |
* @param {string} hashPrefix hashbang prefix | |
*/ | |
function LocationHashbangInHtml5Url(appBase, hashPrefix) { | |
this.$$html5 = true; | |
LocationHashbangUrl.apply(this, arguments); | |
var appBaseNoFile = stripFile(appBase); | |
this.$$rewrite = function(url) { | |
var appUrl; | |
if ( appBase == stripHash(url) ) { | |
return url; | |
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) { | |
return appBase + hashPrefix + appUrl; | |
} else if ( appBaseNoFile === url + '/') { | |
return appBaseNoFile; | |
} | |
} | |
} | |
LocationHashbangInHtml5Url.prototype = | |
LocationHashbangUrl.prototype = | |
LocationHtml5Url.prototype = { | |
/** | |
* Are we in html5 mode? | |
* @private | |
*/ | |
$$html5: false, | |
/** | |
* Has any change been replacing ? | |
* @private | |
*/ | |
$$replace: false, | |
/** | |
* @ngdoc method | |
* @name ng.$location#absUrl | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter only. | |
* | |
* Return full url representation with all segments encoded according to rules specified in | |
* {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}. | |
* | |
* @return {string} full url | |
*/ | |
absUrl: locationGetter('$$absUrl'), | |
/** | |
* @ngdoc method | |
* @name ng.$location#url | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter / setter. | |
* | |
* 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`. | |
* | |
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) | |
* @param {string=} replace The path that will be changed | |
* @return {string} url | |
*/ | |
url: function(url, replace) { | |
if (isUndefined(url)) | |
return this.$$url; | |
var match = PATH_MATCH.exec(url); | |
if (match[1]) this.path(decodeURIComponent(match[1])); | |
if (match[2] || match[1]) this.search(match[3] || ''); | |
this.hash(match[5] || '', replace); | |
return this; | |
}, | |
/** | |
* @ngdoc method | |
* @name ng.$location#protocol | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter only. | |
* | |
* Return protocol of current url. | |
* | |
* @return {string} protocol of current url | |
*/ | |
protocol: locationGetter('$$protocol'), | |
/** | |
* @ngdoc method | |
* @name ng.$location#host | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter only. | |
* | |
* Return host of current url. | |
* | |
* @return {string} host of current url. | |
*/ | |
host: locationGetter('$$host'), | |
/** | |
* @ngdoc method | |
* @name ng.$location#port | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter only. | |
* | |
* Return port of current url. | |
* | |
* @return {Number} port | |
*/ | |
port: locationGetter('$$port'), | |
/** | |
* @ngdoc method | |
* @name ng.$location#path | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter / setter. | |
* | |
* Return path of current url when called without any parameter. | |
* | |
* Change path when called with parameter and return `$location`. | |
* | |
* Note: Path should always begin with forward slash (/), this method will add the forward slash | |
* if it is missing. | |
* | |
* @param {string=} path New path | |
* @return {string} path | |
*/ | |
path: locationGetterSetter('$$path', function(path) { | |
return path.charAt(0) == '/' ? path : '/' + path; | |
}), | |
/** | |
* @ngdoc method | |
* @name ng.$location#search | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter / setter. | |
* | |
* Return search part (as object) of current url when called without any parameter. | |
* | |
* Change search part when called with parameter and return `$location`. | |
* | |
* @param {string|Object.<string>|Object.<Array.<string>>} search New search params - string or hash object. Hash object | |
* may contain an array of values, which will be decoded as duplicates in the url. | |
* @param {string=} paramValue If `search` is a string, then `paramValue` will override only a | |
* single search parameter. If the value is `null`, the parameter will be deleted. | |
* | |
* @return {string} search | |
*/ | |
search: function(search, paramValue) { | |
switch (arguments.length) { | |
case 0: | |
return this.$$search; | |
case 1: | |
if (isString(search)) { | |
this.$$search = parseKeyValue(search); | |
} else if (isObject(search)) { | |
this.$$search = search; | |
} else { | |
throw $locationMinErr('isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.'); | |
} | |
break; | |
default: | |
if (paramValue == undefined || paramValue == null) { | |
delete this.$$search[search]; | |
} else { | |
this.$$search[search] = paramValue; | |
} | |
} | |
this.$$compose(); | |
return this; | |
}, | |
/** | |
* @ngdoc method | |
* @name ng.$location#hash | |
* @methodOf ng.$location | |
* | |
* @description | |
* This method is getter / setter. | |
* | |
* Return hash fragment when called without any parameter. | |
* | |
* Change hash fragment when called with parameter and return `$location`. | |
* | |
* @param {string=} hash New hash fragment | |
* @return {string} hash | |
*/ | |
hash: locationGetterSetter('$$hash', identity), | |
/** | |
* @ngdoc method | |
* @name ng.$location#replace | |
* @methodOf ng.$location | |
* | |
* @description | |
* If called, all changes to $location during current `$digest` will be replacing current history | |
* record, instead of adding new one. | |
*/ | |
replace: function() { | |
this.$$replace = true; | |
return this; | |
} | |
}; | |
function locationGetter(property) { | |
return function() { | |
return this[property]; | |
}; | |
} | |
function locationGetterSetter(property, preprocess) { | |
return function(value) { | |
if (isUndefined(value)) | |
return this[property]; | |
this[property] = preprocess(value); | |
this.$$compose(); | |
return this; | |
}; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$location | |
* | |
* @requires $browser | |
* @requires $sniffer | |
* @requires $rootElement | |
* | |
* @description | |
* The $location service parses the URL in the browser address bar (based on the | |
* {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL | |
* available to your application. Changes to the URL in the address bar are reflected into | |
* $location service and changes to $location are reflected into the browser address bar. | |
* | |
* **The $location service:** | |
* | |
* - Exposes the current URL in the browser address bar, so you can | |
* - Watch and observe the URL. | |
* - Change the URL. | |
* - Synchronizes the URL with the browser when the user | |
* - Changes the address bar. | |
* - Clicks the back or forward button (or clicks a History link). | |
* - Clicks on a link. | |
* - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). | |
* | |
* For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular | |
* Services: Using $location} | |
*/ | |
/** | |
* @ngdoc object | |
* @name ng.$locationProvider | |
* @description | |
* Use the `$locationProvider` to configure how the application deep linking paths are stored. | |
*/ | |
function $LocationProvider(){ | |
var hashPrefix = '', | |
html5Mode = false; | |
/** | |
* @ngdoc property | |
* @name ng.$locationProvider#hashPrefix | |
* @methodOf ng.$locationProvider | |
* @description | |
* @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 | |
*/ | |
this.hashPrefix = function(prefix) { | |
if (isDefined(prefix)) { | |
hashPrefix = prefix; | |
return this; | |
} else { | |
return hashPrefix; | |
} | |
}; | |
/** | |
* @ngdoc property | |
* @name ng.$locationProvider#html5Mode | |
* @methodOf ng.$locationProvider | |
* @description | |
* @param {boolean=} mode Use HTML5 strategy if available. | |
* @returns {*} current value if used as getter or itself (chaining) if used as setter | |
*/ | |
this.html5Mode = function(mode) { | |
if (isDefined(mode)) { | |
html5Mode = mode; | |
return this; | |
} else { | |
return html5Mode; | |
} | |
}; | |
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', | |
function( $rootScope, $browser, $sniffer, $rootElement) { | |
var $location, | |
LocationMode, | |
baseHref = $browser.baseHref(), // if base[href] is undefined, it defaults to '' | |
initialUrl = $browser.url(), | |
appBase; | |
if (html5Mode) { | |
appBase = serverBase(initialUrl) + (baseHref || '/'); | |
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url; | |
} else { | |
appBase = stripHash(initialUrl); | |
LocationMode = LocationHashbangUrl; | |
} | |
$location = new LocationMode(appBase, '#' + hashPrefix); | |
$location.$$parse($location.$$rewrite(initialUrl)); | |
$rootElement.on('click', function(event) { | |
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) | |
// currently we open nice url link and redirect then | |
if (event.ctrlKey || event.metaKey || event.which == 2) return; | |
var elm = jqLite(event.target); | |
// traverse the DOM up to find first A tag | |
while (lowercase(elm[0].nodeName) !== 'a') { | |
// ignore rewriting if no A tag (reached root element, or no parent - removed from document) | |
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return; | |
} | |
var absHref = elm.prop('href'); | |
var rewrittenUrl = $location.$$rewrite(absHref); | |
if (absHref && !elm.attr('target') && rewrittenUrl && !event.isDefaultPrevented()) { | |
event.preventDefault(); | |
if (rewrittenUrl != $browser.url()) { | |
// update location manually | |
$location.$$parse(rewrittenUrl); | |
$rootScope.$apply(); | |
// hack to work around FF6 bug 684208 when scenario runner clicks on links | |
window.angular['ff-684208-preventDefault'] = true; | |
} | |
} | |
}); | |
// rewrite hashbang url <> html5 url | |
if ($location.absUrl() != initialUrl) { | |
$browser.url($location.absUrl(), true); | |
} | |
// update $location when $browser url changes | |
$browser.onUrlChange(function(newUrl) { | |
if ($location.absUrl() != newUrl) { | |
if ($rootScope.$broadcast('$locationChangeStart', newUrl, $location.absUrl()).defaultPrevented) { | |
$browser.url($location.absUrl()); | |
return; | |
} | |
$rootScope.$evalAsync(function() { | |
var oldUrl = $location.absUrl(); | |
$location.$$parse(newUrl); | |
afterLocationChange(oldUrl); | |
}); | |
if (!$rootScope.$$phase) $rootScope.$digest(); | |
} | |
}); | |
// update browser | |
var changeCounter = 0; | |
$rootScope.$watch(function $locationWatch() { | |
var oldUrl = $browser.url(); | |
var currentReplace = $location.$$replace; | |
if (!changeCounter || oldUrl != $location.absUrl()) { | |
changeCounter++; | |
$rootScope.$evalAsync(function() { | |
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). | |
defaultPrevented) { | |
$location.$$parse(oldUrl); | |
} else { | |
$browser.url($location.absUrl(), currentReplace); | |
afterLocationChange(oldUrl); | |
} | |
}); | |
} | |
$location.$$replace = false; | |
return changeCounter; | |
}); | |
return $location; | |
function afterLocationChange(oldUrl) { | |
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); | |
} | |
}]; | |
} | |
/** | |
* @ngdoc object | |
* @name ng.$log | |
* @requires $window | |
* | |
* @description | |
* Simple service for logging. Default implementation writes the message | |
* into the browser's console (if present). | |
* | |
* The main purpose of this service is to simplify debugging and troubleshooting. | |
* | |
* The default is not to log `debug` messages. You can use | |
* {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this. | |
* | |
* @example | |
<example> | |
<file name="script.js"> | |
function LogCtrl($scope, $log) { | |
$scope.$log = $log; | |
$scope.message = 'Hello World!'; | |
} | |
</file> | |
<file name="index.html"> | |
<div ng-controller="LogCtrl"> | |
<p>Reload this page with open console, enter text and hit the log button...</p> | |
Message: | |
<input type="text" ng-model="message"/> | |
<button ng-click="$log.log(message)">log</button> | |
<button ng-click="$log.warn(message)">warn</button> | |
<button ng-click="$log.info(message)">info</button> | |
<button ng-click="$log.error(message)">error</button> | |
</div> | |
</file> | |
</example> | |
*/ | |
/** | |
* @ngdoc object | |
* @name ng.$logProvider | |
* @description | |
* Use the `$logProvider` to configure how the application logs messages | |
*/ | |
function $LogProvider(){ | |
var debug = true, | |
self = this; | |
/** | |
* @ngdoc property | |
* @name ng.$logProvider#debugEnabled | |
* @methodOf ng.$logProvider | |
* @description | |
* @param {string=} flag enable or disable debug level messages | |
* @returns {*} current value if used as getter or itself (chaining) if used as setter | |
*/ | |
this.debugEnabled = function(flag) { | |
if (isDefined(flag)) { | |
debug = flag; | |
return this; | |
} else { | |
return debug; | |
} | |
}; | |
this.$get = ['$window', function($window){ | |
return { | |
/** | |
* @ngdoc me |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment