8690 lines
266 KiB
JavaScript
8690 lines
266 KiB
JavaScript
/**
|
||
* 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, previousOnLoad){
|
||
////////////////////////////////////
|
||
|
||
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',
|
||
$$update = '$update',
|
||
$$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,
|
||
jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy
|
||
_ = window['_'],
|
||
/** holds major version number for IE or NaN for real browsers */
|
||
msie = parseInt((/msie (\d+)/.exec(lowercase(navigator.userAgent)) || [])[1], 10),
|
||
jqLite = jQuery || jqLiteWrap,
|
||
slice = Array.prototype.slice,
|
||
push = Array.prototype.push,
|
||
error = window[$console] ? bind(window[$console], window[$console]['error'] || noop) : noop,
|
||
|
||
angular = window[$angular] || (window[$angular] = {}),
|
||
angularTextMarkup = extensionMap(angular, 'markup'),
|
||
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(\?[^#]*)?(#(.*))?$/,
|
||
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.
|
||
*
|
||
<pre>
|
||
var values = {name: 'misko', gender: 'male'};
|
||
var log = [];
|
||
angular.foreach(values, function(value, key){
|
||
this.push(key + ': ' + value);
|
||
}, log);
|
||
expect(log).toEqual(['name: misko', 'gender:male']);
|
||
</pre>
|
||
*
|
||
* @param {Object|Array} obj Object to iterate over.
|
||
* @param {function()} iterator Iterator function.
|
||
* @param {Object} context Object to become context (`this`) for the iterator function.
|
||
* @returns {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(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.stack;
|
||
} else if (arg.sourceURL) {
|
||
arg = arg.message + '\n' + arg.sourceURL + ':' + arg.line;
|
||
}
|
||
}
|
||
return arg;
|
||
}
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.extend
|
||
* @function
|
||
*
|
||
* @description
|
||
* Extends the destination object `dst` by copying all of the properties from the `src` objects 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.
|
||
<pre>
|
||
function foo(callback) {
|
||
var result = calculateResult();
|
||
(callback || angular.noop)(result);
|
||
}
|
||
</pre>
|
||
*/
|
||
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.
|
||
*
|
||
<pre>
|
||
function transformer(transformationFn, value) {
|
||
return (transformationFn || identity)(value);
|
||
};
|
||
</pre>
|
||
*/
|
||
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)) {
|
||
if (isDefined(extPoint[name])) {
|
||
foreach(extPoint[name], function(property, key) {
|
||
if (key.charAt(0) == '$' && isUndefined(fn[key]))
|
||
fn[key] = property;
|
||
});
|
||
}
|
||
extPoint[name] = extend(fn, prop || {});
|
||
}
|
||
return extPoint[name];
|
||
});
|
||
}
|
||
|
||
function jqLiteWrap(element) {
|
||
// for some reasons the parentNode of an orphan looks like _null but its typeof is object.
|
||
if (element) {
|
||
if (isString(element)) {
|
||
var div = document.createElement('div');
|
||
div.innerHTML = element;
|
||
element = new JQLite(div.childNodes);
|
||
} else if (!(element instanceof JQLite) && isElement(element)) {
|
||
element = new JQLite(element);
|
||
}
|
||
}
|
||
return element;
|
||
}
|
||
|
||
|
||
/**
|
||
* @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;}
|
||
|
||
|
||
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 || node instanceof JQLite || (jQuery && node instanceof jQuery));
|
||
}
|
||
|
||
/**
|
||
* 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) {
|
||
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 quickClone(element) {
|
||
return jqLite(element[0].cloneNode(true));
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.Object.size
|
||
* @function
|
||
*
|
||
* @description
|
||
* Determines the number of elements in an array or number of properties of an object.
|
||
*
|
||
* Note: this function is used to augment the Object type in angular expressions. See
|
||
* {@link angular.Object} for more info.
|
||
*
|
||
* @param {Object|Array} obj Object or array to inspect.
|
||
* @returns {number} The size of `obj` or `0` if `obj` is not an object or array.
|
||
*
|
||
* @example
|
||
* Number of items in array: {{ [1,2].$size() }}<br/>
|
||
* Number of items in object: {{ {a:1, b:2, c:3}.$size() }}<br/>
|
||
*/
|
||
function size(obj) {
|
||
var size = 0;
|
||
if (obj) {
|
||
if (isNumber(obj.length)) {
|
||
return obj.length;
|
||
} else if (isObject(obj)){
|
||
for (key in obj)
|
||
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;
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.Object.copy
|
||
* @function
|
||
*
|
||
* @description
|
||
* Creates a deep copy of `source`.
|
||
*
|
||
* 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.
|
||
*
|
||
* If `source` is an object or an array, all of its members will be copied into the `destination`
|
||
* object.
|
||
*
|
||
* 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.
|
||
* @returns {*} The copy or updated `destination` if `destination` was specified.
|
||
*
|
||
* @example
|
||
Salutation: <input type="text" name="master.salutation" value="Hello" /><br/>
|
||
Name: <input type="text" name="master.name" value="world"/><br/>
|
||
<button ng:click="form = master.$copy()">copy</button>
|
||
<hr/>
|
||
|
||
Master is <span ng:hide="master.$equals(form)">NOT</span> same as form.
|
||
|
||
<pre>master={{master}}</pre>
|
||
<pre>form={{form}}</pre>
|
||
*/
|
||
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;
|
||
}
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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.
|
||
*
|
||
* Supports values types, arrays and objects.
|
||
*
|
||
* For objects `function` properties and properties that start with `$` are not considered during
|
||
* comparisons.
|
||
*
|
||
* 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: <input type="text" name="master.salutation" value="Hello" /><br/>
|
||
Name: <input type="text" name="master.name" value="world"/><br/>
|
||
<button ng:click="form = master.$copy()">copy</button>
|
||
<hr/>
|
||
|
||
Master is <span ng:hide="master.$equals(form)">NOT</span> same as form.
|
||
|
||
<pre>master={{master}}</pre>
|
||
<pre>form={{form}}</pre>
|
||
*/
|
||
function equals(o1, o2) {
|
||
if (o1 == o2) return true;
|
||
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<length; key++) {
|
||
if (!equals(o1[key], o2[key])) return false;
|
||
}
|
||
return true;
|
||
}
|
||
} else {
|
||
keySet = {};
|
||
for(key in o1) {
|
||
if (key.charAt(0) !== '$' && !isFunction(o1[key]) && !equals(o1[key], o2[key])) return false;
|
||
keySet[key] = true;
|
||
}
|
||
for(key in o2) {
|
||
if (!keySet[key] && key.charAt(0) !== '$' && !isFunction(o2[key])) return false;
|
||
}
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function setHtml(node, html) {
|
||
if (isLeafNode(node)) {
|
||
if (msie) {
|
||
node.innerText = html;
|
||
} else {
|
||
node.textContent = html;
|
||
}
|
||
} else {
|
||
node.innerHTML = html;
|
||
}
|
||
}
|
||
|
||
function isRenderableElement(element) {
|
||
var name = element && element[0] && element[0].nodeName;
|
||
return name && name.charAt(0) != '#' &&
|
||
!includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name);
|
||
}
|
||
function elementError(element, type, error) {
|
||
while (!isRenderableElement(element)) {
|
||
element = element.parent() || jqLite(document.body);
|
||
}
|
||
if (element[0]['$NG_ERROR'] !== error) {
|
||
element[0]['$NG_ERROR'] = error;
|
||
if (error) {
|
||
element.addClass(type);
|
||
element.attr(type, error);
|
||
} else {
|
||
element.removeClass(type);
|
||
element.removeAttr(type);
|
||
}
|
||
}
|
||
}
|
||
|
||
function concat(array1, array2, index) {
|
||
return array1.concat(slice.call(array2, index, array2.length));
|
||
}
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.bind
|
||
* @function
|
||
*
|
||
* @description
|
||
* Returns function which calls function `fn` bound to `self` (`self` becomes the `this` for `fn`).
|
||
* Optional `args` can be supplied which are prebound to the function, also known as
|
||
* [function currying](http://en.wikipedia.org/wiki/Currying).
|
||
*
|
||
* @param {Object} self Context in which `fn` should be evaluated in.
|
||
* @param {function()} fn Function to be bound.
|
||
* @param {...*} args Optional arguments to be prebound to the `fn` function call.
|
||
* @returns {function()} Function that wraps the `fn` with all the specified bindings.
|
||
*/
|
||
function bind(self, fn) {
|
||
var curryArgs = arguments.length > 2 ? slice.call(arguments, 2, arguments.length) : [];
|
||
if (typeof fn == $function) {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.compile
|
||
* @function
|
||
*
|
||
* @description
|
||
* Compiles a piece of HTML or DOM into a {@link angular.scope scope} object.
|
||
<pre>
|
||
var scope1 = angular.compile(window.document);
|
||
scope1.$init();
|
||
|
||
var scope2 = angular.compile('<div ng:click="clicked = true">click me</div>');
|
||
scope2.$init();
|
||
</pre>
|
||
*
|
||
* @param {string|DOMElement} element Element to compile.
|
||
* @param {Object=} parentScope Scope to become the parent scope of the newly compiled scope.
|
||
* @returns {Object} Compiled scope object.
|
||
*/
|
||
function compile(element, parentScope) {
|
||
var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget),
|
||
$element = jqLite(element);
|
||
return compiler.compile($element)($element, parentScope);
|
||
}
|
||
/////////////////////////////////////////////////
|
||
|
||
/**
|
||
* 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('&') : '';
|
||
}
|
||
|
||
/**
|
||
* @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
|
||
* This section explains how to bootstrap your application with angular using either the angular
|
||
* javascript file.
|
||
*
|
||
*
|
||
* ## The angular distribution
|
||
* Note that there are two versions of the angular javascript file that you can use:
|
||
*
|
||
* * `angular.js` - the development version - this file is unobfuscated, uncompressed, and thus
|
||
* human-readable and useful when developing your angular applications.
|
||
* * `angular.min.js` - the production version - this is a minified and obfuscated version of
|
||
* `angular.js`. You want to use this version when you want to load a smaller but functionally
|
||
* equivalent version of the code in your application. We use the Closure compiler to create this
|
||
* file.
|
||
*
|
||
*
|
||
* ## Auto-bootstrap with `ng:autobind`
|
||
* The simplest way to get an <angular/> application up and running is by inserting a script tag in
|
||
* your HTML file that bootstraps the `http://code.angularjs.org/angular-x.x.x.min.js` code and uses
|
||
* the special `ng:autobind` attribute, like in this snippet of HTML:
|
||
*
|
||
* <pre>
|
||
<!doctype html>
|
||
<html xmlns:ng="http://angularjs.org">
|
||
<head>
|
||
<script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
|
||
ng:autobind></script>
|
||
</head>
|
||
<body>
|
||
Hello {{'world'}}!
|
||
</body>
|
||
</html>
|
||
* </pre>
|
||
*
|
||
* The `ng:autobind` attribute tells <angular/> to compile and manage the whole HTML document. The
|
||
* compilation occurs in the page's `onLoad` handler. Note that you don't need to explicitly add an
|
||
* `onLoad` event; auto bind mode takes care of all the magic for you.
|
||
*
|
||
*
|
||
* ## Auto-bootstrap with `#autobind`
|
||
* In rare cases when you can't define the `ng` namespace before the script tag (e.g. in some CMS
|
||
* systems, etc), it is possible to auto-bootstrap angular by appending `#autobind` to the script
|
||
* src URL, like in this snippet:
|
||
*
|
||
* <pre>
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<script type="text/javascript"
|
||
src="http://code.angularjs.org/angular-0.9.3.min.js#autobind"></script>
|
||
</head>
|
||
<body>
|
||
<div xmlns:ng="http://angularjs.org">
|
||
Hello {{'world'}}!
|
||
</div>
|
||
</body>
|
||
</html>
|
||
* </pre>
|
||
*
|
||
* In this case it's the `#autobind` URL fragment that tells angular to auto-bootstrap.
|
||
*
|
||
*
|
||
* ## Filename Restrictions for Auto-bootstrap
|
||
* In order for us to find the auto-bootstrap script attribute or URL fragment, the value of the
|
||
* `script` `src` attribute that loads angular script must match one of these naming
|
||
* conventions:
|
||
*
|
||
* - `angular.js`
|
||
* - `angular-min.js`
|
||
* - `angular-x.x.x.js`
|
||
* - `angular-x.x.x.min.js`
|
||
* - `angular-x.x.x-xxxxxxxx.js` (dev snapshot)
|
||
* - `angular-x.x.x-xxxxxxxx.min.js` (dev snapshot)
|
||
* - `angular-bootstrap.js` (used for development of angular)
|
||
*
|
||
* Optionally, any of the filename format above can be prepended with relative or absolute URL that
|
||
* ends with `/`.
|
||
*
|
||
*
|
||
* ## Manual Bootstrap
|
||
* Using auto-bootstrap is a handy way to start using <angular/>, but advanced users who want more
|
||
* control over the initialization process might prefer to use manual bootstrap instead.
|
||
*
|
||
* The best way to get started with manual bootstraping is to look at the magic behind `ng:autobind`
|
||
* by writing out each step of the autobind process explicitly. Note that the following code is
|
||
* equivalent to the code in the previous section.
|
||
*
|
||
* <pre>
|
||
<!doctype html>
|
||
<html xmlns:ng="http://angularjs.org">
|
||
<head>
|
||
<script type="text/javascript" src="http://code.angularjs.org/angular-0.9.3.min.js"
|
||
ng:autobind></script>
|
||
<script type="text/javascript">
|
||
(function(window, previousOnLoad){
|
||
window.onload = function(){
|
||
try { (previousOnLoad||angular.noop)(); } catch(e) {}
|
||
angular.compile(window.document).$init();
|
||
};
|
||
})(window, window.onload);
|
||
</script>
|
||
</head>
|
||
<body>
|
||
Hello {{'World'}}!
|
||
</body>
|
||
</html>
|
||
* </pre>
|
||
*
|
||
* This is the sequence that your code should follow if you're bootstrapping angular on your own:
|
||
*
|
||
* * After the page is loaded, find the root of the HTML template, which is typically the root of
|
||
* the document.
|
||
* * Run the HTML compiler, which converts the templates into an executable, bi-directionally bound
|
||
* application.
|
||
*
|
||
*
|
||
* ##XML Namespace
|
||
* *IMPORTANT:* When using <angular/> you must declare the ng namespace using the xmlns tag. If you
|
||
* don't declare the namespace, Internet Explorer does not render widgets properly.
|
||
*
|
||
* <pre>
|
||
* <html xmlns:ng="http://angularjs.org">
|
||
* </pre>
|
||
*
|
||
*
|
||
* ## Create your own namespace
|
||
* If you want to define your own widgets, you must create your own namespace and use that namespace
|
||
* to form the fully qualified widget name. For example, you could map the alias `my` to your domain
|
||
* and create a widget called my:widget. To create your own namespace, simply add another xmlsn tag
|
||
* to your page, create an alias, and set it to your unique domain:
|
||
*
|
||
* <pre>
|
||
* <html xmlns:ng="http://angularjs.org" xmlns:my="http://mydomain.com">
|
||
* </pre>
|
||
*
|
||
*
|
||
* ## Global Object
|
||
* The <angular/> script creates a single global variable `angular` in the global namespace. All
|
||
* APIs are bound to fields of this global object.
|
||
*
|
||
*/
|
||
function angularInit(config){
|
||
if (config.autobind) {
|
||
// TODO default to the source of angular.js
|
||
var scope = compile(window.document, _null, {'$config':config}),
|
||
$browser = scope.$inject('$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);
|
||
|
||
scope.$init();
|
||
}
|
||
}
|
||
|
||
function angularJsConfig(document, config) {
|
||
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, '_');
|
||
if (name == 'autobind') value = true;
|
||
config[name] = value;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
return config;
|
||
}
|
||
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 (!json) return json;
|
||
|
||
var obj, p, expression;
|
||
|
||
try {
|
||
if (useNative && JSON && JSON.parse) {
|
||
obj = JSON.parse(json);
|
||
return transformDates(obj);
|
||
}
|
||
|
||
p = parser(json, true);
|
||
expression = p.primary();
|
||
p.assertAllConsumed();
|
||
return expression();
|
||
|
||
} 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<len; i++) {
|
||
var item = obj[i];
|
||
if (sep) buf.push(",");
|
||
if (!(item instanceof RegExp) && (isFunction(item) || isUndefined(item))) {
|
||
buf.push($null);
|
||
} else {
|
||
toJsonArray(buf, item, pretty, stack);
|
||
}
|
||
sep = true;
|
||
}
|
||
buf.push("]");
|
||
} else if (isDate(obj)) {
|
||
buf.push(angular['String']['quoteUnicode'](angular['Date']['toString'](obj)));
|
||
} else {
|
||
buf.push("{");
|
||
if (pretty) buf.push(pretty);
|
||
var comma = false;
|
||
var childPretty = pretty ? pretty + " " : false;
|
||
var keys = [];
|
||
for(var k in obj) {
|
||
if (obj[k] === _undefined)
|
||
continue;
|
||
keys.push(k);
|
||
}
|
||
keys.sort();
|
||
for ( var keyIndex = 0; keyIndex < keys.length; keyIndex++) {
|
||
var key = keys[keyIndex];
|
||
var value = obj[key];
|
||
if (typeof value != $function) {
|
||
if (comma) {
|
||
buf.push(",");
|
||
if (pretty) buf.push(pretty);
|
||
}
|
||
buf.push(angular['String']['quote'](key));
|
||
buf.push(":");
|
||
toJsonArray(buf, value, childPretty, stack);
|
||
comma = true;
|
||
}
|
||
}
|
||
buf.push("}");
|
||
}
|
||
}
|
||
if (isObject(obj)) {
|
||
stack.pop();
|
||
}
|
||
}
|
||
/**
|
||
* Template provides directions an how to bind to a given element.
|
||
* It contains a list of init functions which need to be called to
|
||
* bind to a new instance of elements. It also provides a list
|
||
* of child paths which contain child templates
|
||
*/
|
||
function Template(priority) {
|
||
this.paths = [];
|
||
this.children = [];
|
||
this.inits = [];
|
||
this.priority = priority;
|
||
this.newScope = false;
|
||
}
|
||
|
||
Template.prototype = {
|
||
init: function(element, scope) {
|
||
var inits = {};
|
||
this.collectInits(element, inits, scope);
|
||
foreachSorted(inits, function(queue){
|
||
foreach(queue, function(fn) {fn();});
|
||
});
|
||
},
|
||
|
||
collectInits: function(element, inits, scope) {
|
||
var queue = inits[this.priority], childScope = scope;
|
||
if (!queue) {
|
||
inits[this.priority] = queue = [];
|
||
}
|
||
element = jqLite(element);
|
||
if (this.newScope) {
|
||
childScope = createScope(scope);
|
||
scope.$onEval(childScope.$eval);
|
||
element.data($$scope, childScope);
|
||
}
|
||
foreach(this.inits, function(fn) {
|
||
queue.push(function() {
|
||
childScope.$tryEval(function(){
|
||
return childScope.$inject(fn, childScope, element);
|
||
}, element);
|
||
});
|
||
});
|
||
var i,
|
||
childNodes = element[0].childNodes,
|
||
children = this.children,
|
||
paths = this.paths,
|
||
length = paths.length;
|
||
for (i = 0; i < length; i++) {
|
||
children[i].collectInits(childNodes[paths[i]], inits, childScope);
|
||
}
|
||
},
|
||
|
||
|
||
addInit:function(init) {
|
||
if (init) {
|
||
this.inits.push(init);
|
||
}
|
||
},
|
||
|
||
|
||
addChild: function(index, template) {
|
||
if (template) {
|
||
this.paths.push(index);
|
||
this.children.push(template);
|
||
}
|
||
},
|
||
|
||
empty: function() {
|
||
return this.inits.length === 0 && this.paths.length === 0;
|
||
}
|
||
};
|
||
|
||
/*
|
||
* Function walks up the element chain looking for the scope associated with the give element.
|
||
*/
|
||
function retrieveScope(element) {
|
||
var scope;
|
||
while (element && !(scope = element.data($$scope))) {
|
||
element = element.parent();
|
||
}
|
||
return scope;
|
||
}
|
||
|
||
///////////////////////////////////
|
||
//Compiler
|
||
//////////////////////////////////
|
||
function Compiler(markup, attrMarkup, directives, widgets){
|
||
this.markup = markup;
|
||
this.attrMarkup = attrMarkup;
|
||
this.directives = directives;
|
||
this.widgets = widgets;
|
||
}
|
||
|
||
Compiler.prototype = {
|
||
compile: function(rawElement) {
|
||
rawElement = jqLite(rawElement);
|
||
var index = 0,
|
||
template,
|
||
parent = rawElement.parent();
|
||
if (parent && parent[0]) {
|
||
parent = parent[0];
|
||
for(var i = 0; i < parent.childNodes.length; i++) {
|
||
if (parent.childNodes[i] == rawElement[0]) {
|
||
index = i;
|
||
}
|
||
}
|
||
}
|
||
template = this.templatize(rawElement, index, 0) || new Template();
|
||
return function(element, parentScope){
|
||
element = jqLite(element);
|
||
var scope = parentScope && parentScope.$eval ?
|
||
parentScope : createScope(parentScope);
|
||
element.data($$scope, scope);
|
||
return extend(scope, {
|
||
$element:element,
|
||
$init: function() {
|
||
template.init(element, scope);
|
||
scope.$eval();
|
||
delete scope.$init;
|
||
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
|
||
*
|
||
* @exampleDescription
|
||
* try changing the invoice and see that the Total will lag in evaluation
|
||
* @example
|
||
<div>TOTAL: without ng:eval-order {{ items.$sum('total') | currency }}</div>
|
||
<div ng:eval-order='LAST'>TOTAL: with ng:eval-order {{ items.$sum('total') | currency }}</div>
|
||
<table ng:init="items=[{qty:1, cost:9.99, desc:'gadget'}]">
|
||
<tr>
|
||
<td>QTY</td>
|
||
<td>Description</td>
|
||
<td>Cost</td>
|
||
<td>Total</td>
|
||
<td></td>
|
||
</tr>
|
||
<tr ng:repeat="item in items">
|
||
<td><input name="item.qty"/></td>
|
||
<td><input name="item.desc"/></td>
|
||
<td><input name="item.cost"/></td>
|
||
<td>{{item.total = item.qty * item.cost | currency}}</td>
|
||
<td><a href="" ng:click="items.$remove(item)">X</a></td>
|
||
</tr>
|
||
<tr>
|
||
<td colspan="3"><a href="" ng:click="items.$add()">add</a></td>
|
||
<td>{{ items.$sum('total') | currency }}</td>
|
||
</tr>
|
||
</table>
|
||
*
|
||
* @scenario
|
||
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),
|
||
template,
|
||
selfApi = {
|
||
compile: bind(self, self.compile),
|
||
comment:function(text) {return jqLite(document.createComment(text));},
|
||
element:function(type) {return jqLite(document.createElement(type));},
|
||
text:function(text) {return jqLite(document.createTextNode(text));},
|
||
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;
|
||
}
|
||
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 (elementName.indexOf(':') > 0)
|
||
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<child.length; i++) {
|
||
if (isTextNode(child[i])) {
|
||
foreach(self.markup, function(markup){
|
||
if (i<child.length) {
|
||
var textNode = jqLite(child[i]);
|
||
markup.call(selfApi, textNode.text(), textNode, element);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (directives) {
|
||
// Process attributes/directives
|
||
eachAttribute(element, function(value, name){
|
||
foreach(self.attrMarkup, function(markup){
|
||
markup.call(selfApi, value, name, element);
|
||
});
|
||
});
|
||
eachAttribute(element, function(value, name){
|
||
fn = directiveFns[name];
|
||
if (fn) {
|
||
element.addClass('ng-directive');
|
||
template.addInit((directiveFns[name]).call(selfApi, value, element));
|
||
}
|
||
});
|
||
}
|
||
// Process non text child nodes
|
||
if (descend) {
|
||
eachNode(element, function(child, i){
|
||
template.addChild(i, self.templatize(child, i, priority));
|
||
});
|
||
}
|
||
return template.empty() ? _null : template;
|
||
}
|
||
};
|
||
|
||
function eachNode(element, fn){
|
||
var i, chldNodes = element[0].childNodes || [], chld;
|
||
for (i = 0; i < chldNodes.length; i++) {
|
||
if(!isTextNode(chld = chldNodes[i])) {
|
||
fn(jqLite(chld), i);
|
||
}
|
||
}
|
||
}
|
||
|
||
function eachAttribute(element, fn){
|
||
var i, attrs = element[0].attributes || [], chld, attr, name, value, attrValue = {};
|
||
for (i = 0; i < attrs.length; i++) {
|
||
attr = attrs[i];
|
||
name = attr.name;
|
||
value = attr.value;
|
||
if (msie && name == 'href') {
|
||
value = decodeURIComponent(element[0].getAttribute(name, 2));
|
||
}
|
||
attrValue[name] = value;
|
||
}
|
||
foreachSorted(attrValue, fn);
|
||
}
|
||
|
||
function getter(instance, path, unboundFn) {
|
||
if (!path) return instance;
|
||
var element = path.split('.');
|
||
var key;
|
||
var lastInstance = instance;
|
||
var len = element.length;
|
||
for ( var i = 0; i < len; i++) {
|
||
key = element[i];
|
||
if (!key.match(/^[\$\w][\$\w\d]*$/))
|
||
throw "Expression '" + path + "' is not a valid expression for accesing variables.";
|
||
if (instance) {
|
||
lastInstance = instance;
|
||
instance = instance[key];
|
||
}
|
||
if (isUndefined(instance) && key.charAt(0) == '$') {
|
||
var type = angular['Global']['typeOf'](lastInstance);
|
||
type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
|
||
var fn = type ? type[[key.substring(1)]] : _undefined;
|
||
if (fn) {
|
||
instance = bind(lastInstance, fn, lastInstance);
|
||
return instance;
|
||
}
|
||
}
|
||
}
|
||
if (!unboundFn && isFunction(instance)) {
|
||
return bind(lastInstance, instance);
|
||
}
|
||
return instance;
|
||
}
|
||
|
||
function setter(instance, path, value){
|
||
var element = path.split('.');
|
||
for ( var i = 0; element.length > 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();
|
||
p.assertAllConsumed();
|
||
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 model
|
||
* in the model-view-controller design pattern.
|
||
*
|
||
* A few other characteristics of scopes:
|
||
*
|
||
* - Scopes can be nested. A scope (prototypically) inherits properties from its parent scope.
|
||
* - Scopes can be attached (bound) to the HTML DOM tree (the view).
|
||
* - A scope {@link angular.scope.$become becomes} `this` for a controller.
|
||
* - Scope's {@link angular.scope.$eval $eval} is used to update its view.
|
||
* - Scopes can {@link angular.scope.$watch watch} properties and fire events.
|
||
*
|
||
* # Basic Operations
|
||
* Scopes can be created by calling {@link angular.scope() angular.scope()} or by compiling HTML.
|
||
*
|
||
* {@link angular.widget Widgets} and data bindings register listeners on the current scope to get
|
||
* notified of changes to the scope state. When notified, these listeners push the updated state
|
||
* through to the DOM.
|
||
*
|
||
* Here is a simple scope snippet to show how you can interact with the scope.
|
||
* <pre>
|
||
var scope = angular.scope();
|
||
scope.salutation = 'Hello';
|
||
scope.name = 'World';
|
||
|
||
expect(scope.greeting).toEqual(undefined);
|
||
|
||
scope.$watch('name', function(){
|
||
this.greeting = this.salutation + ' ' + this.name + '!';
|
||
});
|
||
|
||
expect(scope.greeting).toEqual('Hello World!');
|
||
scope.name = 'Misko';
|
||
// scope.$eval() will propagate the change to listeners
|
||
expect(scope.greeting).toEqual('Hello World!');
|
||
|
||
scope.$eval();
|
||
expect(scope.greeting).toEqual('Hello Misko!');
|
||
* </pre>
|
||
*
|
||
* # Inheritance
|
||
* A scope can inherit from a parent scope, as in this example:
|
||
* <pre>
|
||
var parent = angular.scope();
|
||
var child = angular.scope(parent);
|
||
|
||
parent.salutation = "Hello";
|
||
child.name = "World";
|
||
expect(child.salutation).toEqual('Hello');
|
||
|
||
child.salutation = "Welcome";
|
||
expect(child.salutation).toEqual('Welcome');
|
||
expect(parent.salutation).toEqual('Hello');
|
||
* </pre>
|
||
*
|
||
* # Dependency Injection
|
||
* Scope also acts as a simple dependency injection framework.
|
||
*
|
||
* **TODO**: more info needed
|
||
*
|
||
* # When scopes are evaluated
|
||
* Anyone can update a scope by calling its {@link angular.scope.$eval $eval()} method. By default
|
||
* angular widgets listen to user change events (e.g. the user enters text into text field), copy
|
||
* the data from the widget to the scope (the MVC model), and then call the `$eval()` method on the
|
||
* root scope to update dependents. This creates a spreadsheet-like behavior: the bound views update
|
||
* immediately as the user types into the text field.
|
||
*
|
||
* Similarly, when a request to fetch data from a server is made and the response comes back, the
|
||
* data is written into the model and then $eval() is called to push updates through to the view and
|
||
* any other dependents.
|
||
*
|
||
* Because a change in the model that's triggered either by user input or by server response calls
|
||
* `$eval()`, it is unnecessary to call `$eval()` from within your controller. The only time when
|
||
* calling `$eval()` is needed, is when implementing a custom widget or service.
|
||
*
|
||
* Because scopes are inherited, the child scope `$eval()` overrides the parent `$eval()` method.
|
||
* So to update the whole page you need to call `$eval()` on the root scope as `$root.$eval()`.
|
||
*
|
||
* Note: A widget that creates scopes (i.e. {@link angular.widget.@ng:repeat ng:repeat}) is
|
||
* responsible for forwarding `$eval()` calls from the parent to those child scopes. That way,
|
||
* calling $eval() on the root scope will update the whole page.
|
||
*
|
||
*
|
||
* @TODO THESE PARAMS AND RETURNS ARE NOT RENDERED IN THE TEMPLATE!! FIX THAT!
|
||
* @param {Object} parent The scope that should become the parent for the newly created scope.
|
||
* @param {Object.<string, function()>=} providers Map of service factory which need to be provided
|
||
* for the current scope. Usually {@link angular.service}.
|
||
* @param {Object.<string, *>=} instanceCache Provides pre-instantiated services which should
|
||
* append/override services provided by `providers`.
|
||
* @returns {Object} Newly created scope.
|
||
*
|
||
*
|
||
* @exampleDescription
|
||
* This example demonstrates scope inheritance and property overriding.
|
||
*
|
||
* In this example, the root scope encompasses the whole HTML DOM tree. This scope has `salutation`,
|
||
* `name`, and `names` properties. The {@link angular.widget@ng:repeat ng:repeat} creates a child
|
||
* scope, one for each element in the names array. The repeater also assigns $index and name into
|
||
* the child scope.
|
||
*
|
||
* Notice that:
|
||
*
|
||
* - While the name is set in the child scope it does not change the name defined in the root scope.
|
||
* - The child scope inherits the salutation property from the root scope.
|
||
* - The $index property does not leak from the child scope to the root scope.
|
||
*
|
||
* @example
|
||
<ul ng:init="salutation='Hello'; name='Misko'; names=['World', 'Earth']">
|
||
<li ng:repeat="name in names">
|
||
{{$index}}: {{salutation}} {{name}}!
|
||
</li>
|
||
</ul>
|
||
<pre>
|
||
$index={{$index}}
|
||
salutation={{salutation}}
|
||
name={{name}}</pre>
|
||
|
||
@scenario
|
||
it('should inherit the salutation property and override the name property', function() {
|
||
expect(using('.doc-example-live').repeater('li').row(0)).
|
||
toEqual(['0', 'Hello', 'World']);
|
||
expect(using('.doc-example-live').repeater('li').row(1)).
|
||
toEqual(['1', 'Hello', 'Earth']);
|
||
expect(using('.doc-example-live').element('pre').text()).
|
||
toBe('$index=\nsalutation=Hello\nname=Misko');
|
||
});
|
||
|
||
*/
|
||
function createScope(parent, providers, instanceCache) {
|
||
function Parent(){}
|
||
parent = Parent.prototype = (parent || {});
|
||
var instance = new Parent();
|
||
var evalLists = {sorted:[]};
|
||
|
||
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}.
|
||
|
||
<pre>
|
||
var scope = angular.scope();
|
||
var fn = scope.$bind(function(){
|
||
return this;
|
||
});
|
||
expect(fn()).toEqual(scope);
|
||
</pre>
|
||
*
|
||
* @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.
|
||
*
|
||
<pre>
|
||
var scope = angular.scope();
|
||
expect(scope.$get('person.name')).toEqual(undefined);
|
||
scope.person = {};
|
||
expect(scope.$get('person.name')).toEqual(undefined);
|
||
scope.person.name = 'misko';
|
||
expect(scope.$get('person.name')).toEqual('misko');
|
||
</pre>
|
||
*
|
||
* @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.
|
||
*
|
||
<pre>
|
||
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');
|
||
</pre>
|
||
*
|
||
* @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 it's 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.
|
||
*
|
||
* # Example
|
||
<pre>
|
||
var scope = angular.scope();
|
||
scope.a = 1;
|
||
scope.b = 2;
|
||
|
||
expect(scope.$eval('a+b')).toEqual(3);
|
||
expect(scope.$eval(function(){ return this.a + this.b; })).toEqual(3);
|
||
|
||
scope.$onEval('sum = a+b');
|
||
expect(scope.sum).toEqual(undefined);
|
||
scope.$eval();
|
||
expect(scope.sum).toEqual(3);
|
||
</pre>
|
||
*
|
||
* @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 exception is thrown then `exceptionHandler` is used to handle the exception.
|
||
*
|
||
* # Example
|
||
<pre>
|
||
var scope = angular.scope();
|
||
scope.error = function(){ throw 'myerror'; };
|
||
scope.$exceptionHandler = function(e) {this.lastException = e; };
|
||
|
||
expect(scope.$eval('error()'));
|
||
expect(scope.lastException).toEqual('myerror');
|
||
this.lastException = null;
|
||
|
||
expect(scope.$eval('error()'), function(e) {this.lastException = e; });
|
||
expect(scope.lastException).toEqual('myerror');
|
||
|
||
var body = angular.element(window.document.body);
|
||
expect(scope.$eval('error()'), body);
|
||
expect(body.attr('ng-exception')).toEqual('"myerror"');
|
||
expect(body.hasClass('ng-exception')).toEqual(true);
|
||
</pre>
|
||
*
|
||
* @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) {
|
||
(instance.$log || {error:error}).error(e);
|
||
if (isFunction(exceptionHandler)) {
|
||
exceptionHandler(e);
|
||
} else if (exceptionHandler) {
|
||
errorHandlerFor(exceptionHandler, e);
|
||
} else if (isFunction(instance.$exceptionHandler)) {
|
||
instance.$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 callback gets, by default, called upon registration, this can be prevented via the
|
||
* `initRun` parameter.
|
||
*
|
||
* # Example
|
||
<pre>
|
||
var scope = angular.scope();
|
||
scope.name = 'misko';
|
||
scope.counter = 0;
|
||
|
||
expect(scope.counter).toEqual(0);
|
||
scope.$watch('name', 'counter = counter + 1');
|
||
expect(scope.counter).toEqual(1);
|
||
|
||
scope.$eval();
|
||
expect(scope.counter).toEqual(1);
|
||
|
||
scope.name = 'adam';
|
||
scope.$eval();
|
||
expect(scope.counter).toEqual(2);
|
||
</pre>
|
||
*
|
||
* @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
|
||
<pre>
|
||
var scope = angular.scope();
|
||
scope.counter = 0;
|
||
scope.$onEval('counter = counter + 1');
|
||
expect(scope.counter).toEqual(0);
|
||
scope.$eval();
|
||
expect(scope.counter).toEqual(1);
|
||
</pre>
|
||
*
|
||
* @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.$inject.apply(instance, concat([Class, instance], arguments, 1));
|
||
|
||
//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;
|
||
(instance.$inject = createInjector(instance, providers, instanceCache))();
|
||
}
|
||
|
||
return instance;
|
||
}
|
||
/**
|
||
* @ngdoc function
|
||
* @name angular.injector
|
||
* @function
|
||
*
|
||
* @description
|
||
* Creates an inject function that can be used for dependency injection.
|
||
*
|
||
* @param {Object=} [providerScope={}] provider's `this`
|
||
* @param {Object.<string, function()>=} [providers=angular.service] Map of provider (factory)
|
||
* function.
|
||
* @param {Object.<string, function()>=} [cache={}] Place where instances are saved for reuse. Can
|
||
* also be used to override services speciafied by `providers` (useful in tests).
|
||
* @returns {function()} Injector function.
|
||
*
|
||
* @TODO These docs need a lot of work. Specifically the returned function should be described in
|
||
* great detail + we need to provide some examples.
|
||
*/
|
||
function createInjector(providerScope, providers, cache) {
|
||
providers = providers || angularService;
|
||
cache = cache || {};
|
||
providerScope = providerScope || {};
|
||
/**
|
||
* injection function
|
||
* @param value: string, array, object or function.
|
||
* @param scope: optional function "this"
|
||
* @param args: optional arguments to pass to function after injection
|
||
* parameters
|
||
* @returns depends on value:
|
||
* string: return an instance for the injection key.
|
||
* array of keys: returns an array of instances.
|
||
* function: look at $inject property of function to determine instances
|
||
* and then call the function with instances and `scope`. Any
|
||
* additional arguments (`args`) are appended to the function
|
||
* arguments.
|
||
* object: initialize eager providers and publish them the ones with publish here.
|
||
* none: same as object but use providerScope as place to publish.
|
||
*/
|
||
return function inject(value, scope, args){
|
||
var returnValue, provider, creation;
|
||
if (isString(value)) {
|
||
if (!cache.hasOwnProperty(value)) {
|
||
provider = providers[value];
|
||
if (!provider) throw "Unknown provider for '"+value+"'.";
|
||
cache[value] = inject(provider, providerScope);
|
||
}
|
||
returnValue = cache[value];
|
||
} else if (isArray(value)) {
|
||
returnValue = [];
|
||
foreach(value, function(name) {
|
||
returnValue.push(inject(name));
|
||
});
|
||
} else if (isFunction(value)) {
|
||
returnValue = inject(value.$inject || []);
|
||
returnValue = value.apply(scope, concat(returnValue, arguments, 2));
|
||
} else if (isObject(value)) {
|
||
foreach(providers, function(provider, name){
|
||
creation = provider.$creation;
|
||
if (creation == 'eager') {
|
||
inject(name);
|
||
}
|
||
if (creation == 'eager-published') {
|
||
setter(value, name, inject(name));
|
||
}
|
||
});
|
||
} else {
|
||
returnValue = inject(providerScope);
|
||
}
|
||
return returnValue;
|
||
};
|
||
}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<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 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);
|
||
return {
|
||
assertAllConsumed: assertAllConsumed,
|
||
primary: primary,
|
||
statements: statements,
|
||
validator: validator,
|
||
filter: filter,
|
||
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
|
||
watch: watch
|
||
};
|
||
|
||
///////////////////////////////////
|
||
function throwError(msg, token) {
|
||
throw Error("Parse Error: Token '" + token.text +
|
||
"' " + msg + " at column " +
|
||
(token.index + 1) + " of 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 assertAllConsumed(){
|
||
if (tokens.length !== 0) {
|
||
throwError("is extra token not part of expression", tokens[0]);
|
||
}
|
||
}
|
||
|
||
function statements(){
|
||
var statements = [];
|
||
while(true) {
|
||
if (tokens.length > 0 && !peek('}', ')', ';', ']'))
|
||
statements.push(filterChain());
|
||
if (!expect(';')) {
|
||
return 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 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;
|
||
};
|
||
}
|
||
|
||
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
|
||
function watch () {
|
||
var decl = [];
|
||
while(hasTokens()) {
|
||
decl.push(watchDecl());
|
||
if (!expect(';')) {
|
||
assertAllConsumed();
|
||
}
|
||
}
|
||
assertAllConsumed();
|
||
return function (self){
|
||
for ( var i = 0; i < decl.length; i++) {
|
||
var d = decl[i](self);
|
||
self.addListener(d.name, d.fn);
|
||
}
|
||
};
|
||
}
|
||
|
||
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 path = [];
|
||
var self = this;
|
||
var url = this.template;
|
||
params = params || {};
|
||
foreach(this.urlParams, function(_, urlParam){
|
||
var value = params[urlParam] || self.defaults[urlParam] || "";
|
||
url = url.replace(new RegExp(":" + urlParam + "(\\W)"), value + "$1");
|
||
});
|
||
url = url.replace(/\/?#$/, '');
|
||
var query = [];
|
||
foreachSorted(params, function(value, key){
|
||
if (!self.urlParams[key]) {
|
||
query.push(encodeURI(key) + '=' + encodeURI(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;
|
||
} 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 (status == 200) {
|
||
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.");
|
||
};
|
||
|
||
function Browser(location, document, head, XHR, $log, setTimeout) {
|
||
var self = this;
|
||
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
|
||
* @param {function(number, string)} callback Function that will be called on response
|
||
*
|
||
* @description
|
||
* Send ajax request
|
||
*/
|
||
self.xhr = function(method, url, post, callback) {
|
||
if (isFunction(post)) {
|
||
callback = post;
|
||
post = _null;
|
||
}
|
||
if (lowercase(method) == 'json') {
|
||
var callbackId = "angular_" + Math.random() + '_' + (idCounter++);
|
||
callbackId = callbackId.replace(/\d\./, '');
|
||
var script = document[0].createElement('script');
|
||
script.type = 'text/javascript';
|
||
script.src = url.replace('JSON_CALLBACK', callbackId);
|
||
window[callbackId] = function(data){
|
||
window[callbackId] = _undefined;
|
||
callback(200, data);
|
||
};
|
||
head.append(script);
|
||
} else {
|
||
var xhr = new XHR();
|
||
xhr.open(method, url, true);
|
||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||
xhr.setRequestHeader("Accept", "application/json, text/plain, */*");
|
||
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||
outstandingRequestCount ++;
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState == 4) {
|
||
completeOutstandingRequest(callback, xhr.status || 200, 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 = [];
|
||
|
||
/**
|
||
* @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
|
||
*
|
||
* @returns {function()} the added function
|
||
*/
|
||
self.addPollFn = function(fn) {
|
||
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();
|
||
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;
|
||
};
|
||
|
||
//////////////////////////////////////////////////////////////
|
||
// Cookies API
|
||
//////////////////////////////////////////////////////////////
|
||
var rawDocument = document[0];
|
||
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:
|
||
* <ul>
|
||
* <li>cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it</li>
|
||
* <li>cookies(name, value) -> set name to value, if value is undefined delete the cookie</li>
|
||
* <li>cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)</li>
|
||
* </ul>
|
||
*
|
||
* @returns {Object} Hash of all cookies (if called without any parameter)
|
||
*/
|
||
self.cookies = function (name, value) {
|
||
var cookieLength, cookieArray, i, keyValue;
|
||
|
||
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++) {
|
||
keyValue = cookieArray[i].split("=");
|
||
if (keyValue.length === 2) { //ignore nameless cookies
|
||
lastCookies[unescape(keyValue[0])] = unescape(keyValue[1]);
|
||
}
|
||
}
|
||
}
|
||
return lastCookies;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc
|
||
* @name angular.service.$browser#defer
|
||
* @methodOf angular.service.$browser
|
||
*
|
||
* @description
|
||
* Executes a fn asynchroniously via `setTimeout(fn, 0)`.
|
||
*
|
||
* 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 programaticaly flushed via
|
||
* `$browser.defer.flush()`.
|
||
*
|
||
* @param {function()} fn A function, who's execution should be defered.
|
||
*/
|
||
self.defer = function(fn) {
|
||
outstandingRequestCount++;
|
||
setTimeout(function() { completeOutstandingRequest(fn); }, 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 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);
|
||
head.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);
|
||
head.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 = /<!\[CDATA\[(.*?)]]>/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);
|
||
|
||
//see: http://www.w3.org/TR/html4/index/attributes.html
|
||
//Attributes that have their values filled in disabled="disabled"
|
||
var fillAttrs = makeMap("compact,ismap,nohref,nowrap");
|
||
//Attributes that have href and hence need to be sanitized
|
||
var uriAttrs = makeMap("background,href,longdesc,src,usemap");
|
||
var validAttrs = extend({}, fillAttrs, uriAttrs, makeMap(
|
||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
|
||
'color,cols,colspan,coords,dir,face,headers,height,hreflang,hspace,'+
|
||
'lang,language,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("<!--") === 0 ) {
|
||
index = 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 );
|
||
|
||
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");
|
||
|
||
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) {
|
||
var value = arguments[2] ? arguments[2] :
|
||
arguments[3] ? arguments[3] :
|
||
arguments[4] ? arguments[4] :
|
||
fillAttrs[name] ? name : "";
|
||
|
||
attrs[name] = decodeEntities(value); //value.replace(/(^|[^\\])"/g, '$1\\\"') //"
|
||
});
|
||
|
||
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-- )
|
||
handler.end( stack[ i ] );
|
||
|
||
// Remove the open elements from the stack
|
||
stack.length = pos;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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;
|
||
}
|
||
|
||
/**
|
||
* 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,"<");
|
||
return hiddenPre.innerText || hiddenPre.textContent;
|
||
}
|
||
|
||
/**
|
||
* Escapes all potentially dangerous characters, so that the
|
||
* resulting string can be safely inserted into attribute or
|
||
* element text.
|
||
* @param value
|
||
* @returns escaped text
|
||
*/
|
||
function encodeEntities(value) {
|
||
return value.
|
||
replace(/&/g, '&').
|
||
replace(NON_ALPHANUMERIC_REGEXP, function(value){
|
||
return '&#' + value.charCodeAt(0) + ';';
|
||
}).
|
||
replace(/</g, '<').
|
||
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
|
||
//////////////////////////////////
|
||
|
||
var jqCache = {},
|
||
jqName = 'ng-' + new Date().getTime(),
|
||
jqId = 1,
|
||
addEventListener = (window.document.attachEvent ?
|
||
function(element, type, fn) {element.attachEvent('on' + type, fn);} :
|
||
function(element, type, fn) {element.addEventListener(type, fn, false);}),
|
||
removeEventListener = (window.document.detachEvent ?
|
||
function(element, type, fn) {element.detachEvent('on' + type, fn); } :
|
||
function(element, type, fn) { element.removeEventListener(type, fn, false); });
|
||
|
||
function jqNextId() { return (jqId++); }
|
||
|
||
function jqClearData(element) {
|
||
var cacheId = element[jqName],
|
||
cache = jqCache[cacheId];
|
||
if (cache) {
|
||
foreach(cache.bind || {}, function(fn, type){
|
||
removeEventListener(element, type, fn);
|
||
});
|
||
delete jqCache[cacheId];
|
||
if (msie)
|
||
element[jqName] = ''; // ie does not allow deletion of attributes on elements.
|
||
else
|
||
delete element[jqName];
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
function JQLite(element) {
|
||
if (isElement(element)) {
|
||
this[0] = element;
|
||
this.length = 1;
|
||
} else if (isDefined(element.length) && element.item) {
|
||
for(var i=0; i < element.length; i++) {
|
||
this[i] = element[i];
|
||
}
|
||
this.length = element.length;
|
||
}
|
||
}
|
||
|
||
JQLite.prototype = {
|
||
data: function(key, value) {
|
||
var element = this[0],
|
||
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;
|
||
}
|
||
},
|
||
|
||
removeData: function(){
|
||
jqClearData(this[0]);
|
||
},
|
||
|
||
dealoc: function(){
|
||
(function dealoc(element){
|
||
jqClearData(element);
|
||
for ( var i = 0, children = element.childNodes; i < children.length; i++) {
|
||
dealoc(children[i]);
|
||
}
|
||
})(this[0]);
|
||
},
|
||
|
||
bind: function(type, fn){
|
||
var self = this,
|
||
element = self[0],
|
||
bind = self.data('bind'),
|
||
eventHandler;
|
||
if (!bind) this.data('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(self, event);
|
||
});
|
||
};
|
||
eventHandler.fns = [];
|
||
addEventListener(element, type, eventHandler);
|
||
}
|
||
eventHandler.fns.push(fn);
|
||
});
|
||
},
|
||
|
||
replaceWith: function(replaceNode) {
|
||
this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]);
|
||
},
|
||
|
||
children: function() {
|
||
return new JQLite(this[0].childNodes);
|
||
},
|
||
|
||
append: function(node) {
|
||
var self = this[0];
|
||
node = jqLite(node);
|
||
foreach(node, function(child){
|
||
self.appendChild(child);
|
||
});
|
||
},
|
||
|
||
remove: function() {
|
||
this.dealoc();
|
||
var parentNode = this[0].parentNode;
|
||
if (parentNode) parentNode.removeChild(this[0]);
|
||
},
|
||
|
||
removeAttr: function(name) {
|
||
this[0].removeAttribute(name);
|
||
},
|
||
|
||
after: function(element) {
|
||
this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling);
|
||
},
|
||
|
||
hasClass: function(selector) {
|
||
var className = " " + selector + " ";
|
||
if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) {
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
removeClass: function(selector) {
|
||
this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", ""));
|
||
},
|
||
|
||
toggleClass: function(selector, condition) {
|
||
var self = this;
|
||
(condition ? self.addClass : self.removeClass).call(self, selector);
|
||
},
|
||
|
||
addClass: function( selector ) {
|
||
if (!this.hasClass(selector)) {
|
||
this[0].className = trim(this[0].className + ' ' + selector);
|
||
}
|
||
},
|
||
|
||
css: function(name, value) {
|
||
var style = this[0].style;
|
||
if (isString(name)) {
|
||
if (isDefined(value)) {
|
||
style[name] = value;
|
||
} else {
|
||
return style[name];
|
||
}
|
||
} else {
|
||
extend(style, name);
|
||
}
|
||
},
|
||
|
||
attr: function(name, value){
|
||
var e = this[0];
|
||
if (isObject(name)) {
|
||
foreach(name, function(value, name){
|
||
e.setAttribute(name, value);
|
||
});
|
||
} else if (isDefined(value)) {
|
||
e.setAttribute(name, value);
|
||
} else {
|
||
// the extra argument is to get the right thing for a.href in IE, see jQuery code
|
||
return e.getAttribute(name, 2);
|
||
}
|
||
},
|
||
|
||
text: function(value) {
|
||
if (isDefined(value)) {
|
||
this[0].textContent = value;
|
||
}
|
||
return this[0].textContent;
|
||
},
|
||
|
||
val: function(value) {
|
||
if (isDefined(value)) {
|
||
this[0].value = value;
|
||
}
|
||
return this[0].value;
|
||
},
|
||
|
||
html: function(value) {
|
||
if (isDefined(value)) {
|
||
var i = 0, childNodes = this[0].childNodes;
|
||
for ( ; i < childNodes.length; i++) {
|
||
jqLite(childNodes[i]).dealoc();
|
||
}
|
||
this[0].innerHTML = value;
|
||
}
|
||
return this[0].innerHTML;
|
||
},
|
||
|
||
parent: function() {
|
||
return jqLite(this[0].parentNode);
|
||
},
|
||
|
||
clone: function() { return jqLite(this[0].cloneNode(true)); }
|
||
};
|
||
|
||
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;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
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;
|
||
}
|
||
};
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc overview
|
||
* @name angular.Object
|
||
* @function
|
||
*
|
||
* @description
|
||
* Utility functions for manipulation with JavaScript objects.
|
||
*
|
||
* These functions are exposed in two ways:
|
||
*
|
||
* - **in angular expressions**: the functions are bound to all objects and augment the Object
|
||
* type. The names of these methods are prefixed with `$` character to minimize naming collisions.
|
||
* To call a method, invoke the function without the first argument, e.g, `myObject.$foo(param2)`.
|
||
*
|
||
* - **in JavaScript code**: the functions don't augment the Object type and must be invoked as
|
||
* functions of `angular.Object` as `angular.Object.foo(myObject, param2)`.
|
||
*
|
||
*/
|
||
var angularCollection = {
|
||
'copy': copy,
|
||
'size': size,
|
||
'equals': equals
|
||
};
|
||
var angularObject = {
|
||
'extend': extend
|
||
};
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc overview
|
||
* @name angular.Array
|
||
*
|
||
* @description
|
||
* Utility functions for manipulation with JavaScript Array objects.
|
||
*
|
||
* These functions are exposed in two ways:
|
||
*
|
||
* - **in angular expressions**: the 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)`.
|
||
*
|
||
* - **in JavaScript code**: the functions don't augment the Array type and must be invoked as
|
||
* functions of `angular.Array` as `angular.Array.foo(myArrayObject, params)`.
|
||
*
|
||
*/
|
||
var angularArray = {
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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
|
||
<div ng:init="books = ['Moby Dick', 'Great Gatsby', 'Romeo and Juliet']"></div>
|
||
<input name='bookName' value='Romeo and Juliet'> <br>
|
||
Index of '{{bookName}}' in the list {{books}} is <em>{{books.$indexOf(bookName)}}</em>.
|
||
|
||
@scenario
|
||
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,
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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
|
||
<table ng:init="invoice= {items:[{qty:10, description:'gadget', cost:9.95}]}">
|
||
<tr><th>Qty</th><th>Description</th><th>Cost</th><th>Total</th><th></th></tr>
|
||
<tr ng:repeat="item in invoice.items">
|
||
<td><input name="item.qty" value="1" size="4" ng:required ng:validate="integer"></td>
|
||
<td><input name="item.description"></td>
|
||
<td><input name="item.cost" value="0.00" ng:required ng:validate="number" size="6"></td>
|
||
<td>{{item.qty * item.cost | currency}}</td>
|
||
<td>[<a href ng:click="invoice.items.$remove(item)">X</a>]</td>
|
||
</tr>
|
||
<tr>
|
||
<td><a href ng:click="invoice.items.$add()">add item</a></td>
|
||
<td></td>
|
||
<td>Total:</td>
|
||
<td>{{invoice.items.$sum('qty*cost') | currency}}</td>
|
||
</tr>
|
||
</table>
|
||
|
||
@scenario
|
||
//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 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;
|
||
},
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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
|
||
<ul ng:init="tasks=['Learn Angular', 'Read Documentation',
|
||
'Check out demos', 'Build cool applications']">
|
||
<li ng:repeat="task in tasks">
|
||
{{task}} [<a href="" ng:click="tasks.$remove(task)">X</a>]
|
||
</li>
|
||
</ul>
|
||
<hr/>
|
||
tasks = {{tasks}}
|
||
|
||
@scenario
|
||
it('should initialize the task list with for tasks', function() {
|
||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(4);
|
||
expect(repeater('.doc-example 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 ul li a:contains("X"):first').click();
|
||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(3);
|
||
|
||
element('.doc-example ul li a:contains("X"):last').click();
|
||
expect(repeater('.doc-example ul li', 'task in tasks').count()).toBe(2);
|
||
|
||
expect(repeater('.doc-example 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;
|
||
},
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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
|
||
<div ng:init="friends = [{name:'John', phone:'555-1276'},
|
||
{name:'Mary', phone:'800-BIG-MARY'},
|
||
{name:'Mike', phone:'555-4321'},
|
||
{name:'Adam', phone:'555-5678'},
|
||
{name:'Julie', phone:'555-8765'}]"></div>
|
||
|
||
Search: <input name="searchText"/>
|
||
<table id="searchTextResults">
|
||
<tr><th>Name</th><th>Phone</th><tr>
|
||
<tr ng:repeat="friend in friends.$filter(searchText)">
|
||
<td>{{friend.name}}</td>
|
||
<td>{{friend.phone}}</td>
|
||
<tr>
|
||
</table>
|
||
<hr>
|
||
Any: <input name="search.$"/> <br>
|
||
Name only <input name="search.name"/><br>
|
||
Phone only <input name="search.phone"/><br>
|
||
<table id="searchObjResults">
|
||
<tr><th>Name</th><th>Phone</th><tr>
|
||
<tr ng:repeat="friend in friends.$filter(search)">
|
||
<td>{{friend.name}}</td>
|
||
<td>{{friend.phone}}</td>
|
||
<tr>
|
||
</table>
|
||
|
||
@scenario
|
||
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, but it differs in that the value being added is optional and defaults to
|
||
* an emty 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.
|
||
*
|
||
* @exampleDescription
|
||
* This example shows how an initially empty array can be filled with objects created from user
|
||
* input via the `$add` method.
|
||
*
|
||
* @example
|
||
[<a href="" ng:click="people.$add()">add empty</a>]
|
||
[<a href="" ng:click="people.$add({name:'John', sex:'male'})">add 'John'</a>]
|
||
[<a href="" ng:click="people.$add({name:'Mary', sex:'female'})">add 'Mary'</a>]
|
||
|
||
<ul ng:init="people=[]">
|
||
<li ng:repeat="person in people">
|
||
<input name="person.name">
|
||
<select name="person.sex">
|
||
<option value="">--chose one--</option>
|
||
<option>male</option>
|
||
<option>female</option>
|
||
</select>
|
||
[<a href="" ng:click="people.$remove(person)">X</a>]
|
||
</li>
|
||
</ul>
|
||
<pre>people = {{people}}</pre>
|
||
|
||
@scenario
|
||
beforeEach(function() {
|
||
expect(binding('people')).toBe('people = []');
|
||
});
|
||
|
||
it('should create an empty record when "add empty" is clicked', function() {
|
||
element('.doc-example 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 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 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 a:contains("add empty")').click();
|
||
element('.doc-example li a:contains("X"):first').click();
|
||
expect(binding('people')).toBe('people = []');
|
||
});
|
||
*/
|
||
'add':function(array, value) {
|
||
array.push(isUndefined(value)? {} : value);
|
||
return array;
|
||
},
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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` evaluets 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
|
||
<pre ng:init="items = [{name:'knife', points:1},
|
||
{name:'fork', points:3},
|
||
{name:'spoon', points:1}]"></pre>
|
||
<ul>
|
||
<li ng:repeat="item in items">
|
||
{{item.name}}: points=
|
||
<input type="text" name="item.points"/> <!-- id="item{{$index}} -->
|
||
</li>
|
||
</ul>
|
||
<p>Number of items which have one point: <em>{{ items.$count('points==1') }}</em></p>
|
||
<p>Number of items which have more than one point: <em>{{items.$count('points>1')}}</em></p>
|
||
|
||
@scenario
|
||
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 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;
|
||
},
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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`: JavaScript's Array#sort comparator function
|
||
* - `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
|
||
<div ng:init="friends = [{name:'John', phone:'555-1212', age:10},
|
||
{name:'Mary', phone:'555-9876', age:19},
|
||
{name:'Mike', phone:'555-4321', age:21},
|
||
{name:'Adam', phone:'555-5678', age:35},
|
||
{name:'Julie', phone:'555-8765', age:29}]"></div>
|
||
|
||
<pre>Sorting predicate = {{predicate}}</pre>
|
||
<hr/>
|
||
<table ng:init="predicate='-age'">
|
||
<tr>
|
||
<th><a href="" ng:click="predicate = 'name'">Name</a>
|
||
(<a href ng:click="predicate = '-name'">^</a>)</th>
|
||
<th><a href="" ng:click="predicate = 'phone'">Phone</a>
|
||
(<a href ng:click="predicate = '-phone'">^</a>)</th>
|
||
<th><a href="" ng:click="predicate = 'age'">Age</a>
|
||
(<a href ng:click="predicate = '-age'">^</a>)</th>
|
||
<tr>
|
||
<tr ng:repeat="friend in friends.$orderBy(predicate)">
|
||
<td>{{friend.name}}</td>
|
||
<td>{{friend.phone}}</td>
|
||
<td>{{friend.age}}</td>
|
||
<tr>
|
||
</table>
|
||
|
||
@scenario
|
||
it('should be reverse ordered by aged', function() {
|
||
expect(binding('predicate')).toBe('Sorting predicate = -age');
|
||
expect(repeater('.doc-example table', 'friend in friends').column('friend.age')).
|
||
toEqual(['35', '29', '21', '19', '10']);
|
||
expect(repeater('.doc-example 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 a:contains("Name")').click();
|
||
expect(repeater('.doc-example table', 'friend in friends').column('friend.name')).
|
||
toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
|
||
expect(repeater('.doc-example table', 'friend in friends').column('friend.age')).
|
||
toEqual(['35', '10', '29', '19', '21']);
|
||
|
||
element('.doc-example a:contains("Phone")+a:contains("^")').click();
|
||
expect(repeater('.doc-example table', 'friend in friends').column('friend.phone')).
|
||
toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
|
||
expect(repeater('.doc-example table', 'friend in friends').column('friend.name')).
|
||
toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
|
||
});
|
||
*/
|
||
//TODO: WTH is descend param for and how/when it should be used, how is it affected by +/- in
|
||
// predicate? the code below is impossible to read and specs are not very good.
|
||
'orderBy':function(array, expression, descend) {
|
||
expression = isArray(expression) ? expression: [expression];
|
||
expression = map(expression, function($){
|
||
var descending = false, get = $ || identity;
|
||
if (isString($)) {
|
||
if (($.charAt(0) == '+' || $.charAt(0) == '-')) {
|
||
descending = $.charAt(0) == '-';
|
||
$ = $.substring(1);
|
||
}
|
||
get = expressionCompile($).fnSelf;
|
||
}
|
||
return reverse(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(reverse(comparator, descend));
|
||
|
||
function comparator(o1, o2){
|
||
for ( var i = 0; i < expression.length; i++) {
|
||
var comp = expression[i](o1, o2);
|
||
if (comp !== 0) return comp;
|
||
}
|
||
return 0;
|
||
}
|
||
function reverse(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;
|
||
}
|
||
}
|
||
},
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @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} New array of length `limit`.
|
||
*
|
||
*/
|
||
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<n; i++) {
|
||
out.push(array[i]);
|
||
}
|
||
|
||
return out;
|
||
}
|
||
};
|
||
|
||
var R_ISO8061_STR = /^(\d{4})-(\d\d)-(\d\d)(?:T(\d\d)(?:\:(\d\d)(?:\:(\d\d)(?:\.(\d{3}))?)?)?Z)?$/;
|
||
|
||
var angularString = {
|
||
'quote':function(string) {
|
||
return '"' + string.replace(/\\/g, '\\\\').
|
||
replace(/"/g, '\\"').
|
||
replace(/\n/g, '\\n').
|
||
replace(/\f/g, '\\f').
|
||
replace(/\r/g, '\\r').
|
||
replace(/\t/g, '\\t').
|
||
replace(/\v/g, '\\v') +
|
||
'"';
|
||
},
|
||
'quoteUnicode':function(string) {
|
||
var str = angular['String']['quote'](string);
|
||
var chars = [];
|
||
for ( var i = 0; i < str.length; i++) {
|
||
var ch = str.charCodeAt(i);
|
||
if (ch < 128) {
|
||
chars.push(str.charAt(i));
|
||
} else {
|
||
var encode = "000" + ch.toString(16);
|
||
chars.push("\\u" + encode.substring(encode.length - 4));
|
||
}
|
||
}
|
||
return chars.join('');
|
||
},
|
||
|
||
/**
|
||
* Tries to convert input to date and if successful returns the date, otherwise returns the input.
|
||
* @param {string} string
|
||
* @return {(Date|string)}
|
||
*/
|
||
'toDate':function(string){
|
||
var match;
|
||
if (isString(string) && (match = string.match(R_ISO8061_STR))){
|
||
var date = new Date(0);
|
||
date.setUTCFullYear(match[1], match[2] - 1, match[3]);
|
||
date.setUTCHours(match[4]||0, match[5]||0, match[6]||0, match[7]||0);
|
||
return date;
|
||
}
|
||
return string;
|
||
}
|
||
};
|
||
|
||
var angularDate = {
|
||
'toString':function(date){
|
||
return !date ?
|
||
date :
|
||
date.toISOString ?
|
||
date.toISOString() :
|
||
padNumber(date.getUTCFullYear(), 4) + '-' +
|
||
padNumber(date.getUTCMonth() + 1, 2) + '-' +
|
||
padNumber(date.getUTCDate(), 2) + 'T' +
|
||
padNumber(date.getUTCHours(), 2) + ':' +
|
||
padNumber(date.getUTCMinutes(), 2) + ':' +
|
||
padNumber(date.getUTCSeconds(), 2) + '.' +
|
||
padNumber(date.getUTCMilliseconds(), 3) + 'Z';
|
||
}
|
||
};
|
||
|
||
var angularFunction = {
|
||
'compile':function(expression) {
|
||
if (isFunction(expression)){
|
||
return expression;
|
||
} else if (expression){
|
||
return expressionCompile(expression).fnSelf;
|
||
} else {
|
||
return identity;
|
||
}
|
||
}
|
||
};
|
||
|
||
function defineApi(dst, chain){
|
||
angular[dst] = angular[dst] || {};
|
||
foreach(chain, function(parent){
|
||
extend(angular[dst], parent);
|
||
});
|
||
}
|
||
defineApi('Global', [angularGlobal]);
|
||
defineApi('Collection', [angularGlobal, angularCollection]);
|
||
defineApi('Array', [angularGlobal, angularCollection, angularArray]);
|
||
defineApi('Object', [angularGlobal, angularCollection, angularObject]);
|
||
defineApi('String', [angularGlobal, angularString]);
|
||
defineApi('Date', [angularGlobal, angularDate]);
|
||
//IE bug
|
||
angular['Date']['toString'] = angularDate['toString'];
|
||
defineApi('Function', [angularGlobal, angularCollection, angularFunction]);
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc filter
|
||
* @name angular.filter.currency
|
||
* @function
|
||
*
|
||
* @description
|
||
* Formats a number as a currency (ie $1,234.56).
|
||
*
|
||
* @param {number} amount Input to filter.
|
||
* @returns {string} Formated number.
|
||
*
|
||
* @css ng-format-negative
|
||
* When the value is negative, this css class is applied to the binding making it by default red.
|
||
*
|
||
* @example
|
||
<input type="text" name="amount" value="1234.56"/> <br/>
|
||
{{amount | currency}}
|
||
*
|
||
* @scenario
|
||
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: <input name='val' value='1234.56789' /><br/>
|
||
Default formatting: {{val | number}}<br/>
|
||
No fractions: {{val | number:0}}<br/>
|
||
Negative number: {{-val | number:4}}
|
||
|
||
* @scenario
|
||
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 = typeof fractionSize == $undefined ? 2 : fractionSize;
|
||
var isNegative = number < 0;
|
||
number = Math.abs(number);
|
||
var pow = Math.pow(10, fractionSize);
|
||
var text = "" + Math.round(number * pow);
|
||
var whole = text.substring(0, text.length - fractionSize);
|
||
whole = whole || '0';
|
||
var frc = text.substring(text.length - fractionSize);
|
||
text = isNegative ? '-' : '';
|
||
for (var i = 0; i < whole.length; i++) {
|
||
if ((whole.length - i)%3 === 0 && i !== 0) {
|
||
text += ',';
|
||
}
|
||
text += whole.charAt(i);
|
||
}
|
||
if (fractionSize > 0) {
|
||
for (var j = frc.length; j < fractionSize; j++) {
|
||
frc += '0';
|
||
}
|
||
text += '.' + frc.substring(0, fractionSize);
|
||
}
|
||
return text;
|
||
};
|
||
|
||
|
||
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);
|
||
};
|
||
}
|
||
|
||
|
||
var DATE_FORMATS = {
|
||
yyyy: dateGetter('FullYear', 4),
|
||
yy: dateGetter('FullYear', 2, 0, 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),
|
||
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 = /([^yMdHhmsaZ]*)(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)
|
||
* * `'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)
|
||
* * `'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
|
||
<span ng:non-bindable>{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}</span>:
|
||
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}<br/>
|
||
<span ng:non-bindable>{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}}</span>:
|
||
{{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}<br/>
|
||
*
|
||
* @scenario
|
||
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 = [];
|
||
while(format) {
|
||
parts = concat(parts, DATE_FORMATS_SPLIT.exec(format), 1);
|
||
format = parts.pop();
|
||
}
|
||
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:
|
||
<input type="text" name="objTxt" value="{a:1, b:[]}"
|
||
ng:eval="obj = $eval(objTxt)"/>
|
||
<pre>{{ obj | json }}</pre>
|
||
*
|
||
* @scenario
|
||
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: <textarea name="snippet" cols="60" rows="3">
|
||
<p style="color:blue">an html
|
||
<em onmouseover="this.textContent='PWN3D!'">click here</em>
|
||
snippet</p></textarea>
|
||
<table>
|
||
<tr>
|
||
<td>Filter</td>
|
||
<td>Source</td>
|
||
<td>Rendered</td>
|
||
</tr>
|
||
<tr id="html-filter">
|
||
<td>html filter</td>
|
||
<td>
|
||
<pre><div ng:bind="snippet | html"><br/></div></pre>
|
||
</td>
|
||
<td>
|
||
<div ng:bind="snippet | html"></div>
|
||
</td>
|
||
</tr>
|
||
<tr id="escaped-html">
|
||
<td>no filter</td>
|
||
<td><pre><div ng:bind="snippet"><br/></div></pre></td>
|
||
<td><div ng:bind="snippet"></div></td>
|
||
</tr>
|
||
<tr id="html-unsafe-filter">
|
||
<td>unsafe html filter</td>
|
||
<td><pre><div ng:bind="snippet | html:'unsafe'"><br/></div></pre></td>
|
||
<td><div ng:bind="snippet | html:'unsafe'"></div></td>
|
||
</tr>
|
||
</table>
|
||
*
|
||
* @scenario
|
||
it('should sanitize the html snippet ', function(){
|
||
expect(using('#html-filter').binding('snippet | html')).
|
||
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
||
});
|
||
|
||
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("<p style=\"color:blue\">an html\n" +
|
||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||
"snippet</p>");
|
||
});
|
||
|
||
it('should update', function(){
|
||
input('snippet').enter('new <b>text</b>');
|
||
expect(using('#html-filter').binding('snippet | html')).toBe('new <b>text</b>');
|
||
expect(using('#escaped-html').binding('snippet')).toBe("new <b>text</b>");
|
||
expect(using('#html-unsafe-filter').binding("snippet | html:'unsafe'")).toBe('new <b>text</b>');
|
||
});
|
||
*/
|
||
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: <textarea name="snippet" cols="60" rows="3">
|
||
Pretty text with some links:
|
||
http://angularjs.org/,
|
||
mailto:us@somewhere.org,
|
||
another@somewhere.org,
|
||
and one more: ftp://127.0.0.1/.</textarea>
|
||
<table>
|
||
<tr>
|
||
<td>Filter</td>
|
||
<td>Source</td>
|
||
<td>Rendered</td>
|
||
</tr>
|
||
<tr id="linky-filter">
|
||
<td>linky filter</td>
|
||
<td>
|
||
<pre><div ng:bind="snippet | linky"><br/></div></pre>
|
||
</td>
|
||
<td>
|
||
<div ng:bind="snippet | linky"></div>
|
||
</td>
|
||
</tr>
|
||
<tr id="escaped-html">
|
||
<td>no filter</td>
|
||
<td><pre><div ng:bind="snippet"><br/></div></pre></td>
|
||
<td><div ng:bind="snippet"></div></td>
|
||
</tr>
|
||
</table>
|
||
|
||
@scenario
|
||
it('should linkify the snippet with urls', function(){
|
||
expect(using('#linky-filter').binding('snippet | linky')).
|
||
toBe('Pretty text with some links:\n' +
|
||
'<a href="http://angularjs.org/">http://angularjs.org/</a>,\n' +
|
||
'<a href="mailto:us@somewhere.org">us@somewhere.org</a>,\n' +
|
||
'<a href="mailto:another@somewhere.org">another@somewhere.org</a>,\n' +
|
||
'and one more: <a href="ftp://127.0.0.1/">ftp://127.0.0.1/</a>.');
|
||
});
|
||
|
||
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 <a href="http://link">http://link</a>.');
|
||
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(''));
|
||
};
|
||
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
|
||
* <div ng:init="data={name:'misko', project:'angular'}">
|
||
* <input type="text" size='50' name="data" ng:format="json"/>
|
||
* <pre>data={{data}}</pre>
|
||
* </div>
|
||
*
|
||
* @scenario
|
||
* 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, fromJson);
|
||
|
||
/**
|
||
* @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:
|
||
* <input type="text" name="value" ng:format="boolean" value="no"/>
|
||
* <input type="checkbox" name="value"/>
|
||
* <pre>value={{value}}</pre>
|
||
*
|
||
* @scenario
|
||
* 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:
|
||
* <input type="text" name="value" ng:format="number" value="1234"/>
|
||
* <pre>value={{value}}</pre>
|
||
*
|
||
* @scenario
|
||
* 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:
|
||
* <input type="text" name="value" ng:format="list" value=" chair ,, table"/>
|
||
* <input type="text" name="value" ng:format="list"/>
|
||
* <pre>value={{value}}</pre>
|
||
*
|
||
* @scenario
|
||
* 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 :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:
|
||
* <input type="text" name="value" ng:format="trim" value=" book "/>
|
||
* <input type="text" name="value" ng:format="trim"/>
|
||
* <pre>value={{value|json}}</pre>
|
||
*
|
||
* @scenario
|
||
* it('should format trim', function(){
|
||
* expect(binding('value')).toEqual('value="book"');
|
||
* this.addFutureAction('change to XYZ', function($window, $document, done){
|
||
* $document.elements('.doc-example :input:last').val(' text ').trigger('change');
|
||
* done();
|
||
* });
|
||
* expect(binding('value')).toEqual('value="text"');
|
||
* });
|
||
*/
|
||
angularFormatter.trim = formatter(
|
||
function(obj) { return obj ? trim("" + obj) : ""; }
|
||
);
|
||
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 {regexp} expression regular expression.
|
||
* @css ng-validation-error
|
||
*
|
||
* @example
|
||
* <script> var ssn = /^\d\d\d-\d\d-\d\d\d\d$/; </script>
|
||
* Enter valid SSN:
|
||
* <input name="ssn" value="123-45-6789" ng:validate="regexp:$window.ssn" >
|
||
*
|
||
* @scenario
|
||
* it('should invalidate non ssn', function(){
|
||
* var textBox = element('.doc-example :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: <input name="n1" ng:validate="number" > <br>
|
||
* Enter number greater than 10: <input name="n2" ng:validate="number:10" > <br>
|
||
* Enter number between 100 and 200: <input name="n3" ng:validate="number:100:200" > <br>
|
||
*
|
||
* @scenario
|
||
* it('should invalidate number', function(){
|
||
* var n1 = element('.doc-example :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 :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 :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: <input name="n1" ng:validate="integer" > <br>
|
||
* Enter integer equal or greater than 10: <input name="n2" ng:validate="integer:10" > <br>
|
||
* Enter integer between 100 and 200 (inclusive): <input name="n3" ng:validate="integer:100:200" > <br>
|
||
*
|
||
* @scenario
|
||
* it('should invalidate integer', function(){
|
||
* var n1 = element('.doc-example :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 :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 :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:
|
||
* <input name="text" value="1/1/2009" ng:validate="date" >
|
||
*
|
||
* @scenario
|
||
* it('should invalidate date', function(){
|
||
* var n1 = element('.doc-example :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:
|
||
* <input name="text" ng:validate="email" value="me@example.com">
|
||
*
|
||
* @scenario
|
||
* it('should invalidate email', function(){
|
||
* var n1 = element('.doc-example :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:
|
||
* <input name="text" value="1(234)567-8901" ng:validate="phone" >
|
||
*
|
||
* @scenario
|
||
* it('should invalidate phone', function(){
|
||
* var n1 = element('.doc-example :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 phone number:
|
||
* <input name="text" value="http://example.com/abc.html" size="40" ng:validate="url" >
|
||
*
|
||
* @scenario
|
||
* it('should invalidate url', function(){
|
||
* var n1 = element('.doc-example :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
|
||
* <textarea name="json" cols="60" rows="5" ng:validate="json">
|
||
* {name:'abc'}
|
||
* </textarea>
|
||
*
|
||
* @scenario
|
||
* it('should invalidate json', function(){
|
||
* var n1 = element('.doc-example :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 <angular/> 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
|
||
* <script>
|
||
* function myValidator(inputToValidate, validationDone) {
|
||
* setTimeout(function(){
|
||
* validationDone(inputToValidate.length % 2);
|
||
* }, 500);
|
||
* }
|
||
* </script>
|
||
* This input is validated asynchronously:
|
||
* <input name="text" ng:validate="asynchronous:$window.myValidator">
|
||
*
|
||
* @scenario
|
||
* it('should change color in delayed way', function(){
|
||
* var textBox = element('.doc-example :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/);
|
||
*
|
||
* pause(.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];
|
||
if (!inputState) {
|
||
cache.inputs[input] = inputState = { inFlight: true };
|
||
scope.$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');
|
||
scope.$invalidWidgets.markValid(element);
|
||
}
|
||
element.data($$validate)();
|
||
scope.$root.$eval();
|
||
});
|
||
} else if (inputState.inFlight) {
|
||
// request in flight, mark widget invalid, but don't show it to user
|
||
scope.$invalidWidgets.markInvalid(scope.$element);
|
||
} else {
|
||
(updateFn||noop)(inputState.response);
|
||
}
|
||
return inputState.error;
|
||
}
|
||
|
||
});
|
||
var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/,
|
||
HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/,
|
||
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21},
|
||
EAGER = 'eager',
|
||
EAGER_PUBLISHED = EAGER + '-published';
|
||
|
||
function angularServiceInject(name, fn, inject, eager) {
|
||
angularService(name, fn, {$inject:inject, $creation:eager});
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$window
|
||
*
|
||
* @description
|
||
* Is reference to the browser's <b>window</b> object. While <b>window</b>
|
||
* is globally available in JavaScript, it causes testability problems, because
|
||
* it is a global variable. In <b><angular/></b> we always refer to it through the
|
||
* $window service, so it may be overriden, removed or mocked for testing.
|
||
*
|
||
* All expressions are evaluated with respect to current scope so they don't
|
||
* suffer from window globality.
|
||
*
|
||
* @example
|
||
<input ng:init="greeting='Hello World!'" type="text" name="greeting" />
|
||
<button ng:click="$window.alert(greeting)">ALERT</button>
|
||
*/
|
||
angularServiceInject("$window", bind(window, identity, window), [], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$document
|
||
* @requires $window
|
||
*
|
||
* @description
|
||
* Reference to the browser window.document, but wrapped into angular.element().
|
||
*/
|
||
angularServiceInject("$document", function(window){
|
||
return jqLite(window.document);
|
||
}, ['$window'], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$location
|
||
* @requires $browser
|
||
*
|
||
* @property {string} href
|
||
* @property {string} protocol
|
||
* @property {string} host
|
||
* @property {number} port
|
||
* @property {string} path
|
||
* @property {Object.<string|boolean>} search
|
||
* @property {string} hash
|
||
* @property {string} hashPath
|
||
* @property {Object.<string|boolean>} hashSearch
|
||
*
|
||
* @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 to url.
|
||
* Notice that using browser's forward/back buttons changes the $location.
|
||
*
|
||
* @example
|
||
<a href="#">clear hash</a> |
|
||
<a href="#myPath?name=misko">test hash</a><br/>
|
||
<input type='text' name="$location.hash"/>
|
||
<pre>$location = {{$location}}</pre>
|
||
*/
|
||
angularServiceInject("$location", function(browser) {
|
||
var scope = this,
|
||
location = {toString:toString, update:update, updateHash: updateHash},
|
||
lastBrowserUrl = browser.getUrl(),
|
||
lastLocationHref,
|
||
lastLocationHash;
|
||
|
||
browser.addPollFn(function() {
|
||
if (lastBrowserUrl != browser.getUrl()) {
|
||
update(lastBrowserUrl = browser.getUrl());
|
||
updateLastLocation();
|
||
scope.$eval();
|
||
}
|
||
});
|
||
|
||
this.$onEval(PRIORITY_FIRST, updateBrowser);
|
||
this.$onEval(PRIORITY_LAST, updateBrowser);
|
||
|
||
update(lastBrowserUrl);
|
||
updateLastLocation();
|
||
|
||
return location;
|
||
|
||
// PUBLIC METHODS
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc method
|
||
* @name angular.service.$location#update
|
||
* @methodOf angular.service.$location
|
||
*
|
||
* @description
|
||
* Update location object
|
||
* Does not immediately update the browser
|
||
* Browser is updated at the end of $eval()
|
||
*
|
||
* @example
|
||
* scope.$location.update('http://www.angularjs.org/path#hash?search=x');
|
||
* scope.$location.update({host: 'www.google.com', protocol: 'https'});
|
||
* scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}});
|
||
*
|
||
* @param {(string|Object)} href Full href as a string or hash object with properties
|
||
*/
|
||
function update(href) {
|
||
if (isString(href)) {
|
||
extend(location, parseHref(href));
|
||
} else {
|
||
if (isDefined(href.hash)) {
|
||
extend(href, parseHash(href.hash));
|
||
}
|
||
|
||
extend(location, href);
|
||
|
||
if (isDefined(href.hashPath || href.hashSearch)) {
|
||
location.hash = composeHash(location);
|
||
}
|
||
|
||
location.href = composeHref(location);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc method
|
||
* @name angular.service.$location#updateHash
|
||
* @methodOf angular.service.$location
|
||
*
|
||
* @description
|
||
* Update location hash part
|
||
* @see update()
|
||
*
|
||
* @example
|
||
* scope.$location.updateHash('/hp')
|
||
* ==> update({hashPath: '/hp'})
|
||
*
|
||
* scope.$location.updateHash({a: true, b: 'val'})
|
||
* ==> update({hashSearch: {a: true, b: 'val'}})
|
||
*
|
||
* scope.$location.updateHash('/hp', {a: true})
|
||
* ==> update({hashPath: '/hp', hashSearch: {a: true}})
|
||
*
|
||
* @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;
|
||
if (isDefined(search))
|
||
hash.hashSearch = search;
|
||
} else
|
||
hash.hashSearch = path;
|
||
|
||
update(hash);
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc method
|
||
* @name angular.service.$location#toString
|
||
* @methodOf angular.service.$location
|
||
*
|
||
* @description
|
||
* Returns string representation - href
|
||
*/
|
||
function toString() {
|
||
updateLocation();
|
||
return location.href;
|
||
}
|
||
|
||
// INNER METHODS
|
||
|
||
/**
|
||
* Update location object
|
||
*
|
||
* User is allowed to change properties, so after property change,
|
||
* location object is not in consistent state.
|
||
*
|
||
* @example
|
||
* scope.$location.href = 'http://www.angularjs.org/path#a/b'
|
||
* immediately after this call, other properties are still the old ones...
|
||
*
|
||
* This method checks the changes and update location to the consistent state
|
||
*/
|
||
function updateLocation() {
|
||
if (location.href == lastLocationHref) {
|
||
if (location.hash == lastLocationHash) {
|
||
location.hash = composeHash(location);
|
||
}
|
||
location.href = composeHref(location);
|
||
}
|
||
update(location.href);
|
||
}
|
||
|
||
/**
|
||
* Update information about last location
|
||
*/
|
||
function updateLastLocation() {
|
||
lastLocationHref = location.href;
|
||
lastLocationHash = location.hash;
|
||
}
|
||
|
||
/**
|
||
* If location has changed, update the browser
|
||
* This method is called at the end of $eval() phase
|
||
*/
|
||
function updateBrowser() {
|
||
updateLocation();
|
||
|
||
if (location.href != lastLocationHref) {
|
||
browser.setUrl(lastBrowserUrl = location.href);
|
||
updateLastLocation();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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'], EAGER_PUBLISHED);
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$log
|
||
* @requires $window
|
||
*
|
||
* @description
|
||
* Is simple service for logging. Default implementation writes the message
|
||
* into the browser's console (if present).
|
||
*
|
||
* This is useful for debugging.
|
||
*
|
||
* @example
|
||
<p>Reload this page with open console, enter text and hit the log button...</p>
|
||
Message:
|
||
<input type="text" name="message" value="Hello World!"/>
|
||
<button ng:click="$log.log(message)">log</button>
|
||
<button ng:click="$log.warn(message)">warn</button>
|
||
<button ng:click="$log.info(message)">info</button>
|
||
<button ng:click="$log.error(message)">error</button>
|
||
*/
|
||
angularServiceInject("$log", 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'], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$exceptionHandler
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
* Any uncaught exception in <angular/> is delegated to this service.
|
||
* The default implementation simply delegates to $log.error which logs it into
|
||
* the browser console.
|
||
*
|
||
* When unit testing it is useful to have uncaught exceptions propagate
|
||
* to the test so the test will fail rather than silently log the exception
|
||
* to the browser console. For this purpose you can override this service with
|
||
* a simple rethrow.
|
||
*
|
||
* @example
|
||
*/
|
||
angularServiceInject('$exceptionHandler', function($log){
|
||
return function(e) {
|
||
$log.error(e);
|
||
};
|
||
}, ['$log'], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @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('<div id="ng-callout"></div>'),
|
||
arrow: jqLite('<div></div>'),
|
||
title: jqLite('<div class="ng-title"></div>'),
|
||
content: jqLite('<div class="ng-content"></div>')
|
||
};
|
||
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'], EAGER);
|
||
|
||
/**
|
||
* @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;
|
||
}, [], EAGER_PUBLISHED);
|
||
|
||
|
||
|
||
function switchRouteMatcher(on, when, dstName) {
|
||
var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$',
|
||
params = [],
|
||
dst = {};
|
||
foreach(when.split(/\W/), function(param){
|
||
if (param) {
|
||
var paramRegExp = new RegExp(":" + param + "([\\W])");
|
||
if (regex.match(paramRegExp)) {
|
||
regex = regex.replace(paramRegExp, "([^\/]*)$1");
|
||
params.push(param);
|
||
}
|
||
}
|
||
});
|
||
var match = on.match(new RegExp(regex));
|
||
if (match) {
|
||
foreach(params, function(name, index){
|
||
dst[name] = match[index + 1];
|
||
});
|
||
if (dstName) this.$set(dstName, dst);
|
||
}
|
||
return match ? dst : _null;
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$route
|
||
* @requires $location
|
||
*
|
||
* @property {Object} current Name of the current route
|
||
* @property {Array.<Object>} routes List of configured routes
|
||
*
|
||
* @description
|
||
* Watches $location.hashPath and tries to map the hash to an existing route
|
||
* definition. It is used for deep-linking URLs to controllers and views (HTML partials).
|
||
*
|
||
* $route is typically used in conjunction with ng:include widget.
|
||
*
|
||
* @example
|
||
<p>
|
||
This example shows how changing the URL hash causes the <tt>$route</tt>
|
||
to match a route against the URL, and the <tt>[[ng:include]]</tt> pulls in the partial.
|
||
Try changing the URL in the input box to see changes.
|
||
</p>
|
||
|
||
<script>
|
||
angular.service('myApp', function($route) {
|
||
$route.when('/Book/:bookId', {template:'rsrc/book.html', controller:BookCntl});
|
||
$route.when('/Book/:bookId/ch/:chapterId', {template:'rsrc/chapter.html', controller:ChapterCntl});
|
||
$route.onChange(function() {
|
||
$route.current.scope.params = $route.current.params;
|
||
});
|
||
}, {$inject: ['$route']});
|
||
|
||
function BookCntl() {
|
||
this.name = "BookCntl";
|
||
}
|
||
|
||
function ChapterCntl() {
|
||
this.name = "ChapterCntl";
|
||
}
|
||
</script>
|
||
|
||
Chose:
|
||
<a href="#/Book/Moby">Moby</a> |
|
||
<a href="#/Book/Moby/ch/1">Moby: Ch1</a> |
|
||
<a href="#/Book/Gatsby">Gatsby</a> |
|
||
<a href="#/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a><br/>
|
||
<input type="text" name="$location.hashPath" size="80" />
|
||
<pre>$location={{$location}}</pre>
|
||
<pre>$route.current.template={{$route.current.template}}</pre>
|
||
<pre>$route.current.params={{$route.current.params}}</pre>
|
||
<pre>$route.current.scope.name={{$route.current.scope.name}}</pre>
|
||
<hr/>
|
||
<ng:include src="$route.current.template" scope="$route.current.scope"/>
|
||
*/
|
||
angularServiceInject('$route', function(location) {
|
||
var routes = {},
|
||
onChange = [],
|
||
matcher = switchRouteMatcher,
|
||
parentScope = this,
|
||
dirty = 0,
|
||
$route = {
|
||
routes: routes,
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc method
|
||
* @name angular.service.$route#onChange
|
||
* @methodOf angular.service.$route
|
||
*
|
||
* @param {function()} fn Function that will be called on route change
|
||
*
|
||
* @description
|
||
* Register a handler function that will be called when route changes
|
||
*/
|
||
onChange: bind(onChange, onChange.push),
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc method
|
||
* @name angular.service.$route#when
|
||
* @methodOf angular.service.$route
|
||
*
|
||
* @param {string} path Route path (matched against $location.hash)
|
||
* @param {Object} params Mapping information to be assigned to `$route.current` on route
|
||
* match.
|
||
* @returns {Object} route object
|
||
*
|
||
* @description
|
||
* Add new route
|
||
*/
|
||
when:function (path, params) {
|
||
if (angular.isUndefined(path)) return routes;
|
||
var route = routes[path];
|
||
if (!route) route = routes[path] = {};
|
||
if (params) angular.extend(route, params);
|
||
dirty++;
|
||
return route;
|
||
}
|
||
};
|
||
function updateRoute(){
|
||
var childScope;
|
||
$route.current = _null;
|
||
angular.foreach(routes, function(routeParams, route) {
|
||
if (!childScope) {
|
||
var pathParams = matcher(location.hashPath, route);
|
||
if (pathParams) {
|
||
childScope = angular.scope(parentScope);
|
||
$route.current = angular.extend({}, routeParams, {
|
||
scope: childScope,
|
||
params: angular.extend({}, location.hashSearch, pathParams)
|
||
});
|
||
}
|
||
}
|
||
});
|
||
angular.foreach(onChange, parentScope.$tryEval);
|
||
if (childScope) {
|
||
childScope.$become($route.current.controller);
|
||
}
|
||
}
|
||
this.$watch(function(){return dirty + location.hash;}, updateRoute);
|
||
return $route;
|
||
}, ['$location'], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$xhr
|
||
* @requires $browser
|
||
* @requires $xhr.error
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
*
|
||
* @example
|
||
*/
|
||
angularServiceInject('$xhr', function($browser, $error, $log){
|
||
var self = this;
|
||
return function(method, url, post, callback){
|
||
if (isFunction(post)) {
|
||
callback = post;
|
||
post = _null;
|
||
}
|
||
if (post && isObject(post)) {
|
||
post = toJson(post);
|
||
}
|
||
$browser.xhr(method, url, post, function(code, response){
|
||
try {
|
||
if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) {
|
||
response = fromJson(response, true);
|
||
}
|
||
if (code == 200) {
|
||
callback(code, response);
|
||
} else {
|
||
$error(
|
||
{method: method, url:url, data:post, callback:callback},
|
||
{status: code, body:response});
|
||
}
|
||
} catch (e) {
|
||
$log.error(e);
|
||
} finally {
|
||
self.$eval();
|
||
}
|
||
});
|
||
};
|
||
}, ['$browser', '$xhr.error', '$log']);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$xhr.error
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
*
|
||
* @example
|
||
*/
|
||
angularServiceInject('$xhr.error', function($log){
|
||
return function(request, response){
|
||
$log.error('ERROR: XHR: ' + request.url, request, response);
|
||
};
|
||
}, ['$log']);
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$xhr.bulk
|
||
* @requires $xhr
|
||
* @requires $xhr.error
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
*
|
||
* @example
|
||
*/
|
||
angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
|
||
var requests = [],
|
||
scope = this;
|
||
function bulkXHR(method, url, post, callback) {
|
||
if (isFunction(post)) {
|
||
callback = post;
|
||
post = _null;
|
||
}
|
||
var currentQueue;
|
||
foreach(bulkXHR.urls, function(queue){
|
||
if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) {
|
||
currentQueue = queue;
|
||
}
|
||
});
|
||
if (currentQueue) {
|
||
if (!currentQueue.requests) currentQueue.requests = [];
|
||
currentQueue.requests.push({method: method, url: url, data:post, callback:callback});
|
||
} else {
|
||
$xhr(method, url, post, callback);
|
||
}
|
||
}
|
||
bulkXHR.urls = {};
|
||
bulkXHR.flush = function(callback){
|
||
foreach(bulkXHR.urls, function(queue, url){
|
||
var currentRequests = queue.requests;
|
||
if (currentRequests && currentRequests.length) {
|
||
queue.requests = [];
|
||
queue.callbacks = [];
|
||
$xhr('POST', url, {requests:currentRequests}, function(code, response){
|
||
foreach(response, function(response, i){
|
||
try {
|
||
if (response.status == 200) {
|
||
(currentRequests[i].callback || noop)(response.status, response.response);
|
||
} else {
|
||
$error(currentRequests[i], response);
|
||
}
|
||
} catch(e) {
|
||
$log.error(e);
|
||
}
|
||
});
|
||
(callback || noop)();
|
||
});
|
||
scope.$eval();
|
||
}
|
||
});
|
||
};
|
||
this.$onEval(PRIORITY_LAST, bulkXHR.flush);
|
||
return bulkXHR;
|
||
}, ['$xhr', '$xhr.error', '$log']);
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$defer
|
||
* @requires $browser
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
* Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function
|
||
* into a try/catch block and delegates any exceptions to
|
||
* {@link angular.services.$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.
|
||
*/
|
||
angularServiceInject('$defer', function($browser, $exceptionHandler) {
|
||
var scope = this;
|
||
|
||
return function(fn) {
|
||
$browser.defer(function() {
|
||
try {
|
||
fn();
|
||
} catch(e) {
|
||
$exceptionHandler(e);
|
||
} finally {
|
||
scope.$eval();
|
||
}
|
||
});
|
||
};
|
||
}, ['$browser', '$exceptionHandler']);
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$xhr.cache
|
||
* @requires $xhr
|
||
*
|
||
* @description
|
||
*
|
||
* @example
|
||
*/
|
||
angularServiceInject('$xhr.cache', function($xhr, $defer){
|
||
var inflight = {}, self = this;
|
||
function cache(method, url, post, callback, verifyCache){
|
||
if (isFunction(post)) {
|
||
callback = post;
|
||
post = _null;
|
||
}
|
||
if (method == 'GET') {
|
||
var data, dataCached;
|
||
if (dataCached = cache.data[url]) {
|
||
$defer(function() { callback(200, copy(dataCached.value)); });
|
||
if (!verifyCache)
|
||
return;
|
||
}
|
||
|
||
if (data = inflight[url]) {
|
||
data.callbacks.push(callback);
|
||
} else {
|
||
inflight[url] = {callbacks: [callback]};
|
||
cache.delegate(method, url, post, function(status, response){
|
||
if (status == 200)
|
||
cache.data[url] = { value: response };
|
||
var callbacks = inflight[url].callbacks;
|
||
delete inflight[url];
|
||
foreach(callbacks, function(callback){
|
||
try {
|
||
(callback||noop)(status, copy(response));
|
||
} catch(e) {
|
||
self.$log.error(e);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
} else {
|
||
cache.data = {};
|
||
cache.delegate(method, url, post, callback);
|
||
}
|
||
}
|
||
cache.data = {};
|
||
cache.delegate = $xhr;
|
||
return cache;
|
||
}, ['$xhr.bulk', '$defer']);
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc function
|
||
* @name angular.service.$resource
|
||
* @requires $xhr
|
||
*
|
||
* @description
|
||
* Is a factory which creates a resource object which lets you interact with
|
||
* <a href="http://en.wikipedia.org/wiki/Representational_State_Transfer" target="_blank">RESTful</a>
|
||
* server-side data sources.
|
||
* Resource object has action methods which provide high-level behaviors without
|
||
* the need to interact with the low level $xhr or XMLHttpRequest().
|
||
*
|
||
* <pre>
|
||
// Define CreditCard class
|
||
var CreditCard = $resource('/user/:userId/card/:cardId',
|
||
{userId:123, cardId:'@id'}, {
|
||
charge: {method:'POST', params:{charge:true}}
|
||
});
|
||
|
||
// We can retrieve a collection from the server
|
||
var cards = CreditCard.query();
|
||
// GET: /user/123/card
|
||
// server returns: [ {id:456, number:'1234', name:'Smith'} ];
|
||
|
||
var card = cards[0];
|
||
// each item is an instance of CreditCard
|
||
expect(card instanceof CreditCard).toEqual(true);
|
||
card.name = "J. Smith";
|
||
// non GET methods are mapped onto the instances
|
||
card.$save();
|
||
// POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
|
||
// server returns: {id:456, number:'1234', name: 'J. Smith'};
|
||
|
||
// our custom method is mapped as well.
|
||
card.$charge({amount:9.99});
|
||
// POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
|
||
// server returns: {id:456, number:'1234', name: 'J. Smith'};
|
||
|
||
// we can create an instance as well
|
||
var newCard = new CreditCard({number:'0123'});
|
||
newCard.name = "Mike Smith";
|
||
newCard.$save();
|
||
// POST: /user/123/card {number:'0123', name:'Mike Smith'}
|
||
// server returns: {id:789, number:'01234', name: 'Mike Smith'};
|
||
expect(newCard.id).toEqual(789);
|
||
* </pre>
|
||
*
|
||
* The object returned from this function execution is a resource "class" which has "static" method
|
||
* for each action in the definition.
|
||
*
|
||
* Calling these methods invoke `$xhr` on the `url` template with the given `method` and `params`.
|
||
* When the data is returned from the server then the object is an instance of the resource type and
|
||
* all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
|
||
* operations (create, read, update, delete) on server-side data.
|
||
|
||
<pre>
|
||
var User = $resource('/user/:userId', {userId:'@id'});
|
||
var user = User.get({userId:123}, function(){
|
||
user.abc = true;
|
||
user.$save();
|
||
});
|
||
</pre>
|
||
*
|
||
* It's worth noting that the callback for `get`, `query` and other method gets passed in the
|
||
* response that came from the server, so one could rewrite the above example as:
|
||
*
|
||
<pre>
|
||
var User = $resource('/user/:userId', {userId:'@id'});
|
||
User.get({userId:123}, function(u){
|
||
u.abc = true;
|
||
u.$save();
|
||
});
|
||
</pre>
|
||
*
|
||
*
|
||
* @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.
|
||
* @param {Object.<Object>=} actions Map of actions available for the resource.
|
||
*
|
||
* Each resource comes preconfigured with `get`, `save`, `query`, `remove`, and `delete` to
|
||
* mimic the RESTful philosophy.
|
||
*
|
||
* To create your own actions, pass in a map keyed on action names (e.g. `'charge'`) with
|
||
* elements consisting of these properties:
|
||
*
|
||
* - `{string} method`: Request method type. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
|
||
* and [`JSON`](http://en.wikipedia.org/wiki/JSON#JSONP) (also known as JSONP).
|
||
* - `{Object=} params`: Set of pre-bound parameters for the action.
|
||
* - `{boolean=} isArray`: If true then the returned object for this action is an array, see the
|
||
* pre-binding section.
|
||
* - `{boolean=} verifyCache`: If true then items returned from cache, are double checked by
|
||
* running the query again and updating the resource asynchroniously.
|
||
*
|
||
* Each service comes preconfigured with the following overridable actions:
|
||
* <pre>
|
||
* { 'get': {method:'GET'},
|
||
'save': {method:'POST'},
|
||
'query': {method:'GET', isArray:true},
|
||
'remove': {method:'DELETE'},
|
||
'delete': {method:'DELETE'} };
|
||
* </pre>
|
||
*
|
||
* @returns {Object} A resource "class".
|
||
*
|
||
* @example
|
||
<script>
|
||
function BuzzController($resource) {
|
||
this.Activity = $resource(
|
||
'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
|
||
{alt:'json', callback:'JSON_CALLBACK'},
|
||
{get:{method:'JSON', params:{visibility:'@self'}}, replies: {method:'JSON', params:{visibility:'@self', comments:'@comments'}}}
|
||
);
|
||
}
|
||
|
||
BuzzController.prototype = {
|
||
fetch: function() {
|
||
this.activities = this.Activity.get({userId:this.userId});
|
||
},
|
||
expandReplies: function(activity) {
|
||
activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
|
||
}
|
||
};
|
||
BuzzController.$inject = ['$resource'];
|
||
</script>
|
||
|
||
<div ng:controller="BuzzController">
|
||
<input name="userId" value="googlebuzz"/>
|
||
<button ng:click="fetch()">fetch</button>
|
||
<hr/>
|
||
<div ng:repeat="item in activities.data.items">
|
||
<h1 style="font-size: 15px;">
|
||
<img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
|
||
<a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
|
||
<a href ng:click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
|
||
</h1>
|
||
{{item.object.content | html}}
|
||
<div ng:repeat="reply in item.replies.data.items" style="margin-left: 20px;">
|
||
<img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
|
||
<a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
*/
|
||
angularServiceInject('$resource', function($xhr){
|
||
var resource = new ResourceFactory($xhr);
|
||
return bind(resource, resource.route);
|
||
}, ['$xhr.cache']);
|
||
|
||
/**
|
||
* @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;
|
||
|
||
//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);
|
||
rootScope.$eval();
|
||
}
|
||
})();
|
||
|
||
//at the end of each eval, push cookies
|
||
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,
|
||
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) {
|
||
if (cookies[name] !== lastCookies[name]) {
|
||
$browser.cookies(name, cookies[name]);
|
||
updated = true;
|
||
}
|
||
}
|
||
|
||
//verify what was actually stored
|
||
if (updated){
|
||
updated = !updated;
|
||
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;
|
||
}
|
||
|
||
}
|
||
|
||
if (updated) {
|
||
rootScope.$eval();
|
||
}
|
||
}
|
||
}
|
||
}, ['$browser'], EAGER_PUBLISHED);
|
||
|
||
/**
|
||
* @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 directive
|
||
* @name angular.directive.ng:init
|
||
*
|
||
* @description
|
||
* `ng:init` attribute allows the for initialization tasks to be executed
|
||
* before the template enters execution mode during bootstrap.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @example
|
||
<div ng:init="greeting='Hello'; person='World'">
|
||
{{greeting}} {{person}}!
|
||
</div>
|
||
*
|
||
* @scenario
|
||
it('should check greeting', function(){
|
||
expect(binding('greeting')).toBe('Hello');
|
||
expect(binding('person')).toBe('World');
|
||
});
|
||
*/
|
||
angularDirective("ng:init", function(expression){
|
||
return function(element){
|
||
this.$tryEval(expression, element);
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:controller
|
||
*
|
||
* @description
|
||
* To support the Model-View-Controller design pattern, it is possible
|
||
* to assign behavior to a scope through `ng:controller`. The scope is
|
||
* the MVC model. The HTML (with data bindings) is the MVC view.
|
||
* The `ng:controller` directive specifies the MVC controller class
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @example
|
||
<script type="text/javascript">
|
||
function SettingsController() {
|
||
this.name = "John Smith";
|
||
this.contacts = [
|
||
{type:'phone', value:'408 555 1212'},
|
||
{type:'email', value:'john.smith@example.org'} ];
|
||
}
|
||
SettingsController.prototype = {
|
||
greet: function(){
|
||
alert(this.name);
|
||
},
|
||
addContact: function(){
|
||
this.contacts.push({type:'email', value:'yourname@example.org'});
|
||
},
|
||
removeContact: function(contactToRemove) {
|
||
angular.Array.remove(this.contacts, contactToRemove);
|
||
},
|
||
clearContact: function(contact) {
|
||
contact.type = 'phone';
|
||
contact.value = '';
|
||
}
|
||
};
|
||
</script>
|
||
<div ng:controller="SettingsController">
|
||
Name: <input type="text" name="name"/>
|
||
[ <a href="" ng:click="greet()">greet</a> ]<br/>
|
||
Contact:
|
||
<ul>
|
||
<li ng:repeat="contact in contacts">
|
||
<select name="contact.type">
|
||
<option>phone</option>
|
||
<option>email</option>
|
||
</select>
|
||
<input type="text" name="contact.value"/>
|
||
[ <a href="" ng:click="clearContact(contact)">clear</a>
|
||
| <a href="" ng:click="removeContact(contact)">X</a> ]
|
||
</li>
|
||
<li>[ <a href="" ng:click="addContact()">add</a> ]</li>
|
||
</ul>
|
||
</div>
|
||
*
|
||
* @scenario
|
||
it('should check controller', function(){
|
||
expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
|
||
expect(element('.doc-example-live li[ng\\:repeat-index="0"] input').val()).toBe('408 555 1212');
|
||
expect(element('.doc-example-live li[ng\\:repeat-index="1"] input').val()).toBe('john.smith@example.org');
|
||
element('.doc-example-live li:first a:contains("clear")').click();
|
||
expect(element('.doc-example-live li:first input').val()).toBe('');
|
||
element('.doc-example-live li:last a:contains("add")').click();
|
||
expect(element('.doc-example-live li[ng\\:repeat-index="2"] input').val()).toBe('yourname@example.org');
|
||
});
|
||
*/
|
||
angularDirective("ng:controller", function(expression){
|
||
this.scope(true);
|
||
return function(element){
|
||
var controller = getter(window, expression, true) || getter(this, expression, true);
|
||
if (!controller)
|
||
throw "Can not find '"+expression+"' controller.";
|
||
if (!isFunction(controller))
|
||
throw "Reference '"+expression+"' is not a class.";
|
||
this.$become(controller);
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:eval
|
||
*
|
||
* @description
|
||
* The `ng:eval` allows you to execute a binding which has side effects
|
||
* without displaying the result to the user.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @exampleDescription
|
||
* Notice that `{{` `obj.multiplied = obj.a * obj.b` `}}` has a side effect of assigning
|
||
* a value to `obj.multiplied` and displaying the result to the user. Sometimes,
|
||
* however, it is desirable to execute a side effect without showing the value to
|
||
* the user. In such a case `ng:eval` allows you to execute code without updating
|
||
* the display.
|
||
*
|
||
* @example
|
||
* <input name="obj.a" value="6" >
|
||
* * <input name="obj.b" value="2">
|
||
* = {{obj.multiplied = obj.a * obj.b}} <br>
|
||
* <span ng:eval="obj.divide = obj.a / obj.b"></span>
|
||
* <span ng:eval="obj.updateCount = 1 + (obj.updateCount||0)"></span>
|
||
* <tt>obj.divide = {{obj.divide}}</tt><br/>
|
||
* <tt>obj.updateCount = {{obj.updateCount}}</tt>
|
||
*
|
||
* @scenario
|
||
it('should check eval', function(){
|
||
expect(binding('obj.divide')).toBe('3');
|
||
expect(binding('obj.updateCount')).toBe('2');
|
||
input('obj.a').enter('12');
|
||
expect(binding('obj.divide')).toBe('6');
|
||
expect(binding('obj.updateCount')).toBe('3');
|
||
});
|
||
*/
|
||
angularDirective("ng:eval", function(expression){
|
||
return function(element){
|
||
this.$onEval(expression, element);
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:bind
|
||
*
|
||
* @description
|
||
* The `ng:bind` attribute asks <angular/> to replace the text content of this
|
||
* HTML element with the value of the given expression and kept it up to
|
||
* date when the expression's value changes. Usually you just write
|
||
* {{expression}} and let <angular/> compile it into
|
||
* `<span ng:bind="expression"></span>` at bootstrap time.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @exampleDescription
|
||
* Try it here: enter text in text box and watch the greeting change.
|
||
* @example
|
||
* Enter name: <input type="text" name="name" value="Whirled">. <br>
|
||
* Hello <span ng:bind="name" />!
|
||
*
|
||
* @scenario
|
||
it('should check ng:bind', function(){
|
||
expect(using('.doc-example-live').binding('name')).toBe('Whirled');
|
||
using('.doc-example-live').input('name').enter('world');
|
||
expect(using('.doc-example-live').binding('name')).toBe('world');
|
||
});
|
||
*/
|
||
angularDirective("ng:bind", function(expression, element){
|
||
element.addClass('ng-binding');
|
||
return function(element) {
|
||
var lastValue = noop, lastError = noop;
|
||
this.$onEval(function() {
|
||
var error, value, html, isHtml, isDomElement,
|
||
oldElement = this.hasOwnProperty($$element) ? this.$element : _undefined;
|
||
this.$element = element;
|
||
value = this.$tryEval(expression, function(e){
|
||
error = formatError(e);
|
||
});
|
||
this.$element = oldElement;
|
||
// If we are HTML than save the raw HTML data so that we don't
|
||
// recompute sanitization since it is expensive.
|
||
// TODO: turn this into a more generic way to compute this
|
||
if (isHtml = (value instanceof HTML))
|
||
value = (html = value).html;
|
||
if (lastValue === value && lastError == error) return;
|
||
isDomElement = isElement(value);
|
||
if (!isHtml && !isDomElement && isObject(value)) {
|
||
value = toJson(value);
|
||
}
|
||
if (value != lastValue || error != lastError) {
|
||
lastValue = value;
|
||
lastError = error;
|
||
elementError(element, NG_EXCEPTION, error);
|
||
if (error) value = error;
|
||
if (isHtml) {
|
||
element.html(html.get());
|
||
} else if (isDomElement) {
|
||
element.html('');
|
||
element.append(value);
|
||
} else {
|
||
element.text(value === _undefined ? '' : value);
|
||
}
|
||
}
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
var bindTemplateCache = {};
|
||
function compileBindTemplate(template){
|
||
var fn = bindTemplateCache[template];
|
||
if (!fn) {
|
||
var bindings = [];
|
||
foreach(parseBindings(template), function(text){
|
||
var exp = binding(text);
|
||
bindings.push(exp ? function(element){
|
||
var error, value = this.$tryEval(exp, function(e){
|
||
error = toJson(e);
|
||
});
|
||
elementError(element, NG_EXCEPTION, error);
|
||
return error ? error : value;
|
||
} : function() {
|
||
return text;
|
||
});
|
||
});
|
||
bindTemplateCache[template] = fn = function(element){
|
||
var parts = [], self = this,
|
||
oldElement = this.hasOwnProperty($$element) ? self.$element : _undefined;
|
||
self.$element = element;
|
||
for ( var i = 0; i < bindings.length; i++) {
|
||
var value = bindings[i].call(self, element);
|
||
if (isElement(value))
|
||
value = '';
|
||
else if (isObject(value))
|
||
value = toJson(value, true);
|
||
parts.push(value);
|
||
}
|
||
self.$element = oldElement;
|
||
return parts.join('');
|
||
};
|
||
}
|
||
return fn;
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:bind-template
|
||
*
|
||
* @description
|
||
* The `ng:bind-template` attribute specifies that the element
|
||
* text should be replaced with the template in ng:bind-template.
|
||
* Unlike ng:bind the ng:bind-template can contain multiple `{{` `}}`
|
||
* expressions. (This is required since some HTML elements
|
||
* can not have SPAN elements such as TITLE, or OPTION to name a few.
|
||
*
|
||
* @element ANY
|
||
* @param {string} template of form
|
||
* <tt>{{</tt> <tt>expression</tt> <tt>}}</tt> to eval.
|
||
*
|
||
* @exampleDescription
|
||
* Try it here: enter text in text box and watch the greeting change.
|
||
* @example
|
||
Salutation: <input type="text" name="salutation" value="Hello"><br/>
|
||
Name: <input type="text" name="name" value="World"><br/>
|
||
<pre ng:bind-template="{{salutation}} {{name}}!"></pre>
|
||
*
|
||
* @scenario
|
||
it('should check ng:bind', function(){
|
||
expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
|
||
toBe('Hello World!');
|
||
using('.doc-example-live').input('salutation').enter('Greetings');
|
||
using('.doc-example-live').input('name').enter('user');
|
||
expect(using('.doc-example-live').binding('{{salutation}} {{name}}')).
|
||
toBe('Greetings user!');
|
||
});
|
||
*/
|
||
angularDirective("ng:bind-template", function(expression, element){
|
||
element.addClass('ng-binding');
|
||
var templateFn = compileBindTemplate(expression);
|
||
return function(element) {
|
||
var lastValue;
|
||
this.$onEval(function() {
|
||
var value = templateFn.call(this, element);
|
||
if (value != lastValue) {
|
||
element.text(value);
|
||
lastValue = value;
|
||
}
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
var REMOVE_ATTRIBUTES = {
|
||
'disabled':'disabled',
|
||
'readonly':'readOnly',
|
||
'checked':'checked',
|
||
'selected':'selected'
|
||
};
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:bind-attr
|
||
*
|
||
* @description
|
||
* The `ng:bind-attr` attribute specifies that the element attributes
|
||
* which should be replaced by the expression in it. Unlike `ng:bind`
|
||
* the `ng:bind-attr` contains a JSON key value pairs representing
|
||
* which attributes need to be changed. You don’t usually write the
|
||
* `ng:bind-attr` in the HTML since embedding
|
||
* <tt ng:non-bindable>{{expression}}</tt> into the
|
||
* attribute directly is the preferred way. The attributes get
|
||
* translated into `<span ng:bind-attr="{attr:expression}"/>` at
|
||
* bootstrap time.
|
||
*
|
||
* This HTML snippet is preferred way of working with `ng:bind-attr`
|
||
* <pre>
|
||
* <a href="http://www.google.com/search?q={{query}}">Google</a>
|
||
* </pre>
|
||
*
|
||
* The above gets translated to bellow during bootstrap time.
|
||
* <pre>
|
||
* <a ng:bind-attr='{"href":"http://www.google.com/search?q={{query}}"}'>Google</a>
|
||
* </pre>
|
||
*
|
||
* @element ANY
|
||
* @param {string} attribute_json a JSON key-value pairs representing
|
||
* the attributes to replace. Each key matches the attribute
|
||
* which needs to be replaced. Each value is a text template of
|
||
* the attribute with embedded
|
||
* <tt ng:non-bindable>{{expression}}</tt>s. Any number of
|
||
* key-value pairs can be specified.
|
||
*
|
||
* @exampleDescription
|
||
* Try it here: enter text in text box and click Google.
|
||
* @example
|
||
Google for:
|
||
<input type="text" name="query" value="AngularJS"/>
|
||
<a href="http://www.google.com/search?q={{query}}">Google</a>
|
||
*
|
||
* @scenario
|
||
it('should check ng:bind-attr', function(){
|
||
expect(using('.doc-example-live').element('a').attr('href')).
|
||
toBe('http://www.google.com/search?q=AngularJS');
|
||
using('.doc-example-live').input('query').enter('google');
|
||
expect(using('.doc-example-live').element('a').attr('href')).
|
||
toBe('http://www.google.com/search?q=google');
|
||
});
|
||
*/
|
||
angularDirective("ng:bind-attr", function(expression){
|
||
return function(element){
|
||
var lastValue = {};
|
||
var updateFn = element.data($$update) || noop;
|
||
this.$onEval(function(){
|
||
var values = this.$eval(expression),
|
||
dirty = noop;
|
||
for(var key in values) {
|
||
var value = compileBindTemplate(values[key]).call(this, element),
|
||
specialName = REMOVE_ATTRIBUTES[lowercase(key)];
|
||
if (lastValue[key] !== value) {
|
||
lastValue[key] = value;
|
||
if (specialName) {
|
||
if (toBoolean(value)) {
|
||
element.attr(specialName, specialName);
|
||
element.attr('ng-' + specialName, value);
|
||
} else {
|
||
element.removeAttr(specialName);
|
||
element.removeAttr('ng-' + specialName);
|
||
}
|
||
(element.data($$validate)||noop)();
|
||
} else {
|
||
element.attr(key, value);
|
||
}
|
||
dirty = updateFn;
|
||
}
|
||
}
|
||
dirty();
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:click
|
||
*
|
||
* @description
|
||
* The ng:click allows you to specify custom behavior when
|
||
* element is clicked.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval upon click.
|
||
*
|
||
* @example
|
||
<button ng:click="count = count + 1" ng:init="count=0">
|
||
Increment
|
||
</button>
|
||
count: {{count}}
|
||
* @scenario
|
||
it('should check ng:click', function(){
|
||
expect(binding('count')).toBe('0');
|
||
element('.doc-example-live :button').click();
|
||
expect(binding('count')).toBe('1');
|
||
});
|
||
*/
|
||
/*
|
||
* A directive that allows creation of custom onclick handlers that are defined as angular
|
||
* expressions and are compiled and executed within the current scope.
|
||
*
|
||
* Events that are handled via these handler are always configured not to propagate further.
|
||
*
|
||
* TODO: maybe we should consider allowing users to control event propagation in the future.
|
||
*/
|
||
angularDirective("ng:click", function(expression, element){
|
||
return function(element){
|
||
var self = this;
|
||
element.bind('click', function(event){
|
||
self.$tryEval(expression, element);
|
||
self.$root.$eval();
|
||
event.stopPropagation();
|
||
});
|
||
};
|
||
});
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:submit
|
||
*
|
||
* @description
|
||
*
|
||
* @element form
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
* <form ng:submit="list.push(text);text='';" ng:init="list=[]">
|
||
* Enter text and hit enter:
|
||
* <input type="text" name="text" value="hello"/>
|
||
* </form>
|
||
* <pre>list={{list}}</pre>
|
||
* @scenario
|
||
it('should check ng:submit', function(){
|
||
expect(binding('list')).toBe('list=[]');
|
||
element('.doc-example-live form input').click();
|
||
this.addFutureAction('submit from', function($window, $document, done) {
|
||
$window.angular.element(
|
||
$document.elements('.doc-example-live form')).
|
||
trigger('submit');
|
||
done();
|
||
});
|
||
expect(binding('list')).toBe('list=["hello"]');
|
||
});
|
||
*/
|
||
/**
|
||
* Enables binding angular expressions to onsubmit events.
|
||
*
|
||
* Additionally it prevents the default action (which for form means sending the request to the
|
||
* server and reloading the current page).
|
||
*/
|
||
angularDirective("ng:submit", function(expression, element) {
|
||
return function(element) {
|
||
var self = this;
|
||
element.bind('submit', function(event) {
|
||
self.$tryEval(expression, element);
|
||
self.$root.$eval();
|
||
event.preventDefault();
|
||
});
|
||
};
|
||
});
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:watch
|
||
*
|
||
* @description
|
||
* The `ng:watch` allows you watch a variable and then execute
|
||
* an evaluation on variable change.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @exampleDescription
|
||
* Notice that the counter is incremented
|
||
* every time you change the text.
|
||
* @example
|
||
<div ng:init="counter=0" ng:watch="name: counter = counter+1">
|
||
<input type="text" name="name" value="hello"><br/>
|
||
Change counter: {{counter}} Name: {{name}}
|
||
</div>
|
||
* @scenario
|
||
it('should check ng:watch', function(){
|
||
expect(using('.doc-example-live').binding('counter')).toBe('2');
|
||
using('.doc-example-live').input('name').enter('abc');
|
||
expect(using('.doc-example-live').binding('counter')).toBe('3');
|
||
});
|
||
*/
|
||
//TODO: delete me, since having watch in UI is logic in UI. (leftover form getangular)
|
||
angularDirective("ng:watch", function(expression, element){
|
||
return function(element){
|
||
var self = this;
|
||
parser(expression).watch()({
|
||
addListener:function(watch, exp){
|
||
self.$watch(watch, function(){
|
||
return exp(self);
|
||
}, element);
|
||
}
|
||
});
|
||
};
|
||
});
|
||
|
||
function ngClass(selector) {
|
||
return function(expression, element){
|
||
var existing = element[0].className + ' ';
|
||
return function(element){
|
||
this.$onEval(function(){
|
||
if (selector(this.$index)) {
|
||
var value = this.$eval(expression);
|
||
if (isArray(value)) value = value.join(' ');
|
||
element[0].className = trim(existing + value);
|
||
}
|
||
}, element);
|
||
};
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:class
|
||
*
|
||
* @description
|
||
* The `ng:class` allows you to set CSS class on HTML element
|
||
* conditionally.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
<input type="button" value="set" ng:click="myVar='ng-input-indicator-wait'">
|
||
<input type="button" value="clear" ng:click="myVar=''">
|
||
<br>
|
||
<span ng:class="myVar">Sample Text </span>
|
||
*
|
||
* @scenario
|
||
it('should check ng:class', function(){
|
||
expect(element('.doc-example-live span').attr('className')).not().
|
||
toMatch(/ng-input-indicator-wait/);
|
||
|
||
using('.doc-example-live').element(':button:first').click();
|
||
|
||
expect(element('.doc-example-live span').attr('className')).
|
||
toMatch(/ng-input-indicator-wait/);
|
||
|
||
using('.doc-example-live').element(':button:last').click();
|
||
|
||
expect(element('.doc-example-live span').attr('className')).not().
|
||
toMatch(/ng-input-indicator-wait/);
|
||
});
|
||
*/
|
||
angularDirective("ng:class", ngClass(function(){return true;}));
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:class-odd
|
||
*
|
||
* @description
|
||
* The `ng:class-odd` and `ng:class-even` works exactly as
|
||
* `ng:class`, except it works in conjunction with `ng:repeat`
|
||
* and takes affect only on odd (even) rows.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval. Must be inside
|
||
* `ng:repeat`.
|
||
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
<ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
|
||
<li ng:repeat="name in names">
|
||
<span ng:class-odd="'ng-format-negative'"
|
||
ng:class-even="'ng-input-indicator-wait'">
|
||
{{name}}
|
||
</span>
|
||
</li>
|
||
</ol>
|
||
*
|
||
* @scenario
|
||
it('should check ng:class-odd and ng:class-even', function(){
|
||
expect(element('.doc-example-live li:first span').attr('className')).
|
||
toMatch(/ng-format-negative/);
|
||
expect(element('.doc-example-live li:last span').attr('className')).
|
||
toMatch(/ng-input-indicator-wait/);
|
||
});
|
||
*/
|
||
angularDirective("ng:class-odd", ngClass(function(i){return i % 2 === 0;}));
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:class-even
|
||
*
|
||
* @description
|
||
* The `ng:class-odd` and `ng:class-even` works exactly as
|
||
* `ng:class`, except it works in conjunction with `ng:repeat`
|
||
* and takes affect only on odd (even) rows.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression to eval. Must be inside
|
||
* `ng:repeat`.
|
||
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
<ol ng:init="names=['John', 'Mary', 'Cate', 'Suz']">
|
||
<li ng:repeat="name in names">
|
||
<span ng:class-odd="'ng-format-negative'"
|
||
ng:class-even="'ng-input-indicator-wait'">
|
||
{{name}}
|
||
</span>
|
||
</li>
|
||
</ol>
|
||
*
|
||
* @scenario
|
||
it('should check ng:class-odd and ng:class-even', function(){
|
||
expect(element('.doc-example-live li:first span').attr('className')).
|
||
toMatch(/ng-format-negative/);
|
||
expect(element('.doc-example-live li:last span').attr('className')).
|
||
toMatch(/ng-input-indicator-wait/);
|
||
});
|
||
*/
|
||
angularDirective("ng:class-even", ngClass(function(i){return i % 2 === 1;}));
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:show
|
||
*
|
||
* @description
|
||
* The `ng:show` and `ng:hide` allows you to show or hide a portion
|
||
* of the HTML conditionally.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression if truthy then the element is
|
||
* shown or hidden respectively.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
Click me: <input type="checkbox" name="checked"><br/>
|
||
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
|
||
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
|
||
*
|
||
* @scenario
|
||
it('should check ng:show / ng:hide', function(){
|
||
expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
|
||
expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
|
||
|
||
input('checked').check();
|
||
|
||
expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
|
||
expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
|
||
});
|
||
*/
|
||
angularDirective("ng:show", function(expression, element){
|
||
return function(element){
|
||
this.$onEval(function(){
|
||
element.css($display, toBoolean(this.$eval(expression)) ? '' : $none);
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:hide
|
||
*
|
||
* @description
|
||
* The `ng:show` and `ng:hide` allows you to show or hide a portion
|
||
* of the HTML conditionally.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression if truthy then the element is
|
||
* shown or hidden respectively.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
Click me: <input type="checkbox" name="checked"><br/>
|
||
Show: <span ng:show="checked">I show up when you checkbox is checked?</span> <br/>
|
||
Hide: <span ng:hide="checked">I hide when you checkbox is checked?</span>
|
||
*
|
||
* @scenario
|
||
it('should check ng:show / ng:hide', function(){
|
||
expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
|
||
expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
|
||
|
||
input('checked').check();
|
||
|
||
expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
|
||
expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
|
||
});
|
||
*/
|
||
angularDirective("ng:hide", function(expression, element){
|
||
return function(element){
|
||
this.$onEval(function(){
|
||
element.css($display, toBoolean(this.$eval(expression)) ? $none : '');
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:style
|
||
*
|
||
* @description
|
||
* The ng:style allows you to set CSS style on an HTML element conditionally.
|
||
*
|
||
* @element ANY
|
||
* @param {expression} expression which evals to an object whes key's are
|
||
* CSS style names and values are coresponding values for those
|
||
* CSS keys.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
<input type="button" value="set" ng:click="myStyle={color:'red'}">
|
||
<input type="button" value="clear" ng:click="myStyle={}">
|
||
<br/>
|
||
<span ng:style="myStyle">Sample Text</span>
|
||
<pre>myStyle={{myStyle}}</pre>
|
||
*
|
||
* @scenario
|
||
it('should check ng:style', function(){
|
||
expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
|
||
element('.doc-example-live :button[value=set]').click();
|
||
expect(element('.doc-example-live span').css('color')).toBe('red');
|
||
element('.doc-example-live :button[value=clear]').click();
|
||
expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
|
||
});
|
||
*/
|
||
angularDirective("ng:style", function(expression, element){
|
||
return function(element){
|
||
var resetStyle = getStyle(element);
|
||
this.$onEval(function(){
|
||
var style = this.$eval(expression) || {}, key, mergedStyle = {};
|
||
for(key in style) {
|
||
if (resetStyle[key] === _undefined) resetStyle[key] = '';
|
||
mergedStyle[key] = style[key];
|
||
}
|
||
for(key in resetStyle) {
|
||
mergedStyle[key] = mergedStyle[key] || resetStyle[key];
|
||
}
|
||
element.css(mergedStyle);
|
||
}, element);
|
||
};
|
||
});
|
||
|
||
function parseBindings(string) {
|
||
var results = [];
|
||
var lastIndex = 0;
|
||
var index;
|
||
while((index = string.indexOf('{{', lastIndex)) > -1) {
|
||
if (lastIndex < index)
|
||
results.push(string.substr(lastIndex, index - lastIndex));
|
||
lastIndex = index;
|
||
|
||
index = string.indexOf('}}', index);
|
||
index = index < 0 ? string.length : index + 2;
|
||
|
||
results.push(string.substr(lastIndex, index - lastIndex));
|
||
lastIndex = index;
|
||
}
|
||
if (lastIndex != string.length)
|
||
results.push(string.substr(lastIndex, string.length - lastIndex));
|
||
return results.length === 0 ? [ string ] : results;
|
||
}
|
||
|
||
function binding(string) {
|
||
var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/);
|
||
return binding ? binding[1] : _null;
|
||
}
|
||
|
||
function hasBindings(bindings) {
|
||
return bindings.length > 1 || binding(bindings[0]) !== _null;
|
||
}
|
||
|
||
angularTextMarkup('{{}}', function(text, textNode, parentElement) {
|
||
var bindings = parseBindings(text),
|
||
self = this;
|
||
if (hasBindings(bindings)) {
|
||
if (isLeafNode(parentElement[0])) {
|
||
parentElement.attr('ng:bind-template', text);
|
||
} else {
|
||
var cursor = textNode, newElement;
|
||
foreach(parseBindings(text), function(text){
|
||
var exp = binding(text);
|
||
if (exp) {
|
||
newElement = self.element('span');
|
||
newElement.attr('ng:bind', exp);
|
||
} else {
|
||
newElement = self.text(text);
|
||
}
|
||
if (msie && text.charAt(0) == ' ') {
|
||
newElement = jqLite('<span> </span>');
|
||
var nbsp = newElement.html();
|
||
newElement.text(text.substr(1));
|
||
newElement.html(nbsp + newElement.html());
|
||
}
|
||
cursor.after(newElement);
|
||
cursor = newElement;
|
||
});
|
||
textNode.remove();
|
||
}
|
||
}
|
||
});
|
||
|
||
// TODO: this should be widget not a markup
|
||
angularTextMarkup('OPTION', function(text, textNode, parentElement){
|
||
if (nodeName(parentElement) == "OPTION") {
|
||
var select = document.createElement('select');
|
||
select.insertBefore(parentElement[0].cloneNode(true), _null);
|
||
if (!select.innerHTML.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\s*option\s*>/gi)) {
|
||
parentElement.attr('value', text);
|
||
}
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:href
|
||
*
|
||
* @description
|
||
* Using <angular/> markup like {{hash}} in an href attribute makes
|
||
* the page open to a wrong URL, ff the user clicks that link before
|
||
* angular has a chance to replace the {{hash}} with actual URL, the
|
||
* link will be broken and will most likely return a 404 error.
|
||
* The `ng:href` solves this problem by placing the `href` in the
|
||
* `ng:` namespace.
|
||
*
|
||
* The buggy way to write it:
|
||
* <pre>
|
||
* <a href="http://www.gravatar.com/avatar/{{hash}}"/>
|
||
* </pre>
|
||
*
|
||
* The correct way to write it:
|
||
* <pre>
|
||
* <a ng:href="http://www.gravatar.com/avatar/{{hash}}"/>
|
||
* </pre>
|
||
*
|
||
* @element ANY
|
||
* @param {template} template any string which can contain `{{}}` markup.
|
||
*/
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:src
|
||
*
|
||
* @description
|
||
* Using <angular/> markup like `{{hash}}` in a `src` attribute doesn't
|
||
* work right: The browser will fetch from the URL with the literal
|
||
* text `{{hash}}` until <angular/> replaces the expression inside
|
||
* `{{hash}}`. The `ng:src` attribute solves this problem by placing
|
||
* the `src` attribute in the `ng:` namespace.
|
||
*
|
||
* The buggy way to write it:
|
||
* <pre>
|
||
* <img src="http://www.gravatar.com/avatar/{{hash}}"/>
|
||
* </pre>
|
||
*
|
||
* The correct way to write it:
|
||
* <pre>
|
||
* <img ng:src="http://www.gravatar.com/avatar/{{hash}}"/>
|
||
* </pre>
|
||
*
|
||
* @element ANY
|
||
* @param {template} template any string which can contain `{{}}` markup.
|
||
*/
|
||
|
||
var NG_BIND_ATTR = 'ng:bind-attr';
|
||
var SPECIAL_ATTRS = {'ng:src': 'src', 'ng:href': 'href'};
|
||
angularAttrMarkup('{{}}', function(value, name, element){
|
||
// don't process existing attribute markup
|
||
if (angularDirective(name) || angularDirective("@" + name)) return;
|
||
if (msie && name == 'src')
|
||
value = decodeURI(value);
|
||
var bindings = parseBindings(value),
|
||
bindAttr;
|
||
if (hasBindings(bindings)) {
|
||
element.removeAttr(name);
|
||
bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}");
|
||
bindAttr[SPECIAL_ATTRS[name] || name] = value;
|
||
element.attr(NG_BIND_ATTR, toJson(bindAttr));
|
||
}
|
||
});
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.HTML
|
||
*
|
||
* @description
|
||
* The most common widgets you will use will be in the from of the
|
||
* standard HTML set. These widgets are bound using the name attribute
|
||
* to an expression. In addition they can have `ng:validate`, `ng:required`,
|
||
* `ng:format`, `ng:change` attribute to further control their behavior.
|
||
*
|
||
* @usageContent
|
||
* see example below for usage
|
||
*
|
||
* <input type="text|checkbox|..." ... />
|
||
* <textarea ... />
|
||
* <select ...>
|
||
* <option>...</option>
|
||
* </select>
|
||
*
|
||
* @example
|
||
<table style="font-size:.9em;">
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Format</th>
|
||
<th>HTML</th>
|
||
<th>UI</th>
|
||
<th ng:non-bindable>{{input#}}</th>
|
||
</tr>
|
||
<tr>
|
||
<th>text</th>
|
||
<td>String</td>
|
||
<td><tt><input type="text" name="input1"></tt></td>
|
||
<td><input type="text" name="input1" size="4"></td>
|
||
<td><tt>{{input1|json}}</tt></td>
|
||
</tr>
|
||
<tr>
|
||
<th>textarea</th>
|
||
<td>String</td>
|
||
<td><tt><textarea name="input2"></textarea></tt></td>
|
||
<td><textarea name="input2" cols='6'></textarea></td>
|
||
<td><tt>{{input2|json}}</tt></td>
|
||
</tr>
|
||
<tr>
|
||
<th>radio</th>
|
||
<td>String</td>
|
||
<td><tt>
|
||
<input type="radio" name="input3" value="A"><br>
|
||
<input type="radio" name="input3" value="B">
|
||
</tt></td>
|
||
<td>
|
||
<input type="radio" name="input3" value="A">
|
||
<input type="radio" name="input3" value="B">
|
||
</td>
|
||
<td><tt>{{input3|json}}</tt></td>
|
||
</tr>
|
||
<tr>
|
||
<th>checkbox</th>
|
||
<td>Boolean</td>
|
||
<td><tt><input type="checkbox" name="input4" value="checked"></tt></td>
|
||
<td><input type="checkbox" name="input4" value="checked"></td>
|
||
<td><tt>{{input4|json}}</tt></td>
|
||
</tr>
|
||
<tr>
|
||
<th>pulldown</th>
|
||
<td>String</td>
|
||
<td><tt>
|
||
<select name="input5"><br>
|
||
<option value="c">C</option><br>
|
||
<option value="d">D</option><br>
|
||
</select><br>
|
||
</tt></td>
|
||
<td>
|
||
<select name="input5">
|
||
<option value="c">C</option>
|
||
<option value="d">D</option>
|
||
</select>
|
||
</td>
|
||
<td><tt>{{input5|json}}</tt></td>
|
||
</tr>
|
||
<tr>
|
||
<th>multiselect</th>
|
||
<td>Array</td>
|
||
<td><tt>
|
||
<select name="input6" multiple size="4"><br>
|
||
<option value="e">E</option><br>
|
||
<option value="f">F</option><br>
|
||
</select><br>
|
||
</tt></td>
|
||
<td>
|
||
<select name="input6" multiple size="4">
|
||
<option value="e">E</option>
|
||
<option value="f">F</option>
|
||
</select>
|
||
</td>
|
||
<td><tt>{{input6|json}}</tt></td>
|
||
</tr>
|
||
</table>
|
||
|
||
* @scenario
|
||
* it('should exercise text', function(){
|
||
* input('input1').enter('Carlos');
|
||
* expect(binding('input1')).toEqual('"Carlos"');
|
||
* });
|
||
* it('should exercise textarea', function(){
|
||
* input('input2').enter('Carlos');
|
||
* expect(binding('input2')).toEqual('"Carlos"');
|
||
* });
|
||
* it('should exercise radio', function(){
|
||
* expect(binding('input3')).toEqual('null');
|
||
* input('input3').select('A');
|
||
* expect(binding('input3')).toEqual('"A"');
|
||
* input('input3').select('B');
|
||
* expect(binding('input3')).toEqual('"B"');
|
||
* });
|
||
* it('should exercise checkbox', function(){
|
||
* expect(binding('input4')).toEqual('false');
|
||
* input('input4').check();
|
||
* expect(binding('input4')).toEqual('true');
|
||
* });
|
||
* it('should exercise pulldown', function(){
|
||
* expect(binding('input5')).toEqual('"c"');
|
||
* select('input5').option('d');
|
||
* expect(binding('input5')).toEqual('"d"');
|
||
* });
|
||
* it('should exercise multiselect', function(){
|
||
* expect(binding('input6')).toEqual('[]');
|
||
* select('input6').options('e');
|
||
* expect(binding('input6')).toEqual('["e"]');
|
||
* select('input6').options('e', 'f');
|
||
* expect(binding('input6')).toEqual('["e","f"]');
|
||
* });
|
||
*/
|
||
|
||
function modelAccessor(scope, element) {
|
||
var expr = element.attr('name');
|
||
if (expr) {
|
||
return {
|
||
get: function() {
|
||
return scope.$eval(expr);
|
||
},
|
||
set: function(value) {
|
||
if (value !== _undefined) {
|
||
return scope.$tryEval(expr + '=' + toJson(value), element);
|
||
}
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
function modelFormattedAccessor(scope, element) {
|
||
var accessor = modelAccessor(scope, element),
|
||
formatterName = element.attr('ng:format') || NOOP,
|
||
formatter = angularFormatter(formatterName);
|
||
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
|
||
if (accessor) {
|
||
return {
|
||
get: function() {
|
||
return formatter.format(accessor.get());
|
||
},
|
||
set: function(value) {
|
||
return accessor.set(formatter.parse(value));
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
function compileValidator(expr) {
|
||
return parser(expr).validator()();
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.@ng:validate
|
||
*
|
||
* @description
|
||
* The `ng:validate` attribute widget validates the user input. If the input does not pass
|
||
* validation, the `ng-validation-error` CSS class and the `ng:error` attribute are set on the input
|
||
* element. Check out {@link angular.validator validators} to find out more.
|
||
*
|
||
* @param {string} validator The name of a built-in or custom {@link angular.validator validator} to
|
||
* to be used.
|
||
*
|
||
* @element INPUT
|
||
* @css ng-validation-error
|
||
*
|
||
* @exampleDescription
|
||
* This example shows how the input element becomes red when it contains invalid input. Correct
|
||
* the input to make the error disappear.
|
||
*
|
||
* @example
|
||
I don't validate:
|
||
<input type="text" name="value" value="NotANumber"><br/>
|
||
|
||
I need an integer or nothing:
|
||
<input type="text" name="value" ng:validate="integer"><br/>
|
||
*
|
||
* @scenario
|
||
it('should check ng:validate', function(){
|
||
expect(element('.doc-example-live :input:last').attr('className')).
|
||
toMatch(/ng-validation-error/);
|
||
|
||
input('value').enter('123');
|
||
expect(element('.doc-example-live :input:last').attr('className')).
|
||
not().toMatch(/ng-validation-error/);
|
||
});
|
||
*/
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.@ng:required
|
||
*
|
||
* @description
|
||
* The `ng:required` attribute widget validates that the user input is present. It is a special case
|
||
* of the {@link angular.widget.@ng:validate ng:validate} attribute widget.
|
||
*
|
||
* @element INPUT
|
||
* @css ng-validation-error
|
||
*
|
||
* @exampleDescription
|
||
* This example shows how the input element becomes red when it contains invalid input. Correct
|
||
* the input to make the error disappear.
|
||
*
|
||
* @example
|
||
I cannot be blank: <input type="text" name="value" ng:required><br/>
|
||
*
|
||
* @scenario
|
||
it('should check ng:required', function(){
|
||
expect(element('.doc-example-live :input').attr('className')).toMatch(/ng-validation-error/);
|
||
input('value').enter('123');
|
||
expect(element('.doc-example-live :input').attr('className')).not().toMatch(/ng-validation-error/);
|
||
});
|
||
*/
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.@ng:format
|
||
*
|
||
* @description
|
||
* The `ng:format` attribute widget formats stored data to user-readable text and parses the text
|
||
* back to the stored form. You might find this useful for example if you collect user input in a
|
||
* text field but need to store the data in the model as a list. Check out
|
||
* {@link angular.formatter formatters} to learn more.
|
||
*
|
||
* @param {string} formatter The name of the built-in or custom {@link angular.formatter formatter}
|
||
* to be used.
|
||
*
|
||
* @element INPUT
|
||
*
|
||
* @exampleDescription
|
||
* This example shows how the user input is converted from a string and internally represented as an
|
||
* array.
|
||
*
|
||
* @example
|
||
Enter a comma separated list of items:
|
||
<input type="text" name="list" ng:format="list" value="table, chairs, plate">
|
||
<pre>list={{list}}</pre>
|
||
*
|
||
* @scenario
|
||
it('should check ng:format', function(){
|
||
expect(binding('list')).toBe('list=["table","chairs","plate"]');
|
||
input('list').enter(',,, a ,,,');
|
||
expect(binding('list')).toBe('list=["a"]');
|
||
});
|
||
*/
|
||
function valueAccessor(scope, element) {
|
||
var validatorName = element.attr('ng:validate') || NOOP,
|
||
validator = compileValidator(validatorName),
|
||
requiredExpr = element.attr('ng:required'),
|
||
formatterName = element.attr('ng:format') || NOOP,
|
||
formatter = angularFormatter(formatterName),
|
||
format, parse, lastError, required,
|
||
invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop};
|
||
if (!validator) throw "Validator named '" + validatorName + "' not found.";
|
||
if (!formatter) throw "Formatter named '" + formatterName + "' not found.";
|
||
format = formatter.format;
|
||
parse = formatter.parse;
|
||
if (requiredExpr) {
|
||
scope.$watch(requiredExpr, function(newValue) {
|
||
required = newValue;
|
||
validate();
|
||
});
|
||
} else {
|
||
required = requiredExpr === '';
|
||
}
|
||
|
||
element.data($$validate, validate);
|
||
return {
|
||
get: function(){
|
||
if (lastError)
|
||
elementError(element, NG_VALIDATION_ERROR, _null);
|
||
try {
|
||
var value = parse(element.val());
|
||
validate();
|
||
return value;
|
||
} catch (e) {
|
||
lastError = e;
|
||
elementError(element, NG_VALIDATION_ERROR, e);
|
||
}
|
||
},
|
||
set: function(value) {
|
||
var oldValue = element.val(),
|
||
newValue = format(value);
|
||
if (oldValue != newValue) {
|
||
element.val(newValue || ''); // needed for ie
|
||
}
|
||
validate();
|
||
}
|
||
};
|
||
|
||
function validate() {
|
||
var value = trim(element.val());
|
||
if (element[0].disabled || element[0].readOnly) {
|
||
elementError(element, NG_VALIDATION_ERROR, _null);
|
||
invalidWidgets.markValid(element);
|
||
} else {
|
||
var error, validateScope = inherit(scope, {$element:element});
|
||
error = required && !value ?
|
||
'Required' :
|
||
(value ? validator(validateScope, value) : _null);
|
||
elementError(element, NG_VALIDATION_ERROR, error);
|
||
lastError = error;
|
||
if (error) {
|
||
invalidWidgets.markInvalid(element);
|
||
} else {
|
||
invalidWidgets.markValid(element);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function checkedAccessor(scope, element) {
|
||
var domElement = element[0], elementValue = domElement.value;
|
||
return {
|
||
get: function(){
|
||
return !!domElement.checked;
|
||
},
|
||
set: function(value){
|
||
domElement.checked = toBoolean(value);
|
||
}
|
||
};
|
||
}
|
||
|
||
function radioAccessor(scope, element) {
|
||
var domElement = element[0];
|
||
return {
|
||
get: function(){
|
||
return domElement.checked ? domElement.value : _null;
|
||
},
|
||
set: function(value){
|
||
domElement.checked = value == domElement.value;
|
||
}
|
||
};
|
||
}
|
||
|
||
function optionsAccessor(scope, element) {
|
||
var options = element[0].options;
|
||
return {
|
||
get: function(){
|
||
var values = [];
|
||
foreach(options, function(option){
|
||
if (option.selected) values.push(option.value);
|
||
});
|
||
return values;
|
||
},
|
||
set: function(values){
|
||
var keys = {};
|
||
foreach(values, function(value){ keys[value] = true; });
|
||
foreach(options, function(option){
|
||
option.selected = keys[option.value];
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
function noopAccessor() { return { get: noop, set: noop }; }
|
||
|
||
var textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue(), true),
|
||
buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop),
|
||
INPUT_TYPE = {
|
||
'text': textWidget,
|
||
'textarea': textWidget,
|
||
'hidden': textWidget,
|
||
'password': textWidget,
|
||
'button': buttonWidget,
|
||
'submit': buttonWidget,
|
||
'reset': buttonWidget,
|
||
'image': buttonWidget,
|
||
'checkbox': inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)),
|
||
'radio': inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit),
|
||
'select-one': inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(_null)),
|
||
'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([]))
|
||
// 'file': fileWidget???
|
||
};
|
||
|
||
|
||
function initWidgetValue(initValue) {
|
||
return function (model, view) {
|
||
var value = view.get();
|
||
if (!value && isDefined(initValue)) {
|
||
value = copy(initValue);
|
||
}
|
||
if (isUndefined(model.get()) && isDefined(value)) {
|
||
model.set(value);
|
||
}
|
||
};
|
||
}
|
||
|
||
function radioInit(model, view, element) {
|
||
var modelValue = model.get(), viewValue = view.get(), input = element[0];
|
||
input.checked = false;
|
||
input.name = this.$id + '@' + input.name;
|
||
if (isUndefined(modelValue)) {
|
||
model.set(modelValue = _null);
|
||
}
|
||
if (modelValue == _null && viewValue !== _null) {
|
||
model.set(viewValue);
|
||
}
|
||
view.set(modelValue);
|
||
}
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc directive
|
||
* @name angular.directive.ng:change
|
||
*
|
||
* @description
|
||
* The directive executes an expression whenever the input widget changes.
|
||
*
|
||
* @element INPUT
|
||
* @param {expression} expression to execute.
|
||
*
|
||
* @exampleDescription
|
||
* @example
|
||
<div ng:init="checkboxCount=0; textCount=0"></div>
|
||
<input type="text" name="text" ng:change="textCount = 1 + textCount">
|
||
changeCount {{textCount}}<br/>
|
||
<input type="checkbox" name="checkbox" ng:change="checkboxCount = 1 + checkboxCount">
|
||
changeCount {{checkboxCount}}<br/>
|
||
*
|
||
* @scenario
|
||
it('should check ng:change', function(){
|
||
expect(binding('textCount')).toBe('0');
|
||
expect(binding('checkboxCount')).toBe('0');
|
||
|
||
using('.doc-example-live').input('text').enter('abc');
|
||
expect(binding('textCount')).toBe('1');
|
||
expect(binding('checkboxCount')).toBe('0');
|
||
|
||
|
||
using('.doc-example-live').input('checkbox').check();
|
||
expect(binding('textCount')).toBe('1');
|
||
expect(binding('checkboxCount')).toBe('1');
|
||
});
|
||
*/
|
||
function inputWidget(events, modelAccessor, viewAccessor, initFn, dirtyChecking) {
|
||
return function(element) {
|
||
var scope = this,
|
||
model = modelAccessor(scope, element),
|
||
view = viewAccessor(scope, element),
|
||
action = element.attr('ng:change') || '',
|
||
lastValue;
|
||
if (model) {
|
||
initFn.call(scope, model, view, element);
|
||
this.$eval(element.attr('ng:init')||'');
|
||
// Don't register a handler if we are a button (noopAccessor) and there is no action
|
||
if (action || modelAccessor !== noopAccessor) {
|
||
element.bind(events, function (){
|
||
var value = view.get();
|
||
if (!dirtyChecking || value != lastValue) {
|
||
model.set(value);
|
||
lastValue = model.get();
|
||
scope.$tryEval(action, element);
|
||
scope.$root.$eval();
|
||
}
|
||
});
|
||
}
|
||
scope.$watch(model.get, function(value){
|
||
if (lastValue !== value) {
|
||
view.set(lastValue = value);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
|
||
function inputWidgetSelector(element){
|
||
this.directives(true);
|
||
return INPUT_TYPE[lowercase(element[0].type)] || noop;
|
||
}
|
||
|
||
angularWidget('input', inputWidgetSelector);
|
||
angularWidget('textarea', inputWidgetSelector);
|
||
angularWidget('button', inputWidgetSelector);
|
||
angularWidget('select', function(element){
|
||
this.descend(true);
|
||
return inputWidgetSelector.call(this, element);
|
||
});
|
||
|
||
|
||
/*
|
||
* Consider this:
|
||
* <select name="selection">
|
||
* <option ng:repeat="x in [1,2]">{{x}}</option>
|
||
* </select>
|
||
*
|
||
* The issue is that the select gets evaluated before option is unrolled.
|
||
* This means that the selection is undefined, but the browser
|
||
* default behavior is to show the top selection in the list.
|
||
* To fix that we register a $update function on the select element
|
||
* and the option creation then calls the $update function when it is
|
||
* unrolled. The $update function then calls this update function, which
|
||
* then tries to determine if the model is unassigned, and if so it tries to
|
||
* chose one of the options from the list.
|
||
*/
|
||
angularWidget('option', function(){
|
||
this.descend(true);
|
||
this.directives(true);
|
||
return function(element) {
|
||
var select = element.parent();
|
||
var scope = retrieveScope(select);
|
||
var model = modelFormattedAccessor(scope, select);
|
||
var view = valueAccessor(scope, select);
|
||
var option = element;
|
||
var lastValue = option.attr($value);
|
||
var lastSelected = option.attr('ng-' + $selected);
|
||
element.data($$update, function(){
|
||
var value = option.attr($value);
|
||
var selected = option.attr('ng-' + $selected);
|
||
var modelValue = model.get();
|
||
if (lastSelected != selected || lastValue != value) {
|
||
lastSelected = selected;
|
||
lastValue = value;
|
||
if (selected || modelValue == _null || modelValue == _undefined)
|
||
model.set(value);
|
||
if (value == modelValue) {
|
||
view.set(lastValue);
|
||
}
|
||
}
|
||
});
|
||
};
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.ng:include
|
||
*
|
||
* @description
|
||
* Include external HTML fragment.
|
||
*
|
||
* Keep in mind that Same Origin Policy applies to included resources
|
||
* (e.g. ng:include won't work for file:// access).
|
||
*
|
||
* @param {string} src expression evaluating to URL.
|
||
* @param {Scope=} [scope=new_child_scope] expression evaluating to angular.scope
|
||
* @param {string=} onload Expression to evaluate when a new partial is loaded.
|
||
*
|
||
* @example
|
||
* <select name="url">
|
||
* <option value="angular.filter.date.html">date filter</option>
|
||
* <option value="angular.filter.html.html">html filter</option>
|
||
* <option value="">(blank)</option>
|
||
* </select>
|
||
* <tt>url = <a href="{{url}}">{{url}}</a></tt>
|
||
* <hr/>
|
||
* <ng:include src="url"></ng:include>
|
||
*
|
||
* @scenario
|
||
* it('should load date filter', function(){
|
||
* expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.date/);
|
||
* });
|
||
* it('should change to hmtl filter', function(){
|
||
* select('url').option('angular.filter.html.html');
|
||
* expect(element('.doc-example ng\\:include').text()).toMatch(/angular\.filter\.html/);
|
||
* });
|
||
* it('should change to blank', function(){
|
||
* select('url').option('(blank)');
|
||
* expect(element('.doc-example ng\\:include').text()).toEqual('');
|
||
* });
|
||
*/
|
||
angularWidget('ng:include', function(element){
|
||
var compiler = this,
|
||
srcExp = element.attr("src"),
|
||
scopeExp = element.attr("scope") || '',
|
||
onloadExp = element[0].getAttribute('onload') || ''; //workaround for jquery bug #7537
|
||
if (element[0]['ng:compiled']) {
|
||
this.descend(true);
|
||
this.directives(true);
|
||
} else {
|
||
element[0]['ng:compiled'] = true;
|
||
return extend(function(xhr, element){
|
||
var scope = this, childScope;
|
||
var changeCounter = 0;
|
||
var preventRecursion = false;
|
||
function incrementChange(){ changeCounter++;}
|
||
this.$watch(srcExp, incrementChange);
|
||
this.$watch(scopeExp, incrementChange);
|
||
scope.$onEval(function(){
|
||
if (childScope && !preventRecursion) {
|
||
preventRecursion = true;
|
||
try {
|
||
childScope.$eval();
|
||
} finally {
|
||
preventRecursion = false;
|
||
}
|
||
}
|
||
});
|
||
this.$watch(function(){return changeCounter;}, function(){
|
||
var src = this.$eval(srcExp),
|
||
useScope = this.$eval(scopeExp);
|
||
|
||
if (src) {
|
||
xhr('GET', src, function(code, response){
|
||
element.html(response);
|
||
childScope = useScope || createScope(scope);
|
||
compiler.compile(element)(element, childScope);
|
||
childScope.$init();
|
||
scope.$eval(onloadExp);
|
||
});
|
||
} else {
|
||
childScope = null;
|
||
element.html('');
|
||
}
|
||
});
|
||
}, {$inject:['$xhr.cache']});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.ng:switch
|
||
*
|
||
* @description
|
||
* Conditionally change the DOM structure.
|
||
*
|
||
* @usageContent
|
||
* <any ng:switch-when="matchValue1">...</any>
|
||
* <any ng:switch-when="matchValue2">...</any>
|
||
* ...
|
||
* <any ng:switch-default>...</any>
|
||
*
|
||
* @param {*} on expression to match against <tt>ng:switch-when</tt>.
|
||
* @paramDescription
|
||
* On child elments add:
|
||
*
|
||
* * `ng:switch-when`: the case statement to match against. If match then this
|
||
* case will be displayed.
|
||
* * `ng:switch-default`: the default case when no other casses match.
|
||
*
|
||
* @example
|
||
<select name="switch">
|
||
<option>settings</option>
|
||
<option>home</option>
|
||
<option>other</option>
|
||
</select>
|
||
<tt>switch={{switch}}</tt>
|
||
</hr>
|
||
<ng:switch on="switch" >
|
||
<div ng:switch-when="settings">Settings Div</div>
|
||
<span ng:switch-when="home">Home Span</span>
|
||
<span ng:switch-default>default</span>
|
||
</ng:switch>
|
||
</code>
|
||
*
|
||
* @scenario
|
||
* it('should start in settings', function(){
|
||
* expect(element('.doc-example ng\\:switch').text()).toEqual('Settings Div');
|
||
* });
|
||
* it('should change to home', function(){
|
||
* select('switch').option('home');
|
||
* expect(element('.doc-example ng\\:switch').text()).toEqual('Home Span');
|
||
* });
|
||
* it('should select deafault', function(){
|
||
* select('switch').option('other');
|
||
* expect(element('.doc-example ng\\:switch').text()).toEqual('default');
|
||
* });
|
||
*/
|
||
var ngSwitch = angularWidget('ng:switch', function (element){
|
||
var compiler = this,
|
||
watchExpr = element.attr("on"),
|
||
usingExpr = (element.attr("using") || 'equals'),
|
||
usingExprParams = usingExpr.split(":"),
|
||
usingFn = ngSwitch[usingExprParams.shift()],
|
||
changeExpr = element.attr('change') || '',
|
||
cases = [];
|
||
if (!usingFn) throw "Using expression '" + usingExpr + "' unknown.";
|
||
if (!watchExpr) throw "Missing 'on' attribute.";
|
||
eachNode(element, function(caseElement){
|
||
var when = caseElement.attr('ng:switch-when');
|
||
var switchCase = {
|
||
change: changeExpr,
|
||
element: caseElement,
|
||
template: compiler.compile(caseElement)
|
||
};
|
||
if (isString(when)) {
|
||
switchCase.when = function(scope, value){
|
||
var args = [value, when];
|
||
foreach(usingExprParams, function(arg){
|
||
args.push(arg);
|
||
});
|
||
return usingFn.apply(scope, args);
|
||
};
|
||
cases.unshift(switchCase);
|
||
} else if (isString(caseElement.attr('ng:switch-default'))) {
|
||
switchCase.when = valueFn(true);
|
||
cases.push(switchCase);
|
||
}
|
||
});
|
||
|
||
// this needs to be here for IE
|
||
foreach(cases, function(_case){
|
||
_case.element.remove();
|
||
});
|
||
|
||
element.html('');
|
||
return function(element){
|
||
var scope = this, childScope;
|
||
this.$watch(watchExpr, function(value){
|
||
var found = false;
|
||
element.html('');
|
||
childScope = createScope(scope);
|
||
foreach(cases, function(switchCase){
|
||
if (!found && switchCase.when(childScope, value)) {
|
||
found = true;
|
||
var caseElement = quickClone(switchCase.element);
|
||
element.append(caseElement);
|
||
childScope.$tryEval(switchCase.change, element);
|
||
switchCase.template(caseElement, childScope);
|
||
childScope.$init();
|
||
}
|
||
});
|
||
});
|
||
scope.$onEval(function(){
|
||
if (childScope) childScope.$eval();
|
||
});
|
||
};
|
||
}, {
|
||
equals: function(on, when) {
|
||
return ''+on == when;
|
||
},
|
||
route: switchRouteMatcher
|
||
});
|
||
|
||
|
||
/*
|
||
* Modifies the default behavior of html A tag, so that the default action is prevented when href
|
||
* attribute is empty.
|
||
*
|
||
* The reasoning for this change is to allow easy creation of action links with ng:click without
|
||
* changing the location or causing page reloads, e.g.:
|
||
* <a href="" ng:click="model.$save()">Save</a>
|
||
*/
|
||
angularWidget('a', function() {
|
||
this.descend(true);
|
||
this.directives(true);
|
||
|
||
return function(element) {
|
||
if (element.attr('href') === '') {
|
||
element.bind('click', function(event){
|
||
event.preventDefault();
|
||
});
|
||
}
|
||
};
|
||
});
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.@ng:repeat
|
||
*
|
||
* @description
|
||
* `ng:repeat` instantiates a template once per item from a collection. The collection is enumerated
|
||
* with `ng:repeat-index` attribute starting from 0. Each template instance gets its own scope where
|
||
* the given loop variable is set to the current collection item and `$index` is set to the item
|
||
* index or key.
|
||
*
|
||
* There are special properties exposed on the local scope of each template instance:
|
||
*
|
||
* * `$index` – `{number}` – iterator offset of the repeated element (0..length-1)
|
||
* * `$position` – {string} – position of the repeated element in the iterator. One of: `'first'`,
|
||
* `'middle'` or `'last'`.
|
||
*
|
||
* NOTE: `ng:repeat` looks like a directive, but is actually an attribute widget.
|
||
*
|
||
* @element ANY
|
||
* @param {string} repeat_expression The expression indicating how to enumerate a collection. Two
|
||
* formats are currently supported:
|
||
*
|
||
* * `variable in expression` – where variable is the user defined loop variable and `expression`
|
||
* is a scope expression giving the collection to enumerate.
|
||
*
|
||
* For example: `track in cd.tracks`.
|
||
* * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
|
||
* and `expression` is the scope expression giving the collection to enumerate.
|
||
*
|
||
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
|
||
*
|
||
* @exampleDescription
|
||
* This example initializes the scope to a list of names and
|
||
* than uses `ng:repeat` to display every person.
|
||
* @example
|
||
<div ng:init="friends = [{name:'John', age:25}, {name:'Mary', age:28}]">
|
||
I have {{friends.length}} friends. They are:
|
||
<ul>
|
||
<li ng:repeat="friend in friends">
|
||
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
* @scenario
|
||
it('should check ng:repeat', function(){
|
||
var r = using('.doc-example-live').repeater('ul li');
|
||
expect(r.count()).toBe(2);
|
||
expect(r.row(0)).toEqual(["1","John","25"]);
|
||
expect(r.row(1)).toEqual(["2","Mary","28"]);
|
||
});
|
||
*/
|
||
angularWidget("@ng:repeat", function(expression, element){
|
||
element.removeAttr('ng:repeat');
|
||
element.replaceWith(this.comment("ng:repeat: " + expression));
|
||
var template = this.compile(element);
|
||
return function(reference){
|
||
var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
|
||
lhs, rhs, valueIdent, keyIdent;
|
||
if (! match) {
|
||
throw Error("Expected ng:repeat in form of 'item in collection' but got '" +
|
||
expression + "'.");
|
||
}
|
||
lhs = match[1];
|
||
rhs = match[2];
|
||
match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
|
||
if (!match) {
|
||
throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
|
||
keyValue + "'.");
|
||
}
|
||
valueIdent = match[3] || match[1];
|
||
keyIdent = match[2];
|
||
|
||
var children = [], currentScope = this;
|
||
this.$onEval(function(){
|
||
var index = 0,
|
||
childCount = children.length,
|
||
lastElement = reference,
|
||
collection = this.$tryEval(rhs, reference),
|
||
is_array = isArray(collection),
|
||
collectionLength = 0,
|
||
childScope,
|
||
key;
|
||
|
||
if (is_array) {
|
||
collectionLength = collection.length;
|
||
} else {
|
||
for (key in collection)
|
||
if (collection.hasOwnProperty(key))
|
||
collectionLength++;
|
||
}
|
||
|
||
for (key in collection) {
|
||
if (!is_array || collection.hasOwnProperty(key)) {
|
||
if (index < childCount) {
|
||
// reuse existing child
|
||
childScope = children[index];
|
||
childScope[valueIdent] = collection[key];
|
||
if (keyIdent) childScope[keyIdent] = key;
|
||
} else {
|
||
// grow children
|
||
childScope = template(quickClone(element), createScope(currentScope));
|
||
childScope[valueIdent] = collection[key];
|
||
if (keyIdent) childScope[keyIdent] = key;
|
||
lastElement.after(childScope.$element);
|
||
childScope.$index = index;
|
||
childScope.$position = index == 0 ?
|
||
'first' :
|
||
(index == collectionLength - 1 ? 'last' : 'middle');
|
||
childScope.$element.attr('ng:repeat-index', index);
|
||
childScope.$init();
|
||
children.push(childScope);
|
||
}
|
||
childScope.$eval();
|
||
lastElement = childScope.$element;
|
||
index ++;
|
||
}
|
||
}
|
||
// shrink children
|
||
while(children.length > index) {
|
||
children.pop().$element.remove();
|
||
}
|
||
}, reference);
|
||
};
|
||
});
|
||
|
||
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc widget
|
||
* @name angular.widget.@ng:non-bindable
|
||
*
|
||
* @description
|
||
* Sometimes it is necessary to write code which looks like bindings but which should be left alone
|
||
* by angular. Use `ng:non-bindable` to make angular ignore a chunk of HTML.
|
||
*
|
||
* NOTE: `ng:non-bindable` looks like a directive, but is actually an attribute widget.
|
||
*
|
||
* @element ANY
|
||
*
|
||
* @exampleDescription
|
||
* In this example there are two location where a siple binding (`{{}}`) is present, but the one
|
||
* wrapped in `ng:non-bindable` is left alone.
|
||
*
|
||
* @example
|
||
<div>Normal: {{1 + 2}}</div>
|
||
<div ng:non-bindable>Ignored: {{1 + 2}}</div>
|
||
*
|
||
* @scenario
|
||
it('should check ng:non-bindable', function(){
|
||
expect(using('.doc-example-live').binding('1 + 2')).toBe('3');
|
||
expect(using('.doc-example-live').element('div:last').text()).
|
||
toMatch(/1 \+ 2/);
|
||
});
|
||
*/
|
||
angularWidget("@ng:non-bindable", noop);
|
||
var browserSingleton;
|
||
/**
|
||
* @workInProgress
|
||
* @ngdoc service
|
||
* @name angular.service.$browser
|
||
* @requires $log
|
||
*
|
||
* @description
|
||
* Represents the browser.
|
||
*/
|
||
angularService('$browser', function($log){
|
||
if (!browserSingleton) {
|
||
browserSingleton = new Browser(
|
||
window.location,
|
||
jqLite(window.document),
|
||
jqLite(window.document.getElementsByTagName('head')[0]),
|
||
XHR,
|
||
$log,
|
||
window.setTimeout);
|
||
browserSingleton.startPoller(50, function(delay, fn){setTimeout(delay,fn);});
|
||
browserSingleton.bind();
|
||
}
|
||
return browserSingleton;
|
||
}, {inject:['$log']});
|
||
|
||
extend(angular, {
|
||
'element': jqLite,
|
||
'compile': compile,
|
||
'scope': createScope,
|
||
'copy': copy,
|
||
'extend': extend,
|
||
'equals': equals,
|
||
'foreach': foreach,
|
||
'injector': createInjector,
|
||
'noop':noop,
|
||
'bind':bind,
|
||
'toJson': toJson,
|
||
'fromJson': fromJson,
|
||
'identity':identity,
|
||
'isUndefined': isUndefined,
|
||
'isDefined': isDefined,
|
||
'isString': isString,
|
||
'isFunction': isFunction,
|
||
'isObject': isObject,
|
||
'isNumber': isNumber,
|
||
'isArray': isArray
|
||
});
|
||
|
||
|
||
window.onload = function(){
|
||
try {
|
||
if (previousOnLoad) previousOnLoad();
|
||
} catch(e) {}
|
||
angularInit(angularJsConfig(document));
|
||
};
|
||
|
||
})(window, document, window.onload);
|
||
document.write('<style type="text/css">@charset "UTF-8";.ng-format-negative{color:red;}.ng-exception{border:2px solid #FF0000;font-family:"Courier New",Courier,monospace;font-size:smaller;white-space:pre;}.ng-validation-error{border:2px solid #FF0000;}#ng-callout{margin:0;padding:0;border:0;outline:0;font-size:13px;font-weight:normal;font-family:Verdana,Arial,Helvetica,sans-serif;vertical-align:baseline;background:transparent;text-decoration:none;}#ng-callout .ng-arrow-left{background-image:url("data:image/gif;base64,R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrSLoc/AG8FeUUIN+sGebWAnbKSJodqqlsOxJtqYooU9vvk+vcJIcTkg+QAAA7");background-repeat:no-repeat;background-position:left top;position:absolute;z-index:101;left:-12px;height:23px;width:10px;top:-3px;}#ng-callout .ng-arrow-right{background-image:url("data:image/gif;base64,R0lGODlhCwAXAKIAAMzMzO/v7/f39////////wAAAAAAAAAAACH5BAUUAAQALAAAAAALABcAAAMrCLTcoM29yN6k9socs91e5X3EyJloipYrO4ohTMqA0Fn2XVNswJe+H+SXAAA7");background-repeat:no-repeat;background-position:left top;position:absolute;z-index:101;height:23px;width:11px;top:-2px;}#ng-callout{position:absolute;z-index:100;border:2px solid #CCCCCC;background-color:#fff;}#ng-callout .ng-content{padding:10px 10px 10px 10px;color:#333333;}#ng-callout .ng-title{background-color:#CCCCCC;text-align:left;padding-left:8px;padding-bottom:5px;padding-top:2px;font-weight:bold;}.ng-input-indicator-wait{background-image:url("data:image/png;base64,R0lGODlhEAAQAPQAAP///wAAAPDw8IqKiuDg4EZGRnp6egAAAFhYWCQkJKysrL6+vhQUFJycnAQEBDY2NmhoaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA==");background-position:right;background-repeat:no-repeat;}</style>'); |