/**
* The MIT License
*
* Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
(function(window, document, undefined){
////////////////////////////////////
if (typeof document.getAttribute == $undefined)
document.getAttribute = function() {};
/**
* @workInProgress
* @ngdoc function
* @name angular.lowercase
* @function
*
* @description Converts string to lowercase
* @param {string} string String to be lowercased.
* @returns {string} Lowercased string.
*/
var lowercase = function (string){ return isString(string) ? string.toLowerCase() : string; };
/**
* @workInProgress
* @ngdoc function
* @name angular.uppercase
* @function
*
* @description Converts string to uppercase.
* @param {string} string String to be uppercased.
* @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 fromCharCode(ch.charCodeAt(0) | 32); })
: s;
};
var manualUppercase = function (s) {
return isString(s)
? s.replace(/[a-z]/g, function (ch) {return 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;
}
function fromCharCode(code) { return String.fromCharCode(code); }
var _undefined = undefined,
_null = null,
$$element = '$element',
$$scope = '$scope',
$$validate = '$validate',
$angular = 'angular',
$array = 'array',
$boolean = 'boolean',
$console = 'console',
$date = 'date',
$display = 'display',
$element = 'element',
$function = 'function',
$length = 'length',
$name = 'name',
$none = 'none',
$noop = 'noop',
$null = 'null',
$number = 'number',
$object = 'object',
$string = 'string',
$value = 'value',
$selected = 'selected',
$undefined = 'undefined',
NG_EXCEPTION = 'ng-exception',
NG_VALIDATION_ERROR = 'ng-validation-error',
NOOP = 'noop',
PRIORITY_FIRST = -99999,
PRIORITY_WATCH = -1000,
PRIORITY_LAST = 99999,
PRIORITY = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH},
Error = window.Error,
/** holds major version number for IE or NaN for real browsers */
msie = parseInt((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1], 10),
jqLite, // delay binding since jQuery could be loaded after us.
jQuery, // delay binding
slice = [].slice,
push = [].push,
error = window[$console]
? bind(window[$console], window[$console]['error'] || noop)
: noop,
/** @name angular */
angular = window[$angular] || (window[$angular] = {}),
/** @name angular.markup */
angularTextMarkup = extensionMap(angular, 'markup'),
/** @name angular.attrMarkup */
angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
/** @name angular.directive */
angularDirective = extensionMap(angular, 'directive'),
/** @name angular.widget */
angularWidget = extensionMap(angular, 'widget', lowercase),
/** @name angular.validator */
angularValidator = extensionMap(angular, 'validator'),
/** @name angular.fileter */
angularFilter = extensionMap(angular, 'filter'),
/** @name angular.formatter */
angularFormatter = extensionMap(angular, 'formatter'),
/** @name angular.service */
angularService = extensionMap(angular, 'service'),
angularCallbacks = extensionMap(angular, 'callbacks'),
nodeName_,
rngScript = /^(|.*\/)angular(-.*?)?(\.min)?.js(\?[^#]*)?(#(.*))?$/,
uid = ['0', '0', '0'];
DATE_ISOSTRING_LN = 24;
/**
* @workInProgress
* @ngdoc function
* @name angular.forEach
* @function
*
* @description
* Invokes the `iterator` function once for each item in `obj` collection. The collection can either
* be 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. Optionally, `context` can be specified for the iterator function.
*
* Note: this function was previously known as `angular.foreach`.
*
*
* @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 {Objet|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 (isObject(obj) && isNumber(obj.length)) {
for (key = 0; key < obj.length; key++)
iterator.call(context, obj[key], key);
} else {
for (key in obj)
iterator.call(context, obj[key], key);
}
}
return obj;
}
function forEachSorted(obj, iterator, context) {
var keys = [];
for (var key in obj) keys.push(key);
keys.sort();
for ( var i = 0; i < keys.length; i++) {
iterator.call(context, obj[keys[i]], keys[i]);
}
return keys;
}
function formatError(arg) {
if (arg instanceof Error) {
if (arg.stack) {
arg = (arg.message && arg.stack.indexOf(arg.message) === -1) ?
'Error: ' + arg.message + '\n' + arg.stack : arg.stack;
} else if (arg.sourceURL) {
arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line;
}
}
return arg;
}
/**
* @description
* 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 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('');
}
/**
* @workInProgress
* @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 The destination object.
* @param {...Object} src The source object(s).
*/
function extend(dst) {
forEach(arguments, function(obj){
if (obj !== dst) {
forEach(obj, function(value, key){
dst[key] = value;
});
}
});
return dst;
}
function inherit(parent, extra) {
return extend(new (extend(function(){}, {prototype:parent}))(), extra);
}
/**
* @workInProgress
* @ngdoc function
* @name angular.noop
* @function
*
* @description
* Empty function that performs no operation whatsoever. This function is useful when writing code
* in the functional style.
function foo(callback) {
var result = calculateResult();
(callback || angular.noop)(result);
}
*/
function noop() {}
/**
* @workInProgress
* @ngdoc function
* @name angular.identity
* @function
*
* @description
* A function that does nothing except for returning its first argument. This function is useful
* when writing code in the functional style.
*
function transformer(transformationFn, value) {
return (transformationFn || identity)(value);
};
*/
function identity($) {return $;}
function valueFn(value) {return function(){ return value; };}
function extensionMap(angular, name, transform) {
var extPoint;
return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
name = (transform || identity)(name);
if (isDefined(fn)) {
extPoint[name] = extend(fn, prop || {});
}
return extPoint[name];
});
}
/**
* @workInProgress
* @ngdoc function
* @name angular.isUndefined
* @function
*
* @description
* Checks if a reference is undefined.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is undefined.
*/
function isUndefined(value){ return typeof value == $undefined; }
/**
* @workInProgress
* @ngdoc function
* @name angular.isDefined
* @function
*
* @description
* Checks if a reference is defined.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is defined.
*/
function isDefined(value){ return typeof value != $undefined; }
/**
* @workInProgress
* @ngdoc function
* @name angular.isObject
* @function
*
* @description
* Checks if a reference is an `Object`. Unlike 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;}
/**
* @workInProgress
* @ngdoc function
* @name angular.isString
* @function
*
* @description
* Checks 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;}
/**
* @workInProgress
* @ngdoc function
* @name angular.isNumber
* @function
*
* @description
* Checks 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;}
/**
* @workInProgress
* @ngdoc function
* @name angular.isDate
* @function
*
* @description
* Checks if value is a date.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is a `Date`.
*/
function isDate(value){ return value instanceof Date; }
/**
* @workInProgress
* @ngdoc function
* @name angular.isArray
* @function
*
* @description
* Checks if a reference is an `Array`.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
*/
function isArray(value) { return value instanceof Array; }
/**
* @workInProgress
* @ngdoc function
* @name angular.isFunction
* @function
*
* @description
* Checks 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;}
/**
* 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 isBoolean(value) { return typeof value == $boolean;}
function isTextNode(node) { return nodeName_(node) == '#text'; }
function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; }
function isElement(node) {
return node &&
(node.nodeName // we are a direct element
|| (node.bind && node.find)); // we have a bind 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;
}
/**
* HTML class which is the only class which can be used in ng:bind to inline HTML for security reasons.
* @constructor
* @param html raw (unsafe) html
* @param {string=} option if set to 'usafe' then get method will return raw (unsafe/unsanitized) html
*/
function HTML(html, option) {
this.html = html;
this.get = lowercase(option) == 'unsafe'
? valueFn(html)
: function htmlSanitize() {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf));
return buf.join('');
};
}
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 isVisible(element) {
var rect = element[0].getBoundingClientRect(),
width = (rect.width || (rect.right||0 - rect.left||0)),
height = (rect.height || (rect.bottom||0 - rect.top||0));
return width>0 && height>0;
}
function map(obj, iterator, context) {
var results = [];
forEach(obj, function(value, index, list) {
results.push(iterator.call(context, value, index, list));
});
return results;
}
/**
* @ngdoc function
* @name angular.Object.size
* @function
*
* @description
* Determines the number of elements in an array, number of properties of an object or string
* length.
*
* Note: this function is used to augment the Object type in angular expressions. See
* {@link angular.Object} for more info.
*
* @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 or an array.
*
* @example
*
*
* Number of items in array: {{ [1,2].$size() }}
* Number of items in object: {{ {a:1, b:2, c:3}.$size() }}
*
*
* it('should print correct sizes for an array and an object', function() {
* expect(binding('[1,2].$size()')).toBe('2');
* expect(binding('{a:1, b:2, c:3}.$size()')).toBe('3');
* });
*
*
*/
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) {
for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return true;
}
return false;
}
function indexOf(array, obj) {
for ( var i = 0; i < array.length; i++) {
if (obj === array[i]) return i;
}
return -1;
}
function isLeafNode (node) {
if (node) {
switch (node.nodeName) {
case "OPTION":
case "PRE":
case "TITLE":
return true;
}
}
return false;
}
/**
* @ngdoc function
* @name angular.Object.copy
* @function
*
* @description
* Creates a deep copy of `source`.
*
* If `source` is an object or an array, all of its members will be copied into the `destination`
* object.
*
* If `destination` is not provided and `source` is an object or an array, a copy is created &
* returned, otherwise the `source` is returned.
*
* If `destination` is provided, all of its properties will be deleted.
*
* Note: this function is used to augment the Object type in angular expressions. See
* {@link angular.Object} for more info.
*
* @param {*} source The source to be used to make a copy.
* Can be any type including primitives, `null` and `undefined`.
* @param {(Object|Array)=} destination Optional 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
*
*
Salutation:
Name:
The master object is NOT equal to the form object.
master={{master}}
form={{form}}
*
*
it('should print that initialy the form object is NOT equal to master', function() {
expect(element('.doc-example-live input[name=master.salutation]').val()).toBe('Hello');
expect(element('.doc-example-live input[name=master.name]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('inline');
});
it('should make form and master equal when the copy button is clicked', function() {
element('.doc-example-live button').click();
expect(element('.doc-example-live span').css('display')).toBe('none');
});
*
*
*/
function copy(source, destination){
if (!destination) {
destination = source;
if (source) {
if (isArray(source)) {
destination = copy(source, []);
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isObject(source)) {
destination = copy(source, {});
}
}
} else {
if (isArray(source)) {
while(destination.length) {
destination.pop();
}
for ( var i = 0; i < source.length; i++) {
destination.push(copy(source[i]));
}
} else {
forEach(destination, function(value, key){
delete destination[key];
});
for ( var key in source) {
destination[key] = copy(source[key]);
}
}
}
return destination;
}
/**
* @ngdoc function
* @name angular.Object.equals
* @function
*
* @description
* Determines if two objects or value are equivalent.
*
* To be equivalent, they must pass `==` comparison or be of the same type and have all their
* properties pass `==` comparison. During property comparision properties of `function` type and
* properties with name starting with `$` are ignored.
*
* Supports values types, arrays and objects.
*
* Note: this function is used to augment the Object type in angular expressions. See
* {@link angular.Object} for more info.
*
* @param {*} o1 Object or value to compare.
* @param {*} o2 Object or value to compare.
* @returns {boolean} True if arguments are equal.
*
* @example
*
*
Salutation:
Name:
The greeting object is
NOT equal to
{salutation:'Hello', name:'world'}.
greeting={{greeting}}
*
*
it('should print that initialy greeting is equal to the hardcoded value object', function() {
expect(element('.doc-example-live input[name=greeting.salutation]').val()).toBe('Hello');
expect(element('.doc-example-live input[name=greeting.name]').val()).toBe('world');
expect(element('.doc-example-live span').css('display')).toBe('none');
});
it('should say that the objects are not equal when the form is modified', function() {
input('greeting.name').enter('kitty');
expect(element('.doc-example-live span').css('display')).toBe('inline');
});
*
*
*/
function equals(o1, o2) {
if (o1 == o2) return true;
if (o1 === null || o2 === null) return false;
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
if (t1 == t2 && t1 == 'object') {
if (o1 instanceof Array) {
if ((length = o1.length) == o2.length) {
for(key=0; key 2
? slice.call(arguments, 2, arguments.length)
: [];
if (typeof fn == $function && !(fn instanceof RegExp)) {
return curryArgs.length
? function() {
return arguments.length
? fn.apply(self, curryArgs.concat(slice.call(arguments, 0, arguments.length)))
: fn.apply(self, curryArgs);
}
: function() {
return arguments.length
? fn.apply(self, arguments)
: fn.call(self);
};
} else {
// in IE, native methods are not functions and so they can not be bound (but they don't need to be)
return fn;
}
}
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;
}
function merge(src, dst) {
for ( var key in src) {
var value = dst[key];
var type = typeof value;
if (type == $undefined) {
dst[key] = fromJson(toJson(src[key]));
} else if (type == 'object' && value.constructor != array &&
key.substring(0, 1) != "$") {
merge(src[key], value);
}
}
}
/** @name angular.compile */
function compile(element) {
return new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget)
.compile(element);
}
/////////////////////////////////////////////////
/**
* 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 = unescape(key_value[0]);
obj[key] = isDefined(key_value[1]) ? unescape(key_value[1]) : true;
}
});
return obj;
}
function toKeyValue(obj) {
var parts = [];
forEach(obj, function(value, key) {
parts.push(escape(key) + (value === true ? '' : '=' + escape(value)));
});
return parts.length ? parts.join('&') : '';
}
/**
* we need our custom mehtod because encodeURIComponent is too agressive 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 becuase encodeURIComponent is too agressive 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((pctEncodeSpaces ? null : /%20/g), '+');
}
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:autobind
* @element script
*
* @TODO ng:autobind is not a directive!! it should be documented as bootstrap parameter in a
* separate bootstrap section.
* @TODO rename to ng:autobind to ng:autoboot
*
* @description
*
* `ng:autobind` with no parameters tells angular to compile and manage the whole page.
*
* `ng:autobind="[root element ID]"` tells angular to compile and manage part of the docucment,
* starting at "root element ID".
*
* For details on bootstrapping angular, see {@link guide/dev_guide.bootstrap Initializing Angular}
* in the Angular Developer Guide.
*/
function angularInit(config, document){
var autobind = config.autobind;
if (autobind) {
var element = isString(autobind) ? document.getElementById(autobind) : document,
scope = compile(element)(createScope({'$config':config})),
$browser = scope.$service('$browser');
if (config.css)
$browser.addCss(config.base_url + config.css);
else if(msie<8)
$browser.addJs(config.base_url + config.ie_compat, config.ie_compat_id);
}
}
function angularJsConfig(document, config) {
bindJQuery();
var scripts = document.getElementsByTagName("script"),
match;
config = extend({
ie_compat_id: 'ng-ie-compat'
}, config);
for(var j = 0; j < scripts.length; j++) {
match = (scripts[j].src || "").match(rngScript);
if (match) {
config.base_url = match[1];
config.ie_compat = match[1] + 'angular-ie-compat' + (match[2] || '') + '.js';
extend(config, parseKeyValue(match[6]));
eachAttribute(jqLite(scripts[j]), function(value, name){
if (/^ng:/.exec(name)) {
name = name.substring(3).replace(/-/g, '_');
value = value || true;
config[name] = value;
}
});
}
}
return config;
}
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
});
} else {
jqLite = jqLiteWrap;
}
angular.element = jqLite;
}
/**
* throw error of the argument is falsy.
*/
function assertArg(arg, name, reason) {
if (!arg) {
var error = new Error("Argument '" + (name||'?') + "' is " +
(reason || "required"));
throw error;
}
}
function assertArgFn(arg, name) {
assertArg(isFunction(arg, name, 'not a function'));
}
var array = [].constructor;
/**
* @workInProgress
* @ngdoc function
* @name angular.toJson
* @function
*
* @description
* Serializes the input into a JSON formated string.
*
* @param {Object|Array|Date|string|number} obj Input to jsonify.
* @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace.
* @returns {string} Jsonified string representing `obj`.
*/
function toJson(obj, pretty) {
var buf = [];
toJsonArray(buf, obj, pretty ? "\n " : null, []);
return buf.join('');
}
/**
* @workInProgress
* @ngdoc function
* @name angular.fromJson
* @function
*
* @description
* Deserializes a string in the JSON format.
*
* @param {string} json JSON string to deserialize.
* @param {boolean} [useNative=false] Use native JSON parser if available
* @returns {Object|Array|Date|string|number} Deserialized thingy.
*/
function fromJson(json, useNative) {
if (!isString(json)) return json;
var obj;
try {
if (useNative && window.JSON && window.JSON.parse) {
obj = JSON.parse(json);
return transformDates(obj);
}
return parser(json, true).primary()();
} catch (e) {
error("fromJson error: ", json, e);
throw e;
}
// TODO make forEach optionally recursive and remove this function
function transformDates(obj) {
if (isString(obj) && obj.length === DATE_ISOSTRING_LN) {
return angularString.toDate(obj);
} else if (isArray(obj) || isObject(obj)) {
forEach(obj, function(val, name) {
obj[name] = transformDates(val);
});
}
return obj;
}
}
angular.toJson = toJson;
angular.fromJson = fromJson;
function toJsonArray(buf, obj, pretty, stack) {
if (isObject(obj)) {
if (obj === window) {
buf.push('WINDOW');
return;
}
if (obj === document) {
buf.push('DOCUMENT');
return;
}
if (includes(stack, obj)) {
buf.push('RECURSION');
return;
}
stack.push(obj);
}
if (obj === null) {
buf.push($null);
} else if (obj instanceof RegExp) {
buf.push(angular.String.quoteUnicode(obj.toString()));
} else if (isFunction(obj)) {
return;
} else if (isBoolean(obj)) {
buf.push('' + obj);
} else if (isNumber(obj)) {
if (isNaN(obj)) {
buf.push($null);
} else {
buf.push('' + obj);
}
} else if (isString(obj)) {
return buf.push(angular.String.quoteUnicode(obj));
} else if (isObject(obj)) {
if (isArray(obj)) {
buf.push("[");
var len = obj.length;
var sep = false;
for(var i=0; i
//copile the entire window.document and give me the scope bound to this template.
var rootSscope = angular.compile(window.document)();
//compile a piece of html
var rootScope2 = angular.compile(''
click me
')();
//compile a piece of html and retain reference to both the dom and scope
var template = angular.element('
click me
'),
scoope = angular.compile(view)();
//at this point template was transformed into a view
*
*
* @param {string|DOMElement} element Element or HTML to compile into a template function.
* @returns {function([scope][, cloneAttachFn])} a template function which is used to bind template
* (a DOM element/tree) to a scope. Where:
*
* * `scope` - A {@link angular.scope Scope} to bind to. If none specified, then a new
* root scope is created.
* * `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 approriate place. The `cloneAttachFn` is
* called as: `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 template function returns the scope to which the element is bound to. It is either
* the same scope as the one passed into the template function, or if none were provided it's the
* newly create scope.
*
* 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.
*
* var view = angular.element('
{{total}}
'),
* scope = angular.compile(view)();
*
*
* - 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:
*
* var original = angular.element('
{{total}}
'),
* scope = someParentScope.$new(),
* clone;
*
* angular.compile(original)(scope, function(clonedElement, scope) {
* clone = clonedElement;
* //attach the clone to DOM document at the right place
* });
*
* //now we have reference to the cloned DOM via `clone`
*
*
*
* Compiler Methods For Widgets and Directives:
*
* The following methods are available for use when you write your own widgets, directives,
* and markup. (Recall that the compile function's this is a reference to the compiler.)
*
* `compile(element)` - returns linker -
* Invoke a new instance of the compiler to compile a DOM element and return a linker function.
* You can apply the linker function to the original element or a clone of the original element.
* The linker function returns a scope.
*
* * `comment(commentText)` - returns element - Create a comment element.
*
* * `element(elementName)` - returns element - Create an element by name.
*
* * `text(text)` - returns element - Create a text element.
*
* * `descend([set])` - returns descend state (true or false). Get or set the current descend
* state. If true the compiler will descend to children elements.
*
* * `directives([set])` - returns directive state (true or false). Get or set the current
* directives processing state. The compiler will process directives only when directives set to
* true.
*
* For information on how the compiler works, see the
* {@link guide/dev_guide.compiler Angular HTML Compiler} section of the Developer Guide.
*/
function Compiler(markup, attrMarkup, directives, widgets){
this.markup = markup;
this.attrMarkup = attrMarkup;
this.directives = directives;
this.widgets = widgets;
}
Compiler.prototype = {
compile: function(templateElement) {
templateElement = jqLite(templateElement);
var index = 0,
template,
parent = templateElement.parent();
if (templateElement.length > 1) {
// https://github.com/angular/angular.js/issues/338
throw Error("Cannot compile multiple element roots: " +
jqLite('
').append(templateElement.clone()).html());
}
if (parent && parent[0]) {
parent = parent[0];
for(var i = 0; i < parent.childNodes.length; i++) {
if (parent.childNodes[i] == templateElement[0]) {
index = i;
}
}
}
template = this.templatize(templateElement, index, 0) || new Template();
return function(scope, cloneConnectFn){
// 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 element = cloneConnectFn
? JQLitePrototype.clone.call(templateElement) // IMPORTANT!!!
: templateElement;
scope = scope || createScope();
element.data($$scope, scope);
scope.$element = element;
(cloneConnectFn||noop)(element, scope);
template.attach(element, scope);
scope.$eval();
return scope;
};
},
/**
* @workInProgress
* @ngdoc directive
* @name angular.directive.ng:eval-order
*
* @description
* Normally the view is updated from top to bottom. This usually is
* not a problem, but under some circumstances the values for data
* is not available until after the full view is computed. If such
* values are needed before they are computed the order of
* evaluation can be change using ng:eval-order
*
* @element ANY
* @param {integer|string=} [priority=0] priority integer, or FIRST, LAST constant
*
* @example
* try changing the invoice and see that the Total will lag in evaluation
* @example
TOTAL: without ng:eval-order {{ items.$sum('total') | currency }}
TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}
it('should check ng:format', function(){
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$9.99');
input('item.qty').enter('2');
expect(using('.doc-example-live div:first').binding("items.$sum('total')")).toBe('$9.99');
expect(using('.doc-example-live div:last').binding("items.$sum('total')")).toBe('$19.98');
});
*/
templatize: function(element, elementIndex, priority){
var self = this,
widget,
fn,
directiveFns = self.directives,
descend = true,
directives = true,
elementName = nodeName_(element),
elementNamespace = elementName.indexOf(':') > 0 ? lowercase(elementName).replace(':', '-') : '',
template,
selfApi = {
compile: bind(self, self.compile),
descend: function(value){ if(isDefined(value)) descend = value; return descend;},
directives: function(value){ if(isDefined(value)) directives = value; return directives;},
scope: function(value){ if(isDefined(value)) template.newScope = template.newScope || value; return template.newScope;}
};
try {
priority = element.attr('ng:eval-order') || priority || 0;
} catch (e) {
// for some reason IE throws error under some weird circumstances. so just assume nothing
priority = priority || 0;
}
element.addClass(elementNamespace);
if (isString(priority)) {
priority = PRIORITY[uppercase(priority)] || parseInt(priority, 10);
}
template = new Template(priority);
eachAttribute(element, function(value, name){
if (!widget) {
if (widget = self.widgets('@' + name)) {
element.addClass('ng-attr-widget');
widget = bind(selfApi, widget, value, element);
}
}
});
if (!widget) {
if (widget = self.widgets(elementName)) {
if (elementNamespace)
element.addClass('ng-widget');
widget = bind(selfApi, widget, element);
}
}
if (widget) {
descend = false;
directives = false;
var parent = element.parent();
template.addInit(widget.call(selfApi, element));
if (parent && parent[0]) {
element = jqLite(parent[0].childNodes[elementIndex]);
}
}
if (descend){
// process markup for text nodes only
for(var i=0, child=element[0].childNodes;
i 1; i++) {
var key = element.shift();
var newInstance = instance[key];
if (!newInstance) {
newInstance = {};
instance[key] = newInstance;
}
instance = newInstance;
}
instance[element.shift()] = value;
return value;
}
///////////////////////////////////
var scopeId = 0,
getterFnCache = {},
compileCache = {},
JS_KEYWORDS = {};
forEach(
("abstract,boolean,break,byte,case,catch,char,class,const,continue,debugger,default," +
"delete,do,double,else,enum,export,extends,false,final,finally,float,for,function,goto," +
"if,implements,import,ininstanceof,intinterface,long,native,new,null,package,private," +
"protected,public,return,short,static,super,switch,synchronized,this,throw,throws," +
"transient,true,try,typeof,var,volatile,void,undefined,while,with").split(/,/),
function(key){ JS_KEYWORDS[key] = true;}
);
function getterFn(path){
var fn = getterFnCache[path];
if (fn) return fn;
var code = 'var l, fn, t;\n';
forEach(path.split('.'), function(key) {
key = (JS_KEYWORDS[key]) ? '["' + key + '"]' : '.' + key;
code += 'if(!s) return s;\n' +
'l=s;\n' +
's=s' + key + ';\n' +
'if(typeof s=="function" && !(s instanceof RegExp)) s = function(){ return l'+key+'.apply(l, arguments); };\n';
if (key.charAt(1) == '$') {
// special code for super-imposed functions
var name = key.substr(2);
code += 'if(!s) {\n' +
' t = angular.Global.typeOf(l);\n' +
' fn = (angular[t.charAt(0).toUpperCase() + t.substring(1)]||{})["' + name + '"];\n' +
' if (fn) s = function(){ return fn.apply(l, [l].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n' +
'}\n';
}
});
code += 'return s;';
fn = Function('s', code);
fn["toString"] = function(){ return code; };
return getterFnCache[path] = fn;
}
///////////////////////////////////
function expressionCompile(exp){
if (typeof exp === $function) return exp;
var fn = compileCache[exp];
if (!fn) {
var p = parser(exp);
var fnSelf = p.statements();
fn = compileCache[exp] = extend(
function(){ return fnSelf(this);},
{fnSelf: fnSelf});
}
return fn;
}
function errorHandlerFor(element, error) {
elementError(element, NG_EXCEPTION, isDefined(error) ? formatError(error) : error);
}
/**
* @workInProgress
* @ngdoc overview
* @name angular.scope
*
* @description
* Scope is a JavaScript object and the execution context for expressions. You can think about
* scopes as JavaScript objects that have extra APIs for registering watchers. A scope is the
* context in which model (from the model-view-controller design pattern) exists.
*
* Angular scope objects provide the following methods:
*
* * {@link angular.scope.$become $become()} -
* * {@link angular.scope.$bind $bind()} -
* * {@link angular.scope.$eval $eval()} -
* * {@link angular.scope.$get $get()} -
* * {@link angular.scope.$new $new()} -
* * {@link angular.scope.$onEval $onEval()} -
* * {@link angular.scope.$service $service()} -
* * {@link angular.scope.$set $set()} -
* * {@link angular.scope.$tryEval $tryEval()} -
* * {@link angular.scope.$watch $watch()} -
*
* For more information about how angular scope objects work, see {@link guide/dev_guide.scopes
* Angular Scope Objects} in the angular Developer Guide.
*/
function createScope(parent, providers, instanceCache) {
function Parent(){}
parent = Parent.prototype = (parent || {});
var instance = new Parent();
var evalLists = {sorted:[]};
var $log, $exceptionHandler;
extend(instance, {
'this': instance,
$id: (scopeId++),
$parent: parent,
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$bind
* @function
*
* @description
* Binds a function `fn` to the current scope. See: {@link angular.bind}.
var scope = angular.scope();
var fn = scope.$bind(function(){
return this;
});
expect(fn()).toEqual(scope);
*
* @param {function()} fn Function to be bound.
*/
$bind: bind(instance, bind, instance),
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$get
* @function
*
* @description
* Returns the value for `property_chain` on the current scope. Unlike in JavaScript, if there
* are any `undefined` intermediary properties, `undefined` is returned instead of throwing an
* exception.
*
*
* @param {string} property_chain String representing name of a scope property. Optionally
* properties can be chained with `.` (dot), e.g. `'person.name.first'`
* @returns {*} Value for the (nested) property.
*/
$get: bind(instance, getter, instance),
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$set
* @function
*
* @description
* Assigns a value to a property of the current scope specified via `property_chain`. Unlike in
* JavaScript, if there are any `undefined` intermediary properties, empty objects are created
* and assigned in to them instead of throwing an exception.
*
var scope = angular.scope();
expect(scope.person).toEqual(undefined);
scope.$set('person.name', 'misko');
expect(scope.person).toEqual({name:'misko'});
expect(scope.person.name).toEqual('misko');
*
* @param {string} property_chain String representing name of a scope property. Optionally
* properties can be chained with `.` (dot), e.g. `'person.name.first'`
* @param {*} value Value to assign to the scope property.
*/
$set: bind(instance, setter, instance),
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$eval
* @function
*
* @description
* Without the `exp` parameter triggers an eval cycle for this scope and its child scopes.
*
* With the `exp` parameter, compiles the expression to a function and calls it with `this` set
* to the current scope and returns the result. In other words, evaluates `exp` as angular
* expression in the context of the current scope.
*
* # Example
*
* @param {(string|function())=} exp An angular expression to be compiled to a function or a js
* function.
*
* @returns {*} The result of calling compiled `exp` with `this` set to the current scope.
*/
$eval: function(exp) {
var type = typeof exp;
var i, iSize;
var j, jSize;
var queue;
var fn;
if (type == $undefined) {
for ( i = 0, iSize = evalLists.sorted.length; i < iSize; i++) {
for ( queue = evalLists.sorted[i],
jSize = queue.length,
j= 0; j < jSize; j++) {
instance.$tryEval(queue[j].fn, queue[j].handler);
}
}
} else if (type === $function) {
return exp.call(instance);
} else if (type === 'string') {
return expressionCompile(exp).call(instance);
}
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$tryEval
* @function
*
* @description
* Evaluates the expression in the context of the current scope just like
* {@link angular.scope.$eval} with expression parameter, but also wraps it in a try/catch
* block.
*
* If an exception is thrown then `exceptionHandler` is used to handle the exception.
*
* # Example
*
* @param {string|function()} expression Angular expression to evaluate.
* @param {(function()|DOMElement)=} exceptionHandler Function to be called or DOMElement to be
* decorated.
* @returns {*} The result of `expression` evaluation.
*/
$tryEval: function (expression, exceptionHandler) {
var type = typeof expression;
try {
if (type == $function) {
return expression.call(instance);
} else if (type == 'string'){
return expressionCompile(expression).call(instance);
}
} catch (e) {
if ($log) $log.error(e);
if (isFunction(exceptionHandler)) {
exceptionHandler(e);
} else if (exceptionHandler) {
errorHandlerFor(exceptionHandler, e);
} else if (isFunction($exceptionHandler)) {
$exceptionHandler(e);
}
}
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$watch
* @function
*
* @description
* Registers `listener` as a callback to be executed every time the `watchExp` changes. Be aware
* that the callback gets, by default, called upon registration, this can be prevented via the
* `initRun` parameter.
*
* # Example
*
* @param {function()|string} watchExp Expression that should be evaluated and checked for
* change during each eval cycle. Can be an angular string expression or a function.
* @param {function()|string} listener Function (or angular string expression) that gets called
* every time the value of the `watchExp` changes. The function will be called with two
* parameters, `newValue` and `oldValue`.
* @param {(function()|DOMElement)=} [exceptionHanlder=angular.service.$exceptionHandler] Handler
* that gets called when `watchExp` or `listener` throws an exception. If a DOMElement is
* specified as handler, the element gets decorated by angular with the information about the
* exception.
* @param {boolean=} [initRun=true] Flag that prevents the first execution of the listener upon
* registration.
*
*/
$watch: function(watchExp, listener, exceptionHandler, initRun) {
var watch = expressionCompile(watchExp),
last = watch.call(instance);
listener = expressionCompile(listener);
function watcher(firstRun){
var value = watch.call(instance),
// we have to save the value because listener can call ourselves => inf loop
lastValue = last;
if (firstRun || lastValue !== value) {
last = value;
instance.$tryEval(function(){
return listener.call(instance, value, lastValue);
}, exceptionHandler);
}
}
instance.$onEval(PRIORITY_WATCH, watcher);
if (isUndefined(initRun)) initRun = true;
if (initRun) watcher(true);
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$onEval
* @function
*
* @description
* Evaluates the `expr` expression in the context of the current scope during each
* {@link angular.scope.$eval eval cycle}.
*
* # Example
*
* @param {number} [priority=0] Execution priority. Lower priority numbers get executed first.
* @param {string|function()} expr Angular expression or function to be executed.
* @param {(function()|DOMElement)=} [exceptionHandler=angular.service.$exceptionHandler] Handler
* function to call or DOM element to decorate when an exception occurs.
*
*/
$onEval: function(priority, expr, exceptionHandler){
if (!isNumber(priority)) {
exceptionHandler = expr;
expr = priority;
priority = 0;
}
var evalList = evalLists[priority];
if (!evalList) {
evalList = evalLists[priority] = [];
evalList.priority = priority;
evalLists.sorted.push(evalList);
evalLists.sorted.sort(function(a,b){return a.priority-b.priority;});
}
evalList.push({
fn: expressionCompile(expr),
handler: exceptionHandler
});
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$become
* @function
* @deprecated This method will be removed before 1.0
*
* @description
* Modifies the scope to act like an instance of the given class by:
*
* - copying the class's prototype methods
* - applying the class's initialization function to the scope instance (without using the new
* operator)
*
* That makes the scope be a `this` for the given class's methods — effectively an instance of
* the given class with additional (scope) stuff. A scope can later `$become` another class.
*
* `$become` gets used to make the current scope act like an instance of a controller class.
* This allows for use of a controller class in two ways.
*
* - as an ordinary JavaScript class for standalone testing, instantiated using the new
* operator, with no attached view.
* - as a controller for an angular model stored in a scope, "instantiated" by
* `scope.$become(ControllerClass)`.
*
* Either way, the controller's methods refer to the model variables like `this.name`. When
* stored in a scope, the model supports data binding. When bound to a view, {{name}} in the
* HTML template refers to the same variable.
*/
$become: function(Class) {
if (isFunction(Class)) {
instance.constructor = Class;
forEach(Class.prototype, function(fn, name){
instance[name] = bind(instance, fn);
});
instance.$service.invoke(instance, Class, slice.call(arguments, 1, arguments.length));
//TODO: backwards compatibility hack, remove when we don't depend on init methods
if (isFunction(Class.prototype.init)) {
instance.init();
}
}
},
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$new
* @function
*
* @description
* Creates a new {@link angular.scope scope}, that:
*
* - is a child of the current scope
* - will {@link angular.scope.$become $become} of type specified via `constructor`
*
* @param {function()} constructor Constructor function of the type the new scope should assume.
* @returns {Object} The newly created child scope.
*
*/
$new: function(constructor) {
var child = createScope(instance);
child.$become.apply(instance, concat([constructor], arguments, 1));
instance.$onEval(child.$eval);
return child;
}
});
if (!parent.$root) {
instance.$root = instance;
instance.$parent = instance;
/**
* @workInProgress
* @ngdoc function
* @name angular.scope.$service
* @function
*
* @description
* Provides access to angular's dependency injector and
* {@link angular.service registered services}. In general the use of this api is discouraged,
* except for tests and components that currently don't support dependency injection (widgets,
* filters, etc).
*
* @param {string} serviceId String ID of the service to return.
* @returns {*} Value, object or function returned by the service factory function if any.
*/
(instance.$service = createInjector(instance, providers, instanceCache)).eager();
}
$log = instance.$service('$log');
$exceptionHandler = instance.$service('$exceptionHandler');
return instance;
}
/**
* @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/dev_guide.di dependency injection}).
*
* Angular creates an injector automatically for the root scope and it is available as the
* {@link angular.scope.$service $service} property. Creation of the injector automatically creates
* all of the `$eager` {@link angular.service services}.
*
* @param {Object=} [factoryScope={}] `this` for the service factory function.
* @param {Object.=} [factories=angular.service] Map of service factory
* functions.
* @param {Object.=} [instanceCache={}] Place where instances of services are
* saved for reuse. Can also be used to override services specified by `serviceFactory`
* (useful in tests).
* @returns {function()} Injector function:
*
* * `injector(serviceName)`:
* * `serviceName` - `{string=}` - name of the service to retrieve.
*
* The injector function also has these properties:
*
* * an `invoke` property which can be used to invoke methods with dependency-injected arguments.
* `injector.invoke(self, fn, curryArgs)`
* * `self` - "`this`" to be used when invoking the function.
* * `fn` - the function to be invoked. The function may have the `$inject` property which
* lists the set of arguments which should be auto injected
* (see {@link guide.di dependency injection}).
* * `curryArgs(array)` - optional array of arguments to pass to function invocation after the
* injection arguments (also known as curry arguments or currying).
* * an `eager` property which is used to initialize the eager services.
* `injector.eager()`
*/
function createInjector(factoryScope, factories, instanceCache) {
factories = factories || angularService;
instanceCache = instanceCache || {};
factoryScope = factoryScope || {};
injector.invoke = invoke;
injector.eager = function(){
forEach(factories, function(factory, name){
if (factory.$eager)
injector(name);
if (factory.$creation)
throw new Error("Failed to register service '" + name +
"': $creation property is unsupported. Use $eager:true or see release notes.");
});
};
return injector;
function injector(value){
if (!(value in instanceCache)) {
var factory = factories[value];
if (!factory) throw Error("Unknown provider for '"+value+"'.");
instanceCache[value] = invoke(factoryScope, factory);
}
return instanceCache[value];
};
function invoke(self, fn, args){
args = args || [];
var injectNames = injectionArgs(fn);
var i = injectNames.length;
while(i--) {
args.unshift(injector(injectNames[i]));
}
return fn.apply(self, args);
}
}
/*NOT_PUBLIC_YET
* @ngdoc function
* @name angular.annotate
* @function
*
* @description
* Annotate the function with injection arguments. This is equivalent to setting the `$inject`
* property as described in {@link guide.di dependency injection}.
*
*
* var MyController = angular.annotate('$location', function($location){ ... });
*
*
* @param {String|Array} serviceName... zero or more service names to inject into the
* `annotatedFunction`.
* @param {function} annotatedFunction function to annotate with `$inject`
* functions.
* @returns {function} `annotatedFunction`
*/
function annotate(services, fn) {
if (services instanceof Array) {
fn.$inject = services;
return fn;
} else {
var i = 0,
length = arguments.length - 1, // last one is the destination function
$inject = arguments[length].$inject = [];
for (; i < length; i++) {
$inject.push(arguments[i]);
}
return arguments[length]; // return the last one
}
}
function angularServiceInject(name, fn, inject, eager) {
angularService(name, fn, {$inject:inject, $eager:eager});
}
/**
* @returns the $inject property of function. If not found the
* the $inject is computed by looking at the toString of function and
* extracting all arguments which and assuming that they are the
* injection names.
*/
var FN_ARGS = /^function\s*[^\(]*\(([^\)]*)\)/;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(.+?)\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function injectionArgs(fn) {
assertArgFn(fn);
if (!fn.$inject) {
var args = fn.$inject = [];
var fnText = fn.toString().replace(STRIP_COMMENTS, '');
var argDecl = fnText.match(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
arg.replace(FN_ARG, function(all, name){
args.push(name);
});
});
}
return fn.$inject;
}
var OPERATORS = {
'null':function(self){return null;},
'true':function(self){return true;},
'false':function(self){return false;},
$undefined:noop,
'+':function(self, a,b){return (isDefined(a)?a:0)+(isDefined(b)?b:0);},
'-':function(self, a,b){return (isDefined(a)?a:0)-(isDefined(b)?b:0);},
'*':function(self, a,b){return a*b;},
'/':function(self, a,b){return a/b;},
'%':function(self, a,b){return a%b;},
'^':function(self, a,b){return a^b;},
'=':noop,
'==':function(self, a,b){return a==b;},
'!=':function(self, a,b){return a!=b;},
'<':function(self, a,b){return a':function(self, a,b){return a>b;},
'<=':function(self, a,b){return a<=b;},
'>=':function(self, a,b){return a>=b;},
'&&':function(self, a,b){return a&&b;},
'||':function(self, a,b){return a||b;},
'&':function(self, a,b){return a&b;},
// '|':function(self, a,b){return a|b;},
'|':function(self, a,b){return b(self, a);},
'!':function(self, a){return !a;}
};
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
function lex(text, parseStringsForObjects){
var dateParseLength = parseStringsForObjects ? DATE_ISOSTRING_LN : -1,
tokens = [],
token,
index = 0,
json = [],
ch,
lastCh = ':'; // can start regexp
while (index < text.length) {
ch = text.charAt(index);
if (is('"\'')) {
readString(ch);
} else if (isNumber(ch) || is('.') && isNumber(peek())) {
readNumber();
} else if (isIdent(ch)) {
readIdent();
// identifiers can only be if the preceding char was a { or ,
if (was('{,') && json[0]=='{' &&
(token=tokens[tokens.length-1])) {
token.json = token.text.indexOf('.') == -1;
}
} else if (is('(){}[].,;:')) {
tokens.push({
index:index,
text:ch,
json:(was(':[,') && is('{[')) || is('}]:,')
});
if (is('{[')) json.unshift(ch);
if (is('}]')) json.shift();
index++;
} else if (isWhitespace(ch)) {
index++;
continue;
} else {
var ch2 = ch + peek(),
fn = OPERATORS[ch],
fn2 = OPERATORS[ch2];
if (fn2) {
tokens.push({index:index, text:ch2, fn:fn2});
index += 2;
} else if (fn) {
tokens.push({index:index, text:ch, fn:fn, json: was('[,:') && is('+-')});
index += 1;
} else {
throwError("Unexpected next character ", index, index+1);
}
}
lastCh = ch;
}
return tokens;
function is(chars) {
return chars.indexOf(ch) != -1;
}
function was(chars) {
return chars.indexOf(lastCh) != -1;
}
function peek() {
return index + 1 < text.length ? text.charAt(index + 1) : false;
}
function isNumber(ch) {
return '0' <= ch && ch <= '9';
}
function isWhitespace(ch) {
return ch == ' ' || ch == '\r' || ch == '\t' ||
ch == '\n' || ch == '\v' || ch == '\u00A0'; // IE treats non-breaking space as \u00A0
}
function isIdent(ch) {
return 'a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' == ch || ch == '$';
}
function isExpOperator(ch) {
return ch == '-' || ch == '+' || isNumber(ch);
}
function throwError(error, start, end) {
end = end || index;
throw Error("Lexer Error: " + error + " at column" +
(isDefined(start)
? "s " + start + "-" + index + " [" + text.substring(start, end) + "]"
: " " + end) +
" in expression [" + text + "].");
}
function readNumber() {
var number = "";
var start = index;
while (index < text.length) {
var ch = lowercase(text.charAt(index));
if (ch == '.' || isNumber(ch)) {
number += ch;
} else {
var peekCh = peek();
if (ch == 'e' && isExpOperator(peekCh)) {
number += ch;
} else if (isExpOperator(ch) &&
peekCh && isNumber(peekCh) &&
number.charAt(number.length - 1) == 'e') {
number += ch;
} else if (isExpOperator(ch) &&
(!peekCh || !isNumber(peekCh)) &&
number.charAt(number.length - 1) == 'e') {
throwError('Invalid exponent');
} else {
break;
}
}
index++;
}
number = 1 * number;
tokens.push({index:start, text:number, json:true,
fn:function(){return number;}});
}
function readIdent() {
var ident = "";
var start = index;
var fn;
while (index < text.length) {
var ch = text.charAt(index);
if (ch == '.' || isIdent(ch) || isNumber(ch)) {
ident += ch;
} else {
break;
}
index++;
}
fn = OPERATORS[ident];
tokens.push({
index:start,
text:ident,
json: fn,
fn:fn||extend(getterFn(ident), {
assign:function(self, value){
return setter(self, ident, value);
}
})
});
}
function readString(quote) {
var start = index;
index++;
var string = "";
var rawString = quote;
var escape = false;
while (index < text.length) {
var ch = text.charAt(index);
rawString += ch;
if (escape) {
if (ch == 'u') {
var hex = text.substring(index + 1, index + 5);
if (!hex.match(/[\da-f]{4}/i))
throwError( "Invalid unicode escape [\\u" + hex + "]");
index += 4;
string += String.fromCharCode(parseInt(hex, 16));
} else {
var rep = ESCAPE[ch];
if (rep) {
string += rep;
} else {
string += ch;
}
}
escape = false;
} else if (ch == '\\') {
escape = true;
} else if (ch == quote) {
index++;
tokens.push({index:start, text:rawString, string:string, json:true,
fn:function(){
return (string.length == dateParseLength)
? angular['String']['toDate'](string)
: string;
}});
return;
} else {
string += ch;
}
index++;
}
throwError("Unterminated quote", start);
}
}
/////////////////////////////////////////
function parser(text, json){
var ZERO = valueFn(0),
tokens = lex(text, json),
assignment = _assignment,
assignable = logicalOR,
functionCall = _functionCall,
fieldAccess = _fieldAccess,
objectIndex = _objectIndex,
filterChain = _filterChain,
functionIdent = _functionIdent,
pipeFunction = _pipeFunction;
if(json){
// The extra level of aliasing is here, just in case the lexer misses something, so that
// we prevent any accidental execution in JSON.
assignment = logicalOR;
functionCall =
fieldAccess =
objectIndex =
assignable =
filterChain =
functionIdent =
pipeFunction =
function (){ throwError("is not valid json", {text:text, index:0}); };
}
//TODO: Shouldn't all of the public methods have assertAllConsumed?
//TODO: I think these should be public as part of the parser api instead of scope.$eval().
return {
assignable: assertConsumed(assignable),
primary: assertConsumed(primary),
statements: assertConsumed(statements),
validator: assertConsumed(validator),
formatter: assertConsumed(formatter),
filter: assertConsumed(filter)
};
function assertConsumed(fn) {
return function(){
var value = fn();
if (tokens.length !== 0) {
throwError("is an unexpected token", tokens[0]);
}
return value;
};
}
///////////////////////////////////
function throwError(msg, token) {
throw Error("Syntax Error: Token '" + token.text +
"' " + msg + " at column " +
(token.index + 1) + " of the expression [" +
text + "] starting at [" + text.substring(token.index) + "].");
}
function peekToken() {
if (tokens.length === 0)
throw Error("Unexpected end of expression: " + text);
return tokens[0];
}
function peek(e1, e2, e3, e4) {
if (tokens.length > 0) {
var token = tokens[0];
var t = token.text;
if (t==e1 || t==e2 || t==e3 || t==e4 ||
(!e1 && !e2 && !e3 && !e4)) {
return token;
}
}
return false;
}
function expect(e1, e2, e3, e4){
var token = peek(e1, e2, e3, e4);
if (token) {
if (json && !token.json) {
index = token.index;
throwError("is not valid json", token);
}
tokens.shift();
this.currentToken = token;
return token;
}
return false;
}
function consume(e1){
if (!expect(e1)) {
throwError("is unexpected, expecting [" + e1 + "]", peek());
}
}
function unaryFn(fn, right) {
return function(self) {
return fn(self, right(self));
};
}
function binaryFn(left, fn, right) {
return function(self) {
return fn(self, left(self), right(self));
};
}
function hasTokens () {
return tokens.length > 0;
}
function statements(){
var statements = [];
while(true) {
if (tokens.length > 0 && !peek('}', ')', ';', ']'))
statements.push(filterChain());
if (!expect(';')) {
// optimize for the common case where there is only one statement.
// TODO(size): maybe we should not support multiple statements?
return statements.length == 1
? statements[0]
: function (self){
var value;
for ( var i = 0; i < statements.length; i++) {
var statement = statements[i];
if (statement)
value = statement(self);
}
return value;
};
}
}
}
function _filterChain(){
var left = expression();
var token;
while(true) {
if ((token = expect('|'))) {
left = binaryFn(left, token.fn, filter());
} else {
return left;
}
}
}
function filter(){
return pipeFunction(angularFilter);
}
function validator(){
return pipeFunction(angularValidator);
}
function formatter(){
var token = expect();
var formatter = angularFormatter[token.text];
var argFns = [];
if (!formatter) throwError('is not a valid formatter.', token);
while(true) {
if ((token = expect(':'))) {
argFns.push(expression());
} else {
return valueFn({
format:invokeFn(formatter.format),
parse:invokeFn(formatter.parse)
});
}
}
function invokeFn(fn){
return function(self, input){
var args = [input];
for ( var i = 0; i < argFns.length; i++) {
args.push(argFns[i](self));
}
return fn.apply(self, args);
};
}
}
function _pipeFunction(fnScope){
var fn = functionIdent(fnScope);
var argsFn = [];
var token;
while(true) {
if ((token = expect(':'))) {
argsFn.push(expression());
} else {
var fnInvoke = function(self, input){
var args = [input];
for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](self));
}
return fn.apply(self, args);
};
return function(){
return fnInvoke;
};
}
}
}
function expression(){
return assignment();
}
function _assignment(){
var left = logicalOR();
var right;
var token;
if (token = expect('=')) {
if (!left.assign) {
throwError("implies assignment but [" +
text.substring(0, token.index) + "] can not be assigned to", token);
}
right = logicalOR();
return function(self){
return left.assign(self, right(self));
};
} else {
return left;
}
}
function logicalOR(){
var left = logicalAND();
var token;
while(true) {
if ((token = expect('||'))) {
left = binaryFn(left, token.fn, logicalAND());
} else {
return left;
}
}
}
function logicalAND(){
var left = equality();
var token;
if ((token = expect('&&'))) {
left = binaryFn(left, token.fn, logicalAND());
}
return left;
}
function equality(){
var left = relational();
var token;
if ((token = expect('==','!='))) {
left = binaryFn(left, token.fn, equality());
}
return left;
}
function relational(){
var left = additive();
var token;
if (token = expect('<', '>', '<=', '>=')) {
left = binaryFn(left, token.fn, relational());
}
return left;
}
function additive(){
var left = multiplicative();
var token;
while(token = expect('+','-')) {
left = binaryFn(left, token.fn, multiplicative());
}
return left;
}
function multiplicative(){
var left = unary();
var token;
while(token = expect('*','/','%')) {
left = binaryFn(left, token.fn, unary());
}
return left;
}
function unary(){
var token;
if (expect('+')) {
return primary();
} else if (token = expect('-')) {
return binaryFn(ZERO, token.fn, unary());
} else if (token = expect('!')) {
return unaryFn(token.fn, unary());
} else {
return primary();
}
}
function _functionIdent(fnScope) {
var token = expect();
var element = token.text.split('.');
var instance = fnScope;
var key;
for ( var i = 0; i < element.length; i++) {
key = element[i];
if (instance)
instance = instance[key];
}
if (typeof instance != $function) {
throwError("should be a function", token);
}
return instance;
}
function primary() {
var primary;
if (expect('(')) {
var expression = filterChain();
consume(')');
primary = expression;
} else if (expect('[')) {
primary = arrayDeclaration();
} else if (expect('{')) {
primary = object();
} else {
var token = expect();
primary = token.fn;
if (!primary) {
throwError("not a primary expression", token);
}
}
var next;
while (next = expect('(', '[', '.')) {
if (next.text === '(') {
primary = functionCall(primary);
} else if (next.text === '[') {
primary = objectIndex(primary);
} else if (next.text === '.') {
primary = fieldAccess(primary);
} else {
throwError("IMPOSSIBLE");
}
}
return primary;
}
function _fieldAccess(object) {
var field = expect().text;
var getter = getterFn(field);
return extend(function (self){
return getter(object(self));
}, {
assign:function(self, value){
return setter(object(self), field, value);
}
});
}
function _objectIndex(obj) {
var indexFn = expression();
consume(']');
return extend(
function (self){
var o = obj(self);
var i = indexFn(self);
return (o) ? o[i] : undefined;
}, {
assign:function(self, value){
return obj(self)[indexFn(self)] = value;
}
});
}
function _functionCall(fn) {
var argsFn = [];
if (peekToken().text != ')') {
do {
argsFn.push(expression());
} while (expect(','));
}
consume(')');
return function (self){
var args = [];
for ( var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](self));
}
var fnPtr = fn(self) || noop;
// IE stupidity!
return fnPtr.apply
? fnPtr.apply(self, args)
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
};
}
// This is used with json array declaration
function arrayDeclaration () {
var elementFns = [];
if (peekToken().text != ']') {
do {
elementFns.push(expression());
} while (expect(','));
}
consume(']');
return function (self){
var array = [];
for ( var i = 0; i < elementFns.length; i++) {
array.push(elementFns[i](self));
}
return array;
};
}
function object () {
var keyValues = [];
if (peekToken().text != '}') {
do {
var token = expect(),
key = token.string || token.text;
consume(":");
var value = expression();
keyValues.push({key:key, value:value});
} while (expect(','));
}
consume('}');
return function (self){
var object = {};
for ( var i = 0; i < keyValues.length; i++) {
var keyValue = keyValues[i];
var value = keyValue.value(self);
object[keyValue.key] = value;
}
return object;
};
}
function watchDecl () {
var anchorName = expect().text;
consume(":");
var expressionFn;
if (peekToken().text == '{') {
consume("{");
expressionFn = statements();
consume("}");
} else {
expressionFn = expression();
}
return function(self) {
return {name:anchorName, fn:expressionFn};
};
}
}
function Route(template, defaults) {
this.template = template = template + '#';
this.defaults = defaults || {};
var urlParams = this.urlParams = {};
forEach(template.split(/\W/), function(param){
if (param && template.match(new RegExp(":" + param + "\\W"))) {
urlParams[param] = true;
}
});
}
Route.prototype = {
url: function(params) {
var self = this,
url = this.template,
encodedVal;
params = params || {};
forEach(this.urlParams, function(_, urlParam){
encodedVal = encodeUriSegment(params[urlParam] || self.defaults[urlParam] || "");
url = url.replace(new RegExp(":" + urlParam + "(\\W)"), encodedVal + "$1");
});
url = url.replace(/\/?#$/, '');
var query = [];
forEachSorted(params, function(value, key){
if (!self.urlParams[key]) {
query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
}
});
url = url.replace(/\/*$/, '');
return url + (query.length ? '?' + query.join('&') : '');
}
};
function ResourceFactory(xhr) {
this.xhr = xhr;
}
ResourceFactory.DEFAULT_ACTIONS = {
'get': {method:'GET'},
'save': {method:'POST'},
'query': {method:'GET', isArray:true},
'remove': {method:'DELETE'},
'delete': {method:'DELETE'}
};
ResourceFactory.prototype = {
route: function(url, paramDefaults, actions){
var self = this;
var route = new Route(url);
actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions);
function extractParams(data){
var ids = {};
forEach(paramDefaults || {}, function(value, key){
ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
});
return ids;
}
function Resource(value){
copy(value || {}, this);
}
forEach(actions, function(action, name){
var isPostOrPut = action.method == 'POST' || action.method == 'PUT';
Resource[name] = function (a1, a2, a3) {
var params = {};
var data;
var callback = noop;
switch(arguments.length) {
case 3: callback = a3;
case 2:
if (isFunction(a2)) {
callback = a2;
//fallthrough
} else {
params = a1;
data = a2;
break;
}
case 1:
if (isFunction(a1)) callback = a1;
else if (isPostOrPut) data = a1;
else params = a1;
break;
case 0: break;
default:
throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments.";
}
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
self.xhr(
action.method,
route.url(extend({}, action.params || {}, extractParams(data), params)),
data,
function(status, response, clear) {
if (200 <= status && status < 300) {
if (response) {
if (action.isArray) {
value.length = 0;
forEach(response, function(item){
value.push(new Resource(item));
});
} else {
copy(response, value);
}
}
(callback||noop)(value);
} else {
throw {status: status, response:response, message: status + ": " + response};
}
},
action.verifyCache);
return value;
};
Resource.bind = function(additionalParamDefaults){
return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions);
};
Resource.prototype['$' + name] = function(a1, a2){
var params = extractParams(this);
var callback = noop;
switch(arguments.length) {
case 2: params = a1; callback = a2;
case 1: if (typeof a1 == $function) callback = a1; else params = a1;
case 0: break;
default:
throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments.";
}
var data = isPostOrPut ? this : undefined;
Resource[name].call(this, params, data, callback);
};
});
return Resource;
}
};
//////////////////////////////
// Browser
//////////////////////////////
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 new Error("This browser does not support XMLHttpRequest.");
};
/**
* @private
* @name Browser
*
* @description
* Constructor for the object exposed as $browser service.
*
* 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
*
* @param {object} window The global window object.
* @param {object} document jQuery wrapped document.
* @param {object} body jQuery wrapped document.body.
* @param {function()} XHR XMLHttpRequest constructor.
* @param {object} $log console.log or an object with the same interface.
*/
function Browser(window, document, body, XHR, $log) {
var self = this,
rawDocument = document[0],
location = window.location,
setTimeout = window.setTimeout;
self.isMock = false;
//////////////////////////////////////////////////////////////
// XHR API
//////////////////////////////////////////////////////////////
var idCounter = 0;
var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];
/**
* 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, slice.call(arguments, 1));
} finally {
outstandingRequestCount--;
if (outstandingRequestCount === 0) {
while(outstandingRequestCallbacks.length) {
try {
outstandingRequestCallbacks.pop()();
} catch (e) {
$log.error(e);
}
}
}
}
}
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#xhr
* @methodOf angular.service.$browser
*
* @param {string} method Requested method (get|post|put|delete|head|json)
* @param {string} url Requested url
* @param {?string} post Post data to send (null if nothing to post)
* @param {function(number, string)} callback Function that will be called on response
* @param {object=} header additional HTTP headers to send with XHR.
* Standard headers are:
*
*
Content-Type: application/x-www-form-urlencoded
*
Accept: application/json, text/plain, */*
*
X-Requested-With: XMLHttpRequest
*
*
* @description
* Send ajax request
*/
self.xhr = function(method, url, post, callback, headers) {
outstandingRequestCount ++;
if (lowercase(method) == 'json') {
var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, '');
var script = jqLite(rawDocument.createElement('script'))
.attr({type: 'text/javascript', src: url.replace('JSON_CALLBACK', callbackId)});
window[callbackId] = function(data){
window[callbackId] = undefined;
script.remove();
completeOutstandingRequest(callback, 200, data);
};
body.append(script);
} else {
var xhr = new XHR();
xhr.open(method, url, true);
forEach(headers, function(value, key) {
if (value) xhr.setRequestHeader(key, value);
});
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
// normalize IE bug (http://bugs.jquery.com/ticket/1450)
var status = xhr.status == 1223 ? 204 : xhr.status || 200;
completeOutstandingRequest(callback, status, xhr.responseText);
}
};
xhr.send(post || '');
}
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#notifyWhenNoOutstandingRequests
* @methodOf angular.service.$browser
*
* @param {function()} callback Function that will be called when no outstanding request
*/
self.notifyWhenNoOutstandingRequests = function(callback) {
if (outstandingRequestCount === 0) {
callback();
} else {
outstandingRequestCallbacks.push(callback);
}
};
//////////////////////////////////////////////////////////////
// Poll Watcher API
//////////////////////////////////////////////////////////////
var pollFns = [],
pollTimeout;
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#poll
* @methodOf angular.service.$browser
*/
self.poll = function() {
forEach(pollFns, function(pollFn){ pollFn(); });
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addPollFn
* @methodOf angular.service.$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 (!pollTimeout) self.startPoller(100, setTimeout);
pollFns.push(fn);
return fn;
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#startPoller
* @methodOf angular.service.$browser
*
* @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.
*/
self.startPoller = function(interval, setTimeout) {
(function check(){
self.poll();
pollTimeout = setTimeout(check, interval);
})();
};
//////////////////////////////////////////////////////////////
// URL API
//////////////////////////////////////////////////////////////
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#setUrl
* @methodOf angular.service.$browser
*
* @param {string} url New url
*
* @description
* Sets browser's url
*/
self.setUrl = function(url) {
var existingURL = location.href;
if (!existingURL.match(/#/)) existingURL += '#';
if (!url.match(/#/)) url += '#';
location.href = url;
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#getUrl
* @methodOf angular.service.$browser
*
* @description
* Get current browser's url
*
* @returns {string} Browser's url
*/
self.getUrl = function() {
return location.href;
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#onHashChange
* @methodOf angular.service.$browser
*
* @description
* Detects if browser support onhashchange events and register a listener otherwise registers
* $browser poller. The `listener` will then get called when the hash changes.
*
* The listener gets called with either HashChangeEvent object or simple object that also contains
* `oldURL` and `newURL` properties.
*
* NOTE: this api is intended for use only by the $location service. Please use the
* {@link angular.service.$location $location service} to monitor hash changes in angular apps.
*
* @param {function(event)} listener Listener function to be called when url hash changes.
* @return {function()} Returns the registered listener fn - handy if the fn is anonymous.
*/
self.onHashChange = function(listener) {
// IE8 comp mode returns true, but doesn't support hashchange event
var dm = window.document.documentMode;
if ('onhashchange' in window && (isUndefined(dm) || dm >= 8)) {
jqLite(window).bind('hashchange', listener);
} else {
var lastBrowserUrl = self.getUrl();
self.addPollFn(function() {
if (lastBrowserUrl != self.getUrl()) {
listener();
lastBrowserUrl = self.getUrl();
}
});
}
return listener;
};
//////////////////////////////////////////////////////////////
// Cookies API
//////////////////////////////////////////////////////////////
var lastCookies = {};
var lastCookieString = '';
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#cookies
* @methodOf angular.service.$browser
*
* @param {string=} name Cookie name
* @param {string=} value Cokkie value
*
* @description
* The cookies method provides a 'private' low level access to browser cookies.
* It is not meant to be used directly, use the $cookie service instead.
*
* The return values vary depending on the arguments that the method was called with as follows:
*
*
cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
*
cookies(name, value) -> set name to value, if value is undefined delete the cookie
*
cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
*
*
* @returns {Object} Hash of all cookies (if called without any parameter)
*/
self.cookies = function (name, value) {
var cookieLength, cookieArray, cookie, i, keyValue, index;
if (name) {
if (value === undefined) {
rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
} else {
if (isString(value)) {
rawDocument.cookie = escape(name) + '=' + escape(value);
cookieLength = name.length + value.length + 1;
if (cookieLength > 4096) {
$log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+
cookieLength + " > 4096 bytes)!");
}
if (lastCookies.length > 20) {
$log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " +
"were already set (" + lastCookies.length + " > 20 )");
}
}
}
} 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
lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1));
}
}
}
return lastCookies;
}
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#defer
* @methodOf angular.service.$browser
* @param {function()} fn A function, who's execution should be defered.
* @param {number=} [delay=0] of milliseconds to defer the function execution.
*
* @description
* Executes a fn asynchroniously 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) {
outstandingRequestCount++;
setTimeout(function() { completeOutstandingRequest(fn); }, delay || 0);
};
//////////////////////////////////////////////////////////////
// Misc API
//////////////////////////////////////////////////////////////
var hoverListener = noop;
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#hover
* @methodOf angular.service.$browser
*
* @description
* Set hover listener.
*
* @param {function(Object, boolean)} listener Function that will be called when a hover event
* occurs.
*/
self.hover = function(listener) { hoverListener = listener; };
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#bind
* @methodOf angular.service.$browser
*
* @description
* Register hover function to real browser
*/
self.bind = function() {
document.bind("mouseover", function(event){
hoverListener(jqLite(msie ? event.srcElement : event.target), true);
return true;
});
document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){
hoverListener(jqLite(event.target), false);
return true;
});
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addCss
* @methodOf angular.service.$browser
*
* @param {string} url Url to css file
* @description
* Adds a stylesheet tag to the head.
*/
self.addCss = function(url) {
var link = jqLite(rawDocument.createElement('link'));
link.attr('rel', 'stylesheet');
link.attr('type', 'text/css');
link.attr('href', url);
body.append(link);
};
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$browser#addJs
* @methodOf angular.service.$browser
*
* @param {string} url Url to js file
* @param {string=} dom_id Optional id for the script tag
*
* @description
* Adds a script tag to the head.
*/
self.addJs = function(url, dom_id) {
var script = jqLite(rawDocument.createElement('script'));
script.attr('type', 'text/javascript');
script.attr('src', url);
if (dom_id) script.attr('id', dom_id);
body.append(script);
};
}
/*
* HTML Parser By Misko Hevery (misko@hevery.com)
* based on: HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
*/
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP = /^<\s*([\w:-]+)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*>/,
END_TAG_REGEXP = /^<\s*\/\s*([\w:-]+)[^>]*>/,
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^,
BEGING_END_TAGE_REGEXP = /^<\s*\//,
COMMENT_REGEXP = //g,
CDATA_REGEXP = //g,
URI_REGEXP = /^((ftp|https?):\/\/|mailto:|#)/,
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; // Match everything outside of normal chars and " (quote character)
// Empty Elements - HTML 4.01
var emptyElements = makeMap("area,br,col,hr,img");
// Block Elements - HTML 4.01
var blockElements = makeMap("address,blockquote,center,dd,del,dir,div,dl,dt,"+
"hr,ins,li,map,menu,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul");
// Inline Elements - HTML 4.01
var inlineElements = makeMap("a,abbr,acronym,b,bdo,big,br,cite,code,del,dfn,em,font,i,img,"+
"ins,kbd,label,map,q,s,samp,small,span,strike,strong,sub,sup,tt,u,var");
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelfElements = makeMap("colgroup,dd,dt,li,p,td,tfoot,th,thead,tr");
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
var validElements = extend({}, emptyElements, blockElements, inlineElements, closeSelfElements);
//Attributes that have href and hence need to be sanitized
var uriAttrs = makeMap("background,href,longdesc,src,usemap");
var validAttrs = extend({}, uriAttrs, makeMap(
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
'scope,scrolling,shape,span,start,summary,target,title,type,'+
'valign,value,vspace,width'));
/**
* @example
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
function htmlParser( html, handler ) {
var index, chars, match, stack = [], last = html;
stack.last = function(){ return stack[ stack.length - 1 ]; };
while ( html ) {
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
// Comment
if ( html.indexOf("");
if ( index >= 0 ) {
if (handler.comment) handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
chars = false;
}
}
if ( chars ) {
index = html.indexOf("<");
var text = index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if (handler.chars) handler.chars( decodeEntities(text) );
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'), function(all, text){
text = text.
replace(COMMENT_REGEXP, "$1").
replace(CDATA_REGEXP, "$1");
if (handler.chars) handler.chars( decodeEntities(text) );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last ) {
throw "Parse Error: " + html;
}
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
tagName = lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( closeSelfElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = emptyElements[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
var attrs = {};
rest.replace(ATTR_REGEXP, function(match, name, doubleQuotedValue, singleQoutedValue, unqoutedValue) {
var value = doubleQuotedValue
|| singleQoutedValue
|| unqoutedValue
|| '';
attrs[name] = decodeEntities(value);
});
if (handler.start) handler.start( tagName, attrs, unary );
}
function parseEndTag( tag, tagName ) {
var pos = 0, i;
tagName = lowercase(tagName);
if ( tagName )
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if (handler.end) handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
}
/**
* decodes all entities into regular string
* @param value
* @returns {string} A string with decoded entities.
*/
var hiddenPre=document.createElement("pre");
function decodeEntities(value) {
hiddenPre.innerHTML=value.replace(//g, '>');
}
/**
* create an HTML/XML writer which writes to buffer
* @param {Array} buf use buf.jain('') to get out sanitized html string
* @returns {object} in the form of {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf){
var ignore = false;
var out = bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
tag = lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
}
if (!ignore && validElements[tag] == true) {
out('<');
out(tag);
forEach(attrs, function(value, key){
var lkey=lowercase(key);
if (validAttrs[lkey]==true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) {
out(' ');
out(key);
out('="');
out(encodeEntities(value));
out('"');
}
});
out(unary ? '/>' : '>');
}
},
end: function(tag){
tag = lowercase(tag);
if (!ignore && validElements[tag] == true) {
out('');
out(tag);
out('>');
}
if (tag == ignore) {
ignore = false;
}
},
chars: function(chars){
if (!ignore) {
out(encodeEntities(chars));
}
}
};
}
//////////////////////////////////
//JQLite
//////////////////////////////////
/**
* @workInProgress
* @ngdoc function
* @name angular.element
* @function
*
* @description
* Wraps a raw DOM element or HTML string as [jQuery](http://jquery.com) element.
* `angular.element` is either an alias for [jQuery](http://api.jquery.com/jQuery/) function if
* jQuery is loaded or a function that wraps the element or string in angular's jQuery lite
* implementation.
*
* Real jQuery always takes precedence (as long as it was loaded before `DOMContentEvent`)
*
* Angular's jQuery lite implementation is a tiny API-compatible subset of jQuery which allows
* angular to manipulate DOM. The jQuery lite implements only a subset of jQuery api, with the
* focus on the most commonly needed functionality and minimal footprint. For this reason only a
* limited number of jQuery methods, arguments and invocation styles are supported.
*
* NOTE: All element references in angular are always wrapped with jQuery (lite) and are never
* raw DOM references.
*
* ## Angular's jQuery lite implements these functions:
*
* - [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/bind/)
* - [children()](http://api.jquery.com/children/)
* - [clone()](http://api.jquery.com/clone/)
* - [css()](http://api.jquery.com/css/)
* - [data()](http://api.jquery.com/data/)
* - [hasClass()](http://api.jquery.com/hasClass/)
* - [parent()](http://api.jquery.com/parent/)
* - [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/)
* - [trigger()](http://api.jquery.com/trigger/)
*
* ## Additionally these methods extend the jQuery and are available in both jQuery and jQuery lite
* version:
*
*- `scope()` - retrieves the current angular scope of the element.
*
* @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery.
* @returns {Object} jQuery object.
*/
var jqCache = {},
jqName = '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++); }
function getStyle(element) {
var current = {}, style = element[0].style, value, name, i;
if (typeof style.length == 'number') {
for(i = 0; i < style.length; i++) {
name = style[i];
current[name] = style[name];
}
} else {
for (name in style) {
value = style[name];
if (1*name != name && name != 'cssText' && value && typeof value == 'string' && value !='false')
current[name] = value;
}
}
return current;
}
//TODO: delete me! dead code?
if (msie) {
extend(JQLite.prototype, {
text: function(value) {
var e = this[0];
// NodeType == 3 is text node
if (e.nodeType == 3) {
if (isDefined(value)) e.nodeValue = value;
return e.nodeValue;
} else {
if (isDefined(value)) e.innerText = value;
return e.innerText;
}
}
});
}
/////////////////////////////////////////////
function jqLiteWrap(element) {
if (isString(element) && element.charAt(0) != '<') {
throw new Error('selectors not implemented');
}
return new JQLite(element);
}
function JQLite(element) {
if (element instanceof JQLite) {
return element;
} else 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 = '
' + element; // IE insanity to make NoScope elements work!
div.removeChild(div.firstChild); // remove the superfluous div
JQLiteAddNodes(this, div.childNodes);
this.remove(); // 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 JQLiteRemoveData(element) {
var cacheId = element[jqName],
cache = jqCache[cacheId];
if (cache) {
forEach(cache.bind || {}, function(fn, type){
removeEventListenerFn(element, type, fn);
});
delete jqCache[cacheId];
element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
}
}
function JQLiteData(element, key, value) {
var cacheId = element[jqName],
cache = jqCache[cacheId || -1];
if (isDefined(value)) {
if (!cache) {
element[jqName] = cacheId = jqNextId();
cache = jqCache[cacheId] = {};
}
cache[key] = value;
} else {
return cache ? cache[key] : null;
}
}
function JQLiteHasClass(element, selector, _) {
// the argument '_' is important, since it makes the function have 3 arguments, which
// is neede for delegate function to realize the this is a getter.
var className = " " + selector + " ";
return ((" " + element.className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1);
}
function JQLiteRemoveClass(element, selector) {
element.className = trim(
(" " + element.className + " ")
.replace(/[\n\t]/g, " ")
.replace(" " + selector + " ", "")
);
}
function JQLiteAddClass(element, selector ) {
if (!JQLiteHasClass(element, selector)) {
element.className = trim(element.className + ' ' + selector);
}
}
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]);
}
}
}
//////////////////////////////////////////
// Functions which are declared directly.
//////////////////////////////////////////
var JQLitePrototype = JQLite.prototype = {
ready: function(fn) {
var fired = false;
function trigger() {
if (fired) return;
fired = true;
fn();
}
this.bind('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.
jqLiteWrap(window).bind('load', trigger); // fallback to window.onload for others
},
toString: function(){
var value = [];
forEach(this, function(e){ value.push('' + e);});
return '[' + value.join(', ') + ']';
},
length: 0,
push: push,
sort: [].sort,
splice: [].splice
};
//////////////////////////////////////////
// Functions iterating getter/setters.
// these functions return self on setter and
// value on get.
//////////////////////////////////////////
var SPECIAL_ATTR = makeMap("multiple,selected,checked,disabled,readonly");
forEach({
data: JQLiteData,
scope: function(element) {
var scope;
while (element && !(scope = jqLite(element).data($$scope))) {
element = element.parentNode;
}
return scope;
},
removeAttr: function(element,name) {
element.removeAttribute(name);
},
hasClass: JQLiteHasClass,
css: function(element, name, value) {
if (isDefined(value)) {
element.style[name] = value;
} else {
return element.style[name];
}
},
attr: function(element, name, value){
if (SPECIAL_ATTR[name]) {
if (isDefined(value)) {
element[name] = !!value;
} else {
return element[name];
}
} 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
return element.getAttribute(name, 2);
}
},
text: extend((msie < 9)
? function(element, value) {
// NodeType == 3 is text node
if (element.nodeType == 3) {
if (isUndefined(value))
return element.nodeValue;
element.nodeValue = value;
} else {
if (isUndefined(value))
return element.innerText;
element.innerText = value;
}
}
: function(element, value) {
if (isUndefined(value)) {
return element.textContent;
}
element.textContent = value;
}, {$dv:''}),
val: function(element, value) {
if (isUndefined(value)) {
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;
if ((fn.length == 2 ? 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++) {
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.
if (this.length)
return fn(this[0], arg1, arg2);
}
} 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;
}
return fn.$dv;
};
});
//////////////////////////////////////////
// Functions iterating traversal.
// These functions chain results into a single
// selector.
//////////////////////////////////////////
forEach({
removeData: JQLiteRemoveData,
dealoc: JQLiteDealoc,
bind: function(element, type, fn){
var bind = JQLiteData(element, 'bind'),
eventHandler;
if (!bind) JQLiteData(element, 'bind', bind = {});
forEach(type.split(' '), function(type){
eventHandler = bind[type];
if (!eventHandler) {
bind[type] = eventHandler = function(event) {
if (!event.preventDefault) {
event.preventDefault = function(){
event.returnValue = false; //ie
};
}
if (!event.stopPropagation) {
event.stopPropagation = function() {
event.cancelBubble = true; //ie
};
}
forEach(eventHandler.fns, function(fn){
fn.call(element, event);
});
};
eventHandler.fns = [];
addEventListenerFn(element, type, eventHandler);
}
eventHandler.fns.push(fn);
});
},
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.nodeName != '#text')
children.push(element);
});
return children;
},
append: function(element, node) {
forEach(new JQLite(node), function(child){
if (element.nodeType === 1)
element.appendChild(child);
});
},
prepend: function(element, node) {
if (element.nodeType === 1) {
var index = element.firstChild;
forEach(new JQLite(node), function(child){
if (index) {
element.insertBefore(child, index);
} else {
element.appendChild(child);
index = child;
}
});
}
},
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) {
return element.nextSibling;
},
find: function(element, selector) {
return element.getElementsByTagName(selector);
},
clone: JQLiteClone
}, function(fn, name){
/**
* chaining functions
*/
JQLite.prototype[name] = function(arg1, arg2) {
var value;
for(var i=0; i < this.length; i++) {
if (value == undefined) {
value = fn(this[i], arg1, arg2);
if (value !== undefined) {
// any function which returns a value needs to be wrapped
value = jqLite(value);
}
} else {
JQLiteAddNodes(value, fn(this[i], arg1, arg2));
}
}
return value == undefined ? this : value;
};
});
var angularGlobal = {
'typeOf':function(obj){
if (obj === null) return $null;
var type = typeof obj;
if (type == $object) {
if (obj instanceof Array) return $array;
if (isDate(obj)) return $date;
if (obj.nodeType == 1) return $element;
}
return type;
}
};
/**
* @ngdoc overview
* @name angular.Object
* @function
*
* @description
* A namespace for utility functions used to work with JavaScript objects. These functions are
* exposed in two ways:
*
* __* Angular expressions:__ Functions are bound to all objects and augment the Object type. The
* names of these methods are prefixed with the '$' character in order to minimize naming collisions.
* To call a method, invoke the function without the first argument, e.g, `myObject.$foo(param2)`.
*
* __* JavaScript code:__ Functions don't augment the Object type and must be invoked as functions of
* `angular.Object` as `angular.Object.foo(myObject, param2)`.
*
* * {@link angular.Object.copy angular.Object.copy()} - Creates a deep copy of the source parameter
* * {@link angular.Object.equals angular.Object.equals()} - Determines if two objects or values are
* equivalent
* * {@link angular.Object.size angular.Object.size()} - Determines the number of elements in
* strings, arrays, and objects.
*/
var angularCollection = {
'copy': copy,
'size': size,
'equals': equals
};
var angularObject = {
'extend': extend
};
/**
* @ngdoc overview
* @name angular.Array
*
* @description
* A namespace for utility functions for the manipulation of JavaScript Array objects.
*
* These functions are exposed in two ways:
*
* * __Angular expressions:__ Functions are bound to the Array objects and augment the Array type as
* array methods. The names of these methods are prefixed with $ character to minimize naming
* collisions. To call a method, invoke myArrayObject.$foo(params).
*
* Because Array type is a subtype of the Object type, all angular.Object functions augment
* theArray type in angular expressions as well.
*
* * __JavaScript code:__ Functions don't augment the Array type and must be invoked as functions of
* `angular.Array` as `angular.Array.foo(myArrayObject, params)`.
*
* The following APIs are built-in to the angular Array object:
*
* * {@link angular.Array.add angular.Array.add()} - Optionally adds a new element to an array.
* * {@link angular.Array.count angular.Array.count()} - Determines the number of elements in an
* array.
* * {@link angular.Array.filter angular.Array.filter()} - Returns a subset of items as a new array.
* * {@link angular.Array.indexOf angular.Array.indexOf()} - Determines the index of an array value.
* * {@link angular.Array.limitTo angular.Array.limitTo()} - Creates a new array off the front or
* back of an existing array.
* * {@link angular.Array.orderBy angular.Array.orderBy()} - Orders array elements
* * {@link angular.Array.remove angular.Array.remove()} - Removes array elements
* * {@link angular.Array.sum angular.Array.sum()} - Sums the number elements in an array
*/
var angularArray = {
/**
* @ngdoc function
* @name angular.Array.indexOf
* @function
*
* @description
* Determines the index of `value` in `array`.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array Array to search.
* @param {*} value Value to search for.
* @returns {number} The position of the element in `array`. The position is 0-based. `-1` is returned if the value can't be found.
*
* @example
Index of '{{bookName}}' in the list {{books}} is {{books.$indexOf(bookName)}}.
it('should correctly calculate the initial index', function() {
expect(binding('books.$indexOf(bookName)')).toBe('2');
});
it('should recalculate', function() {
input('bookName').enter('foo');
expect(binding('books.$indexOf(bookName)')).toBe('-1');
input('bookName').enter('Moby Dick');
expect(binding('books.$indexOf(bookName)')).toBe('0');
});
*/
'indexOf': indexOf,
/**
* @ngdoc function
* @name angular.Array.sum
* @function
*
* @description
* This function calculates the sum of all numbers in `array`. If the `expressions` is supplied,
* it is evaluated once for each element in `array` and then the sum of these values is returned.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array The source array.
* @param {(string|function())=} expression Angular expression or a function to be evaluated for each
* element in `array`. The array element becomes the `this` during the evaluation.
* @returns {number} Sum of items in the array.
*
* @example
//TODO: these specs are lame because I had to work around issues #164 and #167
it('should initialize and calculate the totals', function() {
expect(repeater('.doc-example-live table tr', 'item in invoice.items').count()).toBe(3);
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(1)).
toEqual(['$99.50']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$99.50');
});
it('should add an entry and recalculate', function() {
element('.doc-example-live a:contains("add item")').click();
using('.doc-example-live tr:nth-child(3)').input('item.qty').enter('20');
using('.doc-example-live tr:nth-child(3)').input('item.cost').enter('100');
expect(repeater('.doc-example-live table tr', 'item in invoice.items').row(2)).
toEqual(['$2,000.00']);
expect(binding("invoice.items.$sum('qty*cost')")).toBe('$2,099.50');
});
*/
'sum':function(array, expression) {
var fn = angular['Function']['compile'](expression);
var sum = 0;
for (var i = 0; i < array.length; i++) {
var value = 1 * fn(array[i]);
if (!isNaN(value)){
sum += value;
}
}
return sum;
},
/**
* @ngdoc function
* @name angular.Array.remove
* @function
*
* @description
* Modifies `array` by removing an element from it. The element will be looked up using the
* {@link angular.Array.indexOf indexOf} function on the `array` and only the first instance of
* the element will be removed.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array Array from which an element should be removed.
* @param {*} value Element to be removed.
* @returns {*} The removed element.
*
* @example
tasks = {{tasks}}
it('should initialize the task list with for tasks', function() {
expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(4);
expect(repeater('.doc-example-live ul li', 'task in tasks').column('task')).
toEqual(['Learn Angular', 'Read Documentation', 'Check out demos',
'Build cool applications']);
});
it('should initialize the task list with for tasks', function() {
element('.doc-example-live ul li a:contains("X"):first').click();
expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(3);
element('.doc-example-live ul li a:contains("X"):last').click();
expect(repeater('.doc-example-live ul li', 'task in tasks').count()).toBe(2);
expect(repeater('.doc-example-live ul li', 'task in tasks').column('task')).
toEqual(['Read Documentation', 'Check out demos']);
});
*/
'remove':function(array, value) {
var index = indexOf(array, value);
if (index >=0)
array.splice(index, 1);
return value;
},
/**
* @ngdoc function
* @name angular.Array.filter
* @function
*
* @description
* Selects a subset of items from `array` and returns it as a new array.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array The source array.
* @param {string|Object|function()} expression The predicate to be used for selecting items from
* `array`.
*
* Can be one of:
*
* - `string`: Predicate that results in a substring match using the value of `expression`
* string. All strings or objects with string properties in `array` that contain this string
* will be returned. The predicate can be negated by prefixing the string with `!`.
*
* - `Object`: A pattern object can be used to filter specific properties on objects contained
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
* which have property `name` containing "M" and property `phone` containing "1". A special
* property name `$` can be used (as in `{$:"text"}`) to accept a match against any
* property of the object. That's equivalent to the simple substring match with a `string`
* as described above.
*
* - `function`: A predicate function can be used to write arbitrary filters. The function is
* called for each element of `array`. The final result is an array of those elements that
* the predicate returned true for.
*
* @example
Search:
Name
Phone
{{friend.name}}
{{friend.phone}}
Any:
Name only
Phone only
Name
Phone
{{friend.name}}
{{friend.phone}}
it('should search across all fields when filtering with a string', function() {
input('searchText').enter('m');
expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
toEqual(['Mary', 'Mike', 'Adam']);
input('searchText').enter('76');
expect(repeater('#searchTextResults tr', 'friend in friends').column('name')).
toEqual(['John', 'Julie']);
});
it('should search in specific fields when filtering with a predicate object', function() {
input('search.$').enter('i');
expect(repeater('#searchObjResults tr', 'friend in friends').column('name')).
toEqual(['Mary', 'Mike', 'Julie']);
});
*/
'filter':function(array, expression) {
var predicates = [];
predicates.check = function(value) {
for (var j = 0; j < predicates.length; j++) {
if(!predicates[j](value)) {
return false;
}
}
return true;
};
var search = function(obj, text){
if (text.charAt(0) === '!') {
return !search(obj, text.substr(1));
}
switch (typeof obj) {
case "boolean":
case "number":
case "string":
return ('' + obj).toLowerCase().indexOf(text) > -1;
case "object":
for ( var objKey in obj) {
if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) {
return true;
}
}
return false;
case "array":
for ( var i = 0; i < obj.length; i++) {
if (search(obj[i], text)) {
return true;
}
}
return false;
default:
return false;
}
};
switch (typeof expression) {
case "boolean":
case "number":
case "string":
expression = {$:expression};
case "object":
for (var key in expression) {
if (key == '$') {
(function(){
var text = (''+expression[key]).toLowerCase();
if (!text) return;
predicates.push(function(value) {
return search(value, text);
});
})();
} else {
(function(){
var path = key;
var text = (''+expression[key]).toLowerCase();
if (!text) return;
predicates.push(function(value) {
return search(getter(value, path), text);
});
})();
}
}
break;
case $function:
predicates.push(expression);
break;
default:
return array;
}
var filtered = [];
for ( var j = 0; j < array.length; j++) {
var value = array[j];
if (predicates.check(value)) {
filtered.push(value);
}
}
return filtered;
},
/**
* @workInProgress
* @ngdoc function
* @name angular.Array.add
* @function
*
* @description
* `add` is a function similar to JavaScript's `Array#push` method, in that it appends a new
* element to an array. The difference is that the value being added is optional and defaults to
* an empty object.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array The array expand.
* @param {*=} [value={}] The value to be added.
* @returns {Array} The expanded array.
*
* @TODO simplify the example.
*
* @example
* This example shows how an initially empty array can be filled with objects created from user
* input via the `$add` method.
[add empty]
[add 'John']
[add 'Mary']
beforeEach(function() {
expect(binding('people')).toBe('people = []');
});
it('should create an empty record when "add empty" is clicked', function() {
element('.doc-example-live a:contains("add empty")').click();
expect(binding('people')).toBe('people = [{\n "name":"",\n "sex":null}]');
});
it('should create a "John" record when "add \'John\'" is clicked', function() {
element('.doc-example-live a:contains("add \'John\'")').click();
expect(binding('people')).toBe('people = [{\n "name":"John",\n "sex":"male"}]');
});
it('should create a "Mary" record when "add \'Mary\'" is clicked', function() {
element('.doc-example-live a:contains("add \'Mary\'")').click();
expect(binding('people')).toBe('people = [{\n "name":"Mary",\n "sex":"female"}]');
});
it('should delete a record when "X" is clicked', function() {
element('.doc-example-live a:contains("add empty")').click();
element('.doc-example-live li a:contains("X"):first').click();
expect(binding('people')).toBe('people = []');
});
*/
'add':function(array, value) {
array.push(isUndefined(value)? {} : value);
return array;
},
/**
* @ngdoc function
* @name angular.Array.count
* @function
*
* @description
* Determines the number of elements in an array. Optionally it will count only those elements
* for which the `condition` evaluates to `true`.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array The array to count elements in.
* @param {(function()|string)=} condition A function to be evaluated or angular expression to be
* compiled and evaluated. The element that is currently being iterated over, is exposed to
* the `condition` as `this`.
* @returns {number} Number of elements in the array (for which the condition evaluates to true).
*
* @example
{{item.name}}: points=
Number of items which have one point: {{ items.$count('points==1') }}
Number of items which have more than one point: {{items.$count('points>1')}}
it('should calculate counts', function() {
expect(binding('items.$count(\'points==1\')')).toEqual(2);
expect(binding('items.$count(\'points>1\')')).toEqual(1);
});
it('should recalculate when updated', function() {
using('.doc-example-live li:first-child').input('item.points').enter('23');
expect(binding('items.$count(\'points==1\')')).toEqual(1);
expect(binding('items.$count(\'points>1\')')).toEqual(2);
});
*/
'count':function(array, condition) {
if (!condition) return array.length;
var fn = angular['Function']['compile'](condition), count = 0;
forEach(array, function(value){
if (fn(value)) {
count ++;
}
});
return count;
},
/**
* @ngdoc function
* @name angular.Array.orderBy
* @function
*
* @description
* Orders `array` by the `expression` predicate.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array The array to sort.
* @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be
* used by the comparator to determine the order of elements.
*
* Can be one of:
*
* - `function`: getter function. The result of this function will be sorted using the
* `<`, `=`, `>` operator
* - `string`: angular expression which evaluates to an object to order by, such as 'name' to
* sort by a property called 'name'. Optionally prefixed with `+` or `-` to control ascending
* or descending sort order (e.g. +name or -name).
* - `Array`: array of function or string predicates, such that a first predicate in the array
* is used for sorting, but when the items are equivalent next predicate is used.
*
* @param {boolean=} reverse Reverse the order the array.
* @returns {Array} Sorted copy of the source array.
*
* @example
it('should be reverse ordered by aged', function() {
expect(binding('predicate')).toBe('Sorting predicate = -age; reverse = ');
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
toEqual(['35', '29', '21', '19', '10']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
});
it('should reorder the table when user selects different predicate', function() {
element('.doc-example-live a:contains("Name")').click();
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.age')).
toEqual(['35', '10', '29', '19', '21']);
element('.doc-example-live a:contains("Phone")').click();
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.phone')).
toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
expect(repeater('.doc-example-live table', 'friend in friends').column('friend.name')).
toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
});
*/
'orderBy':function(array, sortPredicate, reverseOrder) {
if (!sortPredicate) return array;
sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate];
sortPredicate = map(sortPredicate, function(predicate){
var descending = false, get = predicate || identity;
if (isString(predicate)) {
if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
descending = predicate.charAt(0) == '-';
predicate = predicate.substring(1);
}
get = expressionCompile(predicate).fnSelf;
}
return reverseComparator(function(a,b){
return compare(get(a),get(b));
}, descending);
});
var arrayCopy = [];
for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); }
return arrayCopy.sort(reverseComparator(comparator, reverseOrder));
function comparator(o1, o2){
for ( var i = 0; i < sortPredicate.length; i++) {
var comp = sortPredicate[i](o1, o2);
if (comp !== 0) return comp;
}
return 0;
}
function reverseComparator(comp, descending) {
return toBoolean(descending)
? function(a,b){return comp(b,a);}
: comp;
}
function compare(v1, v2){
var t1 = typeof v1;
var t2 = typeof v2;
if (t1 == t2) {
if (t1 == "string") v1 = v1.toLowerCase();
if (t1 == "string") v2 = v2.toLowerCase();
if (v1 === v2) return 0;
return v1 < v2 ? -1 : 1;
} else {
return t1 < t2 ? -1 : 1;
}
}
},
/**
* @ngdoc function
* @name angular.Array.limitTo
* @function
*
* @description
* Creates a new array containing only the first, or last `limit` number of elements of the
* source `array`.
*
* Note: this function is used to augment the `Array` type in angular expressions. See
* {@link angular.Array} for more info.
*
* @param {Array} array Source array to be limited.
* @param {string|Number} limit The length of the returned array. If the number is positive, the
* first `limit` items from the source array will be copied, if the number is negative, the
* last `limit` items will be copied.
* @returns {Array} A new sub-array of length `limit`.
*
* @example
Limit [1,2,3,4,5,6,7,8,9] to:
Output: {{ numbers.$limitTo(limit) | json }}
it('should limit the numer array to first three items', function() {
expect(element('.doc-example-live input[name=limit]').val()).toBe('3');
expect(binding('numbers.$limitTo(limit) | json')).toEqual('[1,2,3]');
});
it('should update the output when -3 is entered', function() {
input('limit').enter(-3);
expect(binding('numbers.$limitTo(limit) | json')).toEqual('[7,8,9]');
});
*/
limitTo: function(array, limit) {
limit = parseInt(limit, 10);
var out = [],
i, n;
if (limit > 0) {
i = 0;
n = limit;
} else {
i = array.length + limit;
n = array.length;
}
for (; i
{{amount | currency}}
it('should init with 1234.56', function(){
expect(binding('amount | currency')).toBe('$1,234.56');
});
it('should update', function(){
input('amount').enter('-1234');
expect(binding('amount | currency')).toBe('$-1,234.00');
expect(element('.doc-example-live .ng-binding').attr('className')).
toMatch(/ng-format-negative/);
});
*/
angularFilter.currency = function(amount){
this.$element.toggleClass('ng-format-negative', amount < 0);
return '$' + angularFilter.number.apply(this, [amount, 2]);
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.number
* @function
*
* @description
* Formats a number as text.
*
* If the input is not a number empty string is returned.
*
* @param {number|string} number Number to format.
* @param {(number|string)=} [fractionSize=2] Number of decimal places to round the number to.
* @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
*
* @example
Enter number:
Default formatting: {{val | number}}
No fractions: {{val | number:0}}
Negative number: {{-val | number:4}}
it('should format numbers', function(){
expect(binding('val | number')).toBe('1,234.57');
expect(binding('val | number:0')).toBe('1,235');
expect(binding('-val | number:4')).toBe('-1,234.5679');
});
it('should update', function(){
input('val').enter('3374.333');
expect(binding('val | number')).toBe('3,374.33');
expect(binding('val | number:0')).toBe('3,374');
expect(binding('-val | number:4')).toBe('-3,374.3330');
});
*/
angularFilter.number = function(number, fractionSize){
if (isNaN(number) || !isFinite(number)) {
return '';
}
fractionSize = isUndefined(fractionSize)? 2 : fractionSize;
var isNegative = number < 0,
pow = Math.pow(10, fractionSize),
whole = '' + number,
formatedText = '',
i;
if (whole.indexOf('e') > -1) return whole;
number = Math.round(number * pow) / pow;
fraction = ('' + number).split('.');
whole = fraction[0];
fraction = fraction[1] || '';
if (isNegative) {
formatedText = '-';
whole = whole.substring(1);
}
for (i = 0; i < whole.length; i++) {
if ((whole.length - i)%3 === 0 && i !== 0) {
formatedText += ',';
}
formatedText += whole.charAt(i);
}
if (fractionSize) {
while(fraction.length < fractionSize) {
fraction += '0';
}
formatedText += '.' + fraction.substring(0, fractionSize);
}
return formatedText;
};
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
neg = '-';
num = -num;
}
num = '' + num;
while(num.length < digits) num = '0' + num;
if (trim)
num = num.substr(num.length - digits);
return neg + num;
}
function dateGetter(name, size, offset, trim) {
return function(date) {
var value = date['get' + name]();
if (offset > 0 || value > -offset)
value += offset;
if (value === 0 && offset == -12 ) value = 12;
return padNumber(value, size, trim);
};
}
function dateStrGetter(name, shortForm) {
return function(date) {
var value = date['get' + name]();
if(name == 'Month') {
value = MONTH[value];
} else {
value = DAY[value];
}
return shortForm ? value.substr(0,3) : value;
};
}
var DAY = 'Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday'.split(',');
var MONTH = 'January,February,March,April,May,June,July,August,September,October,November,December'.
split(',');
var DATE_FORMATS = {
yyyy: dateGetter('FullYear', 4),
yy: dateGetter('FullYear', 2, 0, true),
MMMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
H: dateGetter('Hours', 1),
hh: dateGetter('Hours', 2, -12),
h: dateGetter('Hours', 1, -12),
mm: dateGetter('Minutes', 2),
m: dateGetter('Minutes', 1),
ss: dateGetter('Seconds', 2),
s: dateGetter('Seconds', 1),
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true),
a: function(date){return date.getHours() < 12 ? 'am' : 'pm';},
Z: function(date){
var offset = date.getTimezoneOffset();
return padNumber(offset / 60, 2) + padNumber(Math.abs(offset % 60), 2);
}
};
var DATE_FORMATS_SPLIT = /([^yMdHhmsaZE]*)(E+|y+|M+|d+|H+|h+|m+|s+|a|Z)(.*)/;
var NUMBER_STRING = /^\d+$/;
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.date
* @function
*
* @description
* Formats `date` to a string based on the requested `format`.
*
* `format` string can be composed of the following elements:
*
* * `'yyyy'`: 4 digit representation of year e.g. 2010
* * `'yy'`: 2 digit representation of year, padded (00-99)
* * `'MMMMM'`: Month in year (January‒December)
* * `'MMM'`: Month in year (Jan - Dec)
* * `'MM'`: Month in year, padded (01‒12)
* * `'M'`: Month in year (1‒12)
* * `'dd'`: Day in month, padded (01‒31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday‒Saturday)
* * `'EEE'`: Day in Week, (Sun-Sat)
* * `'HH'`: Hour in day, padded (00‒23)
* * `'H'`: Hour in day (0-23)
* * `'hh'`: Hour in am/pm, padded (01‒12)
* * `'h'`: Hour in am/pm, (1-12)
* * `'mm'`: Minute in hour, padded (00‒59)
* * `'m'`: Minute in hour (0-59)
* * `'ss'`: Second in minute, padded (00‒59)
* * `'s'`: Second in minute (0‒59)
* * `'a'`: am/pm marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200‒1200)
*
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
* number) or ISO 8601 extended datetime string (yyyy-MM-ddTHH:mm:ss.SSSZ).
* @param {string=} format Formatting rules. If not specified, Date#toLocaleDateString is used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}:
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}} {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}:
{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
it('should format date', function(){
expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).
toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} \-?\d{4}/);
expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(am|pm)/);
});
*/
angularFilter.date = function(date, format) {
if (isString(date)) {
if (NUMBER_STRING.test(date)) {
date = parseInt(date, 10);
} else {
date = angularString.toDate(date);
}
}
if (isNumber(date)) {
date = new Date(date);
}
if (!isDate(date)) {
return date;
}
var text = date.toLocaleDateString(), fn;
if (format && isString(format)) {
text = '';
var parts = [], match;
while(format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
parts = concat(parts, match, 1);
format = parts.pop();
} else {
parts.push(format);
format = null;
}
}
forEach(parts, function(value){
fn = DATE_FORMATS[value];
text += fn ? fn(date) : value;
});
}
return text;
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.json
* @function
*
* @description
* Allows you to convert a JavaScript object into JSON string.
*
* This filter is mostly useful for debugging. When using the double curly {{value}} notation
* the binding is automatically converted to JSON.
*
* @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
* @returns {string} JSON string.
*
* @css ng-monospace Always applied to the encapsulating element.
*
* @example:
{{ obj | json }}
it('should jsonify filtered objects', function() {
expect(binding('obj | json')).toBe('{\n "a":1,\n "b":[]}');
});
it('should update', function() {
input('objTxt').enter('[1, 2, 3]');
expect(binding('obj | json')).toBe('[1,2,3]');
});
*
*/
angularFilter.json = function(object) {
this.$element.addClass("ng-monospace");
return toJson(object, true);
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.lowercase
* @function
*
* @see angular.lowercase
*/
angularFilter.lowercase = lowercase;
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.uppercase
* @function
*
* @see angular.uppercase
*/
angularFilter.uppercase = uppercase;
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.html
* @function
*
* @description
* Prevents the input from getting escaped by angular. By default the input is sanitized and
* inserted into the DOM as is.
*
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
* it into the returned string, however since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer.
*
* If you hate your users, you may call the filter with optional 'unsafe' argument, which bypasses
* the html sanitizer, but makes your application vulnerable to XSS and other attacks. Using this
* option is strongly discouraged and should be used only if you absolutely trust the input being
* filtered and you can't get the content through the sanitizer.
*
* @param {string} html Html input.
* @param {string=} option If 'unsafe' then do not sanitize the HTML input.
* @returns {string} Sanitized or raw html.
*
* @example
Snippet:
Filter
Source
Rendered
html filter
<div ng:bind="snippet | html"> </div>
no filter
<div ng:bind="snippet"> </div>
unsafe html filter
<div ng:bind="snippet | html:'unsafe'"> </div>
it('should sanitize the html snippet ', function(){
expect(using('#html-filter').binding('snippet | html')).
toBe('
an html\nclick here\nsnippet
');
});
it('should escape snippet without any filter', function() {
expect(using('#escaped-html').binding('snippet')).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should inline raw snippet if filtered as unsafe', function() {
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).
toBe("
an html\n" +
"click here\n" +
"snippet
");
});
it('should update', function(){
input('snippet').enter('new text');
expect(using('#html-filter').binding('snippet | html')).toBe('new text');
expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>");
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new text');
});
*/
angularFilter.html = function(html, option){
return new HTML(html, option);
};
/**
* @workInProgress
* @ngdoc filter
* @name angular.filter.linky
* @function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plane email address links.
*
* @param {string} text Input text.
* @returns {string} Html-linkified text.
*
* @example
Snippet:
Filter
Source
Rendered
linky filter
<div ng:bind="snippet | linky"> </div>
no filter
<div ng:bind="snippet"> </div>
it('should linkify the snippet with urls', function(){
expect(using('#linky-filter').binding('snippet | linky')).
toBe('Pretty text with some links:\n' +
'http://angularjs.org/,\n' +
'us@somewhere.org,\n' +
'another@somewhere.org,\n' +
'and one more: ftp://127.0.0.1/.');
});
it ('should not linkify snippet without the linky filter', function() {
expect(using('#escaped-html').binding('snippet')).
toBe("Pretty text with some links:\n" +
"http://angularjs.org/,\n" +
"mailto:us@somewhere.org,\n" +
"another@somewhere.org,\n" +
"and one more: ftp://127.0.0.1/.");
});
it('should update', function(){
input('snippet').enter('new http://link.');
expect(using('#linky-filter').binding('snippet | linky')).
toBe('new http://link.');
expect(using('#escaped-html').binding('snippet')).toBe('new http://link.');
});
*/
//TODO: externalize all regexps
angularFilter.linky = function(text){
if (!text) return text;
var URL = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s\.\;\,\(\)\{\}\<\>]/;
var match;
var raw = text;
var html = [];
var writer = htmlSanitizeWriter(html);
var url;
var i;
while (match=raw.match(URL)) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2]==match[3]) url = 'mailto:' + url;
i = match.index;
writer.chars(raw.substr(0, i));
writer.start('a', {href:url});
writer.chars(match[0].replace(/^mailto:/, ''));
writer.end('a');
raw = raw.substring(i + match[0].length);
}
writer.chars(raw);
return new HTML(html.join(''));
};
/**
* @workInProgress
* @ngdoc overview
* @name angular.formatter
* @description
*
* Formatters are used for translating data formats between those used in for display and those used
* for storage.
*
* Following is the list of built-in angular formatters:
*
* * {@link angular.formatter.boolean boolean} - Formats user input in boolean format
* * {@link angular.formatter.index index} - Manages indexing into an HTML select widget
* * {@link angular.formatter.json json} - Formats user input in JSON format
* * {@link angular.formatter.list list} - Formats user input string as an array
* * {@link angular.formatter.number} - Formats user input strings as a number
* * {@link angular.formatter.trim} - Trims extras spaces from end of user input
*
* For more information about how angular formatters work, and how to create your own formatters,
* see {@link guide/dev_guide.templates.formatters Understanding Angular Formatters} in the angular
* Developer Guide.
*/
function formatter(format, parse) {return {'format':format, 'parse':parse || format};}
function toString(obj) {
return (isDefined(obj) && obj !== null) ? "" + obj : obj;
}
var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/;
angularFormatter.noop = formatter(identity, identity);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.json
*
* @description
* Formats the user input as JSON text.
*
* @returns {?string} A JSON string representation of the model.
*
* @example
data={{data}}
it('should format json', function(){
expect(binding('data')).toEqual('data={\n \"name\":\"misko\",\n \"project\":\"angular\"}');
input('data').enter('{}');
expect(binding('data')).toEqual('data={\n }');
});
*/
angularFormatter.json = formatter(toJson, function(value){
return fromJson(value || 'null');
});
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.boolean
*
* @description
* Use boolean formatter if you wish to store the data as boolean.
*
* @returns {boolean} Converts to `true` unless user enters (blank), `f`, `false`, `0`, `no`, `[]`.
*
* @example
Enter truthy text:
value={{value}}
it('should format boolean', function(){
expect(binding('value')).toEqual('value=false');
input('value').enter('truthy');
expect(binding('value')).toEqual('value=true');
});
*/
angularFormatter['boolean'] = formatter(toString, toBoolean);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.number
*
* @description
* Use number formatter if you wish to convert the user entered string to a number.
*
* @returns {number} Number from the parsed string.
*
* @example
Enter valid number:
value={{value}}
it('should format numbers', function(){
expect(binding('value')).toEqual('value=1234');
input('value').enter('5678');
expect(binding('value')).toEqual('value=5678');
});
*/
angularFormatter.number = formatter(toString, function(obj){
if (obj == null || NUMBER.exec(obj)) {
return obj===null || obj === '' ? null : 1*obj;
} else {
throw "Not a number";
}
});
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.list
*
* @description
* Use list formatter if you wish to convert the user entered string to an array.
*
* @returns {Array} Array parsed from the entered string.
*
* @example
Enter a list of items:
value={{value}}
it('should format lists', function(){
expect(binding('value')).toEqual('value=["chair","table"]');
this.addFutureAction('change to XYZ', function($window, $document, done){
$document.elements('.doc-example-live :input:last').val(',,a,b,').trigger('change');
done();
});
expect(binding('value')).toEqual('value=["a","b"]');
});
*/
angularFormatter.list = formatter(
function(obj) { return obj ? obj.join(", ") : obj; },
function(value) {
var list = [];
forEach((value || '').split(','), function(item){
item = trim(item);
if (item) list.push(item);
});
return list;
}
);
/**
* @workInProgress
* @ngdoc formatter
* @name angular.formatter.trim
*
* @description
* Use trim formatter if you wish to trim extra spaces in user text.
*
* @returns {String} Trim excess leading and trailing space.
*
* @example
Enter text with leading/trailing spaces:
value={{value|json}}
it('should format trim', function(){
expect(binding('value')).toEqual('value="book"');
this.addFutureAction('change to XYZ', function($window, $document, done){
$document.elements('.doc-example-live :input:last').val(' text ').trigger('change');
done();
});
expect(binding('value')).toEqual('value="text"');
});
*/
angularFormatter.trim = formatter(
function(obj) { return obj ? trim("" + obj) : ""; }
);
/**
* @workInProgress
* @ngdoc overview
* @name angular.validator
* @description
*
* Most of the built-in angular validators are used to check user input against defined types or
* patterns. You can easily create your own custom validators as well.
*
* Following is the list of built-in angular validators:
*
* * {@link angular.validator.asynchronous asynchronous()} - Provides asynchronous validation via a
* callback function.
* * {@link angular.validator.date date()} - Checks user input against default date format:
* "MM/DD/YYYY"
* * {@link angular.validator.email email()} - Validates that user input is a well-formed email
* address.
* * {@link angular.validator.integer integer()} - Validates that user input is an integer
* * {@link angular.validator.json json()} - Validates that user input is valid JSON
* * {@link angular.validator.number number()} - Validates that user input is a number
* * {@link angular.validator.phone phone()} - Validates that user input matches the pattern
* "1(123)123-1234"
* * {@link angular.validator.regexp regexp()} - Restricts valid input to a specified regular
* expression pattern
* * {@link angular.validator.url url()} - Validates that user input is a well-formed URL.
*
* For more information about how angular validators work, and how to create your own validators,
* see {@link guide/dev_guide.templates.validators Understanding Angular Validators} in the angular
* Developer Guide.
*/
extend(angularValidator, {
'noop': function() { return null; },
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.regexp
* @description
* Use regexp validator to restrict the input to any Regular Expression.
*
* @param {string} value value to validate
* @param {string|regexp} expression regular expression.
* @param {string=} msg error message to display.
* @css ng-validation-error
*
* @example
Enter valid SSN:
it('should invalidate non ssn', function(){
var textBox = element('.doc-example-live :input');
expect(textBox.attr('className')).not().toMatch(/ng-validation-error/);
expect(textBox.val()).toEqual('123-45-6789');
input('ssn').enter('123-45-67890');
expect(textBox.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'regexp': function(value, regexp, msg) {
if (!value.match(regexp)) {
return msg ||
"Value does not match expected format " + regexp + ".";
} else {
return null;
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.number
* @description
* Use number validator to restrict the input to numbers with an
* optional range. (See integer for whole numbers validator).
*
* @param {string} value value to validate
* @param {int=} [min=MIN_INT] minimum value.
* @param {int=} [max=MAX_INT] maximum value.
* @css ng-validation-error
*
* @example
Enter number:
Enter number greater than 10:
Enter number between 100 and 200:
it('should invalidate number', function(){
var n1 = element('.doc-example-live :input[name=n1]');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('n1').enter('1.x');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
var n2 = element('.doc-example-live :input[name=n2]');
expect(n2.attr('className')).not().toMatch(/ng-validation-error/);
input('n2').enter('9');
expect(n2.attr('className')).toMatch(/ng-validation-error/);
var n3 = element('.doc-example-live :input[name=n3]');
expect(n3.attr('className')).not().toMatch(/ng-validation-error/);
input('n3').enter('201');
expect(n3.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'number': function(value, min, max) {
var num = 1 * value;
if (num == value) {
if (typeof min != $undefined && num < min) {
return "Value can not be less than " + min + ".";
}
if (typeof min != $undefined && num > max) {
return "Value can not be greater than " + max + ".";
}
return null;
} else {
return "Not a number";
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.integer
* @description
* Use number validator to restrict the input to integers with an
* optional range. (See integer for whole numbers validator).
*
* @param {string} value value to validate
* @param {int=} [min=MIN_INT] minimum value.
* @param {int=} [max=MAX_INT] maximum value.
* @css ng-validation-error
*
* @example
Enter integer:
Enter integer equal or greater than 10:
Enter integer between 100 and 200 (inclusive):
it('should invalidate integer', function(){
var n1 = element('.doc-example-live :input[name=n1]');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('n1').enter('1.1');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
var n2 = element('.doc-example-live :input[name=n2]');
expect(n2.attr('className')).not().toMatch(/ng-validation-error/);
input('n2').enter('10.1');
expect(n2.attr('className')).toMatch(/ng-validation-error/);
var n3 = element('.doc-example-live :input[name=n3]');
expect(n3.attr('className')).not().toMatch(/ng-validation-error/);
input('n3').enter('100.1');
expect(n3.attr('className')).toMatch(/ng-validation-error/);
});
*/
'integer': function(value, min, max) {
var numberError = angularValidator['number'](value, min, max);
if (numberError) return numberError;
if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) {
return "Not a whole number";
}
return null;
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.date
* @description
* Use date validator to restrict the user input to a valid date
* in format in format MM/DD/YYYY.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
Enter valid date:
it('should invalidate date', function(){
var n1 = element('.doc-example-live :input');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('text').enter('123/123/123');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'date': function(value) {
var fields = /^(\d\d?)\/(\d\d?)\/(\d\d\d\d)$/.exec(value);
var date = fields ? new Date(fields[3], fields[1]-1, fields[2]) : 0;
return (date &&
date.getFullYear() == fields[3] &&
date.getMonth() == fields[1]-1 &&
date.getDate() == fields[2])
? null
: "Value is not a date. (Expecting format: 12/31/2009).";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.email
* @description
* Use email validator if you wist to restrict the user input to a valid email.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
Enter valid email:
it('should invalidate email', function(){
var n1 = element('.doc-example-live :input');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('text').enter('a@b.c');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'email': function(value) {
if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) {
return null;
}
return "Email needs to be in username@host.com format.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.phone
* @description
* Use phone validator to restrict the input phone numbers.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
Enter valid phone number:
it('should invalidate phone', function(){
var n1 = element('.doc-example-live :input');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('text').enter('+12345678');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'phone': function(value) {
if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) {
return null;
}
if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) {
return null;
}
return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.url
* @description
* Use phone validator to restrict the input URLs.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
Enter valid URL:
it('should invalidate url', function(){
var n1 = element('.doc-example-live :input');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('text').enter('abc://server/path');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'url': function(value) {
if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) {
return null;
}
return "URL needs to be in http://server[:port]/path format.";
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.json
* @description
* Use json validator if you wish to restrict the user input to a valid JSON.
*
* @param {string} value value to validate
* @css ng-validation-error
*
* @example
it('should invalidate json', function(){
var n1 = element('.doc-example-live :input');
expect(n1.attr('className')).not().toMatch(/ng-validation-error/);
input('json').enter('{name}');
expect(n1.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
'json': function(value) {
try {
fromJson(value);
return null;
} catch (e) {
return e.toString();
}
},
/**
* @workInProgress
* @ngdoc validator
* @name angular.validator.asynchronous
* @description
* Use asynchronous validator if the validation can not be computed
* immediately, but is provided through a callback. The widget
* automatically shows a spinning indicator while the validity of
* the widget is computed. This validator caches the result.
*
* @param {string} value value to validate
* @param {function(inputToValidate,validationDone)} validate function to call to validate the state
* of the input.
* @param {function(data)=} [update=noop] function to call when state of the
* validator changes
*
* @paramDescription
* The `validate` function (specified by you) is called as
* `validate(inputToValidate, validationDone)`:
*
* * `inputToValidate`: value of the input box.
* * `validationDone`: `function(error, data){...}`
* * `error`: error text to display if validation fails
* * `data`: data object to pass to update function
*
* The `update` function is optionally specified by you and is
* called by on input change. Since the
* asynchronous validator caches the results, the update
* function can be called without a call to `validate`
* function. The function is called as `update(data)`:
*
* * `data`: data object as passed from validate function
*
* @css ng-input-indicator-wait, ng-validation-error
*
* @example
This input is validated asynchronously:
it('should change color in delayed way', function(){
var textBox = element('.doc-example-live :input');
expect(textBox.attr('className')).not().toMatch(/ng-input-indicator-wait/);
expect(textBox.attr('className')).not().toMatch(/ng-validation-error/);
input('text').enter('X');
expect(textBox.attr('className')).toMatch(/ng-input-indicator-wait/);
sleep(.6);
expect(textBox.attr('className')).not().toMatch(/ng-input-indicator-wait/);
expect(textBox.attr('className')).toMatch(/ng-validation-error/);
});
*
*/
/*
* cache is attached to the element
* cache: {
* inputs : {
* 'user input': {
* response: server response,
* error: validation error
* },
* current: 'current input'
* }
*
*/
'asynchronous': function(input, asynchronousFn, updateFn) {
if (!input) return;
var scope = this;
var element = scope.$element;
var cache = element.data('$asyncValidator');
if (!cache) {
element.data('$asyncValidator', cache = {inputs:{}});
}
cache.current = input;
var inputState = cache.inputs[input],
$invalidWidgets = scope.$service('$invalidWidgets');
if (!inputState) {
cache.inputs[input] = inputState = { inFlight: true };
$invalidWidgets.markInvalid(scope.$element);
element.addClass('ng-input-indicator-wait');
asynchronousFn(input, function(error, data) {
inputState.response = data;
inputState.error = error;
inputState.inFlight = false;
if (cache.current == input) {
element.removeClass('ng-input-indicator-wait');
$invalidWidgets.markValid(element);
}
element.data($$validate)();
scope.$service('$updateView')();
});
} else if (inputState.inFlight) {
// request in flight, mark widget invalid, but don't show it to user
$invalidWidgets.markInvalid(scope.$element);
} else {
(updateFn||noop)(inputState.response);
}
return inputState.error;
}
});
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$cookieStore
* @requires $cookies
*
* @description
* Provides a key-value (string-object) storage, that is backed by session cookies.
* Objects put or retrieved from this storage are automatically serialized or
* deserialized by angular's toJson/fromJson.
* @example
*/
angularServiceInject('$cookieStore', function($store) {
return {
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$cookieStore#get
* @methodOf angular.service.$cookieStore
*
* @description
* Returns the value of given cookie key
*
* @param {string} key Id to use for lookup.
* @returns {Object} Deserialized cookie value.
*/
get: function(key) {
return fromJson($store[key]);
},
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$cookieStore#put
* @methodOf angular.service.$cookieStore
*
* @description
* Sets a value for given cookie key
*
* @param {string} key Id for the `value`.
* @param {Object} value Value to be stored.
*/
put: function(key, value) {
$store[key] = toJson(value);
},
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$cookieStore#remove
* @methodOf angular.service.$cookieStore
*
* @description
* Remove given cookie
*
* @param {string} key Id of the key-value pair to delete.
*/
remove: function(key) {
delete $store[key];
}
};
}, ['$cookies']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$cookies
* @requires $browser
*
* @description
* Provides read/write access to browser's cookies.
*
* Only a simple Object is exposed and by adding or removing properties to/from
* this object, new cookies are created/deleted at the end of current $eval.
*
* @example
*/
angularServiceInject('$cookies', function($browser) {
var rootScope = this,
cookies = {},
lastCookies = {},
lastBrowserCookies,
runEval = false;
//creates a poller fn that copies all cookies from the $browser to service & inits the service
$browser.addPollFn(function() {
var currentCookies = $browser.cookies();
if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl
lastBrowserCookies = currentCookies;
copy(currentCookies, lastCookies);
copy(currentCookies, cookies);
if (runEval) rootScope.$eval();
}
})();
runEval = true;
//at the end of each eval, push cookies
//TODO: this should happen before the "delayed" watches fire, because if some cookies are not
// strings or browser refuses to store some cookies, we update the model in the push fn.
this.$onEval(PRIORITY_LAST, push);
return cookies;
/**
* Pushes all the cookies from the service to the browser and verifies if all cookies were stored.
*/
function push(){
var name,
value,
browserCookies,
updated;
//delete any cookies deleted in $cookies
for (name in lastCookies) {
if (isUndefined(cookies[name])) {
$browser.cookies(name, undefined);
}
}
//update all cookies updated in $cookies
for(name in cookies) {
value = cookies[name];
if (!isString(value)) {
if (isDefined(lastCookies[name])) {
cookies[name] = lastCookies[name];
} else {
delete cookies[name];
}
} else if (value !== lastCookies[name]) {
$browser.cookies(name, value);
updated = true;
}
}
//verify what was actually stored
if (updated){
updated = false;
browserCookies = $browser.cookies();
for (name in cookies) {
if (cookies[name] !== browserCookies[name]) {
//delete or reset all cookies that the browser dropped from $cookies
if (isUndefined(browserCookies[name])) {
delete cookies[name];
} else {
cookies[name] = browserCookies[name];
}
updated = true;
}
}
}
}
}, ['$browser']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$defer
* @requires $browser
* @requires $exceptionHandler
* @requires $updateView
*
* @description
* Delegates to {@link angular.service.$browser $browser.defer}, but wraps the `fn` function
* into a try/catch block and delegates any exceptions to
* {@link angular.service.$exceptionHandler $exceptionHandler} service.
*
* In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions.
*
* @param {function()} fn A function, who's execution should be deferred.
* @param {number=} [delay=0] of milliseconds to defer the function execution.
*/
angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) {
return function(fn, delay) {
$browser.defer(function() {
try {
fn();
} catch(e) {
$exceptionHandler(e);
} finally {
$updateView();
}
}, delay);
};
}, ['$browser', '$exceptionHandler', '$updateView']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$document
* @requires $window
*
* @description
* A {@link angular.element jQuery (lite)}-wrapped reference to the browser's `window.document`
* element.
*/
angularServiceInject("$document", function(window){
return jqLite(window.document);
}, ['$window']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$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 overriden by
* {@link angular.mock.service.$exceptionHandler mock $exceptionHandler}
*
* @example
*/
var $exceptionHandlerFactory; //reference to be used only in tests
angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){
return function(e) {
$log.error(e);
};
}, ['$log']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$hover
* @requires $browser
* @requires $document
*
* @description
*
* @example
*/
angularServiceInject("$hover", function(browser, document) {
var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body);
browser.hover(function(element, show){
if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) {
if (!tooltip) {
tooltip = {
callout: jqLite(''),
arrow: jqLite(''),
title: jqLite(''),
content: jqLite('')
};
tooltip.callout.append(tooltip.arrow);
tooltip.callout.append(tooltip.title);
tooltip.callout.append(tooltip.content);
body.append(tooltip.callout);
}
var docRect = body[0].getBoundingClientRect(),
elementRect = element[0].getBoundingClientRect(),
leftSpace = docRect.right - elementRect.right - arrowWidth;
tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error...");
tooltip.content.text(error);
if (leftSpace < width) {
tooltip.arrow.addClass('ng-arrow-right');
tooltip.arrow.css({left: (width + 1)+'px'});
tooltip.callout.css({
position: 'fixed',
left: (elementRect.left - arrowWidth - width - 4) + "px",
top: (elementRect.top - 3) + "px",
width: width + "px"
});
} else {
tooltip.arrow.addClass('ng-arrow-left');
tooltip.callout.css({
position: 'fixed',
left: (elementRect.right + arrowWidth) + "px",
top: (elementRect.top - 3) + "px",
width: width + "px"
});
}
} else if (tooltip) {
tooltip.callout.remove();
tooltip = null;
}
});
}, ['$browser', '$document'], true);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$invalidWidgets
*
* @description
* Keeps references to all invalid widgets found during validation.
* Can be queried to find whether there are any invalid widgets currently displayed.
*
* @example
*/
angularServiceInject("$invalidWidgets", function(){
var invalidWidgets = [];
/** Remove an element from the array of invalid widgets */
invalidWidgets.markValid = function(element){
var index = indexOf(invalidWidgets, element);
if (index != -1)
invalidWidgets.splice(index, 1);
};
/** Add an element to the array of invalid widgets */
invalidWidgets.markInvalid = function(element){
var index = indexOf(invalidWidgets, element);
if (index === -1)
invalidWidgets.push(element);
};
/** Return count of all invalid widgets that are currently visible */
invalidWidgets.visible = function() {
var count = 0;
forEach(invalidWidgets, function(widget){
count = count + (isVisible(widget) ? 1 : 0);
});
return count;
};
/* At the end of each eval removes all invalid widgets that are not part of the current DOM. */
this.$onEval(PRIORITY_LAST, function() {
for(var i = 0; i < invalidWidgets.length;) {
var widget = invalidWidgets[i];
if (isOrphan(widget[0])) {
invalidWidgets.splice(i, 1);
if (widget.dealoc) widget.dealoc();
} else {
i++;
}
}
});
/**
* Traverses DOM element's (widget's) parents and considers the element to be an orphant if one of
* it's parents isn't the current window.document.
*/
function isOrphan(widget) {
if (widget == window.document) return false;
var parent = widget.parentNode;
return !parent || isOrphan(parent);
}
return invalidWidgets;
});
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$location
* @requires $browser
*
* @property {string} href The full URL of the current location.
* @property {string} protocol The protocol part of the URL (e.g. http or https).
* @property {string} host The host name, ip address or FQDN of the current location.
* @property {number} port The port number of the current location (e.g. 80, 443, 8080).
* @property {string} path The path of the current location (e.g. /myapp/inbox).
* @property {Object.} search Map of query parameters (e.g. {user:"foo", page:23}).
* @property {string} hash The fragment part of the URL of the current location (e.g. #foo).
* @property {string} hashPath Similar to `path`, but located in the `hash` fragment
* (e.g. ../foo#/some/path => /some/path).
* @property {Object.} hashSearch Similar to `search` but located in `hash`
* fragment (e.g. .../foo#/some/path?hashQuery=param => {hashQuery: "param"}).
*
* @description
* Parses the browser location url and makes it available to your application.
* Any changes to the url are reflected into `$location` service and changes to
* `$location` are reflected in the browser location url.
*
* Notice that using browser's forward/back buttons changes the $location.
*
* @example
it('should initialize the input field', function() {
expect(using('.doc-example-live').input('$location.hash').val()).
toBe('!/api/angular.service.$location');
});
it('should bind $location.hash to the input field', function() {
using('.doc-example-live').input('$location.hash').enter('foo');
expect(browser().location().hash()).toBe('foo');
});
it('should set the hash to a test string with test link is presed', function() {
using('.doc-example-live').element('#ex-test').click();
expect(using('.doc-example-live').input('$location.hash').val()).
toBe('myPath?name=misko');
});
it('should reset $location when reset link is pressed', function() {
using('.doc-example-live').input('$location.hash').enter('foo');
using('.doc-example-live').element('#ex-reset').click();
expect(using('.doc-example-live').input('$location.hash').val()).
toBe('!/api/angular.service.$location');
});
*/
angularServiceInject("$location", function($browser) {
var scope = this,
location = {update:update, updateHash: updateHash},
lastLocation = {};
$browser.onHashChange(function() { //register
update($browser.getUrl());
copy(location, lastLocation);
scope.$eval();
})(); //initialize
this.$onEval(PRIORITY_FIRST, sync);
this.$onEval(PRIORITY_LAST, updateBrowser);
return location;
// PUBLIC METHODS
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$location#update
* @methodOf angular.service.$location
*
* @description
* Updates the location object.
*
* Does not immediately update the browser. Instead the browser is updated at the end of $eval()
* cycle.
*
*
*
* @param {string|Object} path A hashPath or hashSearch object
* @param {Object=} search A hashSearch object
*/
function updateHash(path, search) {
var hash = {};
if (isString(path)) {
hash.hashPath = path;
hash.hashSearch = search || {};
} else
hash.hashSearch = path;
hash.hash = composeHash(hash);
update({hash: hash});
}
// INNER METHODS
/**
* Synchronizes all location object properties.
*
* User is allowed to change properties, so after property change,
* location object is not in consistent state.
*
* Properties are synced with the following precedence order:
*
* - `$location.href`
* - `$location.hash`
* - everything else
*
* Keep in mind that if the following code is executed:
*
* scope.$location.href = 'http://www.angularjs.org/path#a/b'
*
* immediately afterwards all other properties are still the old ones...
*
* This method checks the changes and update location to the consistent state
*/
function sync() {
if (!equals(location, lastLocation)) {
if (location.href != lastLocation.href) {
update(location.href);
return;
}
if (location.hash != lastLocation.hash) {
var hash = parseHash(location.hash);
updateHash(hash.hashPath, hash.hashSearch);
} else {
location.hash = composeHash(location);
location.href = composeHref(location);
}
update(location.href);
}
}
/**
* If location has changed, update the browser
* This method is called at the end of $eval() phase
*/
function updateBrowser() {
sync();
if ($browser.getUrl() != location.href) {
$browser.setUrl(location.href);
copy(location, lastLocation);
}
}
/**
* Compose href string from a location object
*
* @param {Object} loc The location object with all properties
* @return {string} Composed href
*/
function composeHref(loc) {
var url = toKeyValue(loc.search);
var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? null : loc.port);
return loc.protocol + '://' + loc.host +
(port ? ':' + port : '') + loc.path +
(url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : '');
}
/**
* Compose hash string from location object
*
* @param {Object} loc Object with hashPath and hashSearch properties
* @return {string} Hash string
*/
function composeHash(loc) {
var hashSearch = toKeyValue(loc.hashSearch);
//TODO: temporary fix for issue #158
return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') +
(hashSearch ? '?' + hashSearch : '');
}
/**
* Parse href string into location object
*
* @param {string} href
* @return {Object} The location object
*/
function parseHref(href) {
var loc = {};
var match = URL_MATCH.exec(href);
if (match) {
loc.href = href.replace(/#$/, '');
loc.protocol = match[1];
loc.host = match[3] || '';
loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || null;
loc.path = match[6] || '';
loc.search = parseKeyValue(match[8]);
loc.hash = match[10] || '';
extend(loc, parseHash(loc.hash));
}
return loc;
}
/**
* Parse hash string into object
*
* @param {string} hash
*/
function parseHash(hash) {
var h = {};
var match = HASH_MATCH.exec(hash);
if (match) {
h.hash = hash;
h.hashPath = unescape(match[1] || '');
h.hashSearch = parseKeyValue(match[3]);
}
return h;
}
}, ['$browser']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$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.
*
* @example
Reload this page with open console, enter text and hit the log button...
Message:
*/
var $logFactory; //reference to be used only in tests
angularServiceInject("$log", $logFactory = function($window){
return {
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$log#log
* @methodOf angular.service.$log
*
* @description
* Write a log message
*/
log: consoleLog('log'),
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$log#warn
* @methodOf angular.service.$log
*
* @description
* Write a warning message
*/
warn: consoleLog('warn'),
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$log#info
* @methodOf angular.service.$log
*
* @description
* Write an information message
*/
info: consoleLog('info'),
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$log#error
* @methodOf angular.service.$log
*
* @description
* Write an error message
*/
error: consoleLog('error')
};
function consoleLog(type) {
var console = $window.console || {};
var logFn = console[type] || console.log || noop;
if (logFn.apply) {
return function(){
var args = [];
forEach(arguments, function(arg){
args.push(formatError(arg));
});
return logFn.apply(console, args);
};
} else {
// we are IE, in which case there is nothing we can do
return logFn;
}
}
}, ['$window']);
/**
* @workInProgress
* @ngdoc service
* @name angular.service.$resource
* @requires $xhr.cache
*
* @description
* A factory which creates a resource object that lets you interact with
* [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
*
* The returned resource object has action methods which provide high-level behaviors without
* the need to interact with the low level {@link angular.service.$xhr $xhr} service or
* raw XMLHttpRequest.
*
* @param {string} url A parameterized URL template with parameters prefixed by `:` as in
* `/user/:username`.
*
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods.
*
* Each key value in the parameter object is first bound to url template if present and then any
* excess keys are appended to the url search query after the `?`.
*
* Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
* URL `/path/greet?salutation=Hello`.
*
* If the parameter value is prefixed with `@` then the value of that parameter is extracted from
* the data object (useful for non-GET operations).
*
* @param {Object.