aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRob Spies2010-06-22 17:09:55 -0700
committerRob Spies2010-06-22 17:09:55 -0700
commit1500e91defa4020bfe9608749b25e585cd1d8e3d (patch)
tree8c2872252b62567dc4eb00f7d7547661d5674c55 /src
parenteaa397c76b7d28343cde9f3a0338b9b0e79197c8 (diff)
parentb129a1094e6b42ed82c3ccecc2f40daaa0a6cb6a (diff)
downloadangular.js-1500e91defa4020bfe9608749b25e585cd1d8e3d.tar.bz2
Merge http://github.com/angular/angular.js into angular
Conflicts: .gitignore
Diffstat (limited to 'src')
-rw-r--r--src/Angular.js401
-rw-r--r--src/AngularPublic.js29
-rw-r--r--src/Browser.js130
-rw-r--r--src/Compiler.js212
-rw-r--r--src/JSON.js105
-rw-r--r--src/Parser.js730
-rw-r--r--src/Resource.js140
-rw-r--r--src/Scope.js224
-rw-r--r--src/angular-bootstrap.js70
-rw-r--r--src/angular.prefix24
-rw-r--r--src/angular.suffix9
-rw-r--r--src/apis.js338
-rw-r--r--src/delete/Binder.js356
-rw-r--r--src/delete/Model.js65
-rw-r--r--src/delete/Scope.js407
-rw-r--r--src/delete/Widgets.js806
-rw-r--r--src/directives.js261
-rw-r--r--src/filters.js298
-rw-r--r--src/formatters.js32
-rw-r--r--src/jqLite.js248
-rw-r--r--src/markups.js85
-rw-r--r--src/moveToAngularCom/ControlBar.js72
-rw-r--r--src/moveToAngularCom/DataStore.js330
-rw-r--r--src/moveToAngularCom/Server.js68
-rw-r--r--src/moveToAngularCom/Users.js35
-rw-r--r--src/moveToAngularCom/directivesAngularCom.js29
-rw-r--r--src/scenario/DSL.js63
-rw-r--r--src/scenario/Runner.js161
-rw-r--r--src/scenario/angular.prefix30
-rw-r--r--src/scenario/angular.suffix11
-rw-r--r--src/scenario/bootstrap.js44
-rw-r--r--src/services.js361
-rw-r--r--src/validators.js132
-rw-r--r--src/widgets.js328
34 files changed, 6634 insertions, 0 deletions
diff --git a/src/Angular.js b/src/Angular.js
new file mode 100644
index 00000000..2b26c88d
--- /dev/null
+++ b/src/Angular.js
@@ -0,0 +1,401 @@
+////////////////////////////////////
+
+if (typeof document.getAttribute == 'undefined')
+ document.getAttribute = function() {};
+
+if (!window['console']) window['console']={'log':noop, 'error':noop};
+
+var consoleNode,
+ PRIORITY_FIRST = -99999,
+ PRIORITY_WATCH = -1000,
+ PRIORITY_LAST = 99999,
+ PRIORITY = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH},
+ NOOP = 'noop',
+ NG_EXCEPTION = 'ng-exception',
+ NG_VALIDATION_ERROR = 'ng-validation-error',
+ jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy
+ _ = window['_'],
+ msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)),
+ jqLite = jQuery || jqLiteWrap,
+ slice = Array.prototype.slice,
+ angular = window['angular'] || (window['angular'] = {}),
+ angularTextMarkup = extensionMap(angular, 'textMarkup'),
+ angularAttrMarkup = extensionMap(angular, 'attrMarkup'),
+ angularDirective = extensionMap(angular, 'directive'),
+ angularWidget = extensionMap(angular, 'widget'),
+ angularValidator = extensionMap(angular, 'validator'),
+ angularFilter = extensionMap(angular, 'filter'),
+ angularFormatter = extensionMap(angular, 'formatter'),
+ angularService = extensionMap(angular, 'service'),
+ angularCallbacks = extensionMap(angular, 'callbacks'),
+ nodeName;
+
+function angularAlert(){
+ log(arguments); window.alert.apply(window, arguments);
+}
+
+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 extend(dst) {
+ foreach(arguments, function(obj){
+ if (obj !== dst) {
+ foreach(obj, function(value, key){
+ dst[key] = value;
+ });
+ }
+ });
+ return dst;
+}
+
+function noop() {}
+function identity($) {return $;}
+function extensionMap(angular, name) {
+ var extPoint;
+ return angular[name] || (extPoint = angular[name] = function (name, fn, prop){
+ if (isDefined(fn)) {
+ 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;
+}
+function isUndefined(value){ return typeof value == 'undefined'; }
+function isDefined(value){ return typeof value != 'undefined'; }
+function isObject(value){ return typeof value == 'object';}
+function isString(value){ return typeof value == 'string';}
+function isNumber(value){ return typeof value == 'number';}
+function isArray(value) { return value instanceof Array; }
+function isFunction(value){ return typeof value == 'function';}
+function isTextNode(node) { return nodeName(node) == '#text'; }
+function lowercase(value){ return isString(value) ? value.toLowerCase() : value; }
+function uppercase(value){ return isString(value) ? value.toUpperCase() : value; }
+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));
+}
+
+function HTML(html) {
+ this.html = html;
+}
+
+if (msie) {
+ nodeName = function(element) {
+ element = element[0] || element;
+ return (element.scopeName && element.scopeName != 'HTML' ) ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName;
+ };
+} else {
+ nodeName = function(element) {
+ return (element[0] || element).nodeName;
+ };
+}
+
+function isVisible(element) {
+ var rect = element[0].getBoundingClientRect(),
+ width = (rect.width || (rect.right||0 - rect.left||0)),
+ height = (rect.height || (rect.bottom||0 - rect.top||0));
+ return width>0 && height>0;
+}
+
+function map(obj, iterator, context) {
+ var results = [];
+ foreach(obj, function(value, index, list) {
+ results.push(iterator.call(context, value, index, list));
+ });
+ return results;
+}
+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 log(a, b, c){
+ var console = window['console'];
+ switch(arguments.length) {
+ case 1:
+ console['log'](a);
+ break;
+ case 2:
+ console['log'](a, b);
+ break;
+ default:
+ console['log'](a, b, c);
+ break;
+ }
+}
+
+function error(a, b, c){
+ var console = window['console'];
+ switch(arguments.length) {
+ case 1:
+ console['error'](a);
+ break;
+ case 2:
+ console['error'](a, b);
+ break;
+ default:
+ console['error'](a, b, c);
+ break;
+ }
+}
+
+function consoleLog(level, objs) {
+ var log = document.createElement("div");
+ log.className = level;
+ var msg = "";
+ var sep = "";
+ for ( var i = 0; i < objs.length; i++) {
+ var obj = objs[i];
+ msg += sep + (typeof obj == 'string' ? obj : toJson(obj));
+ sep = " ";
+ }
+ log.appendChild(document.createTextNode(msg));
+ consoleNode.appendChild(log);
+}
+
+function isLeafNode (node) {
+ if (node) {
+ switch (node.nodeName) {
+ case "OPTION":
+ case "PRE":
+ case "TITLE":
+ return true;
+ }
+ }
+ return false;
+}
+
+function copy(source, destination){
+ if (!destination) {
+ if (source) {
+ if (isArray(source)) {
+ return copy(source, []);
+ } else if (isObject(source)) {
+ return copy(source, {});
+ }
+ }
+ return 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;
+ }
+}
+
+function setHtml(node, html) {
+ if (isLeafNode(node)) {
+ if (msie) {
+ node.innerText = html;
+ } else {
+ node.textContent = html;
+ }
+ } else {
+ node.innerHTML = html;
+ }
+}
+
+function escapeHtml(html) {
+ if (!html || !html.replace)
+ return html;
+ return html.
+ replace(/&/g, '&amp;').
+ replace(/</g, '&lt;').
+ replace(/>/g, '&gt;');
+}
+
+
+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 escapeAttr(html) {
+ if (!html || !html.replace)
+ return html;
+ return html.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g,
+ '&quot;');
+}
+
+function bind(_this, _function) {
+ if (!isFunction(_function))
+ throw "Not a function!";
+ var curryArgs = slice.call(arguments, 2, arguments.length);
+ return function() {
+ return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length)));
+ };
+}
+
+function outerHTML(node) {
+ var temp = document.createElement('div');
+ temp.appendChild(node);
+ var outerHTML = temp.innerHTML;
+ temp.removeChild(node);
+ return outerHTML;
+}
+
+function toBoolean(value) {
+ if (value && value.length !== 0) {
+ var v = lowercase("" + value);
+ value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || 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);
+ }
+ }
+}
+
+function compile(element, parentScope, overrides) {
+ var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget),
+ $element = jqLite(element),
+ parent = extend({}, parentScope);
+ parent.$element = $element;
+ return compiler.compile($element)($element, parent, overrides);
+}
+/////////////////////////////////////////////////
+
+function parseKeyValue(keyValue) {
+ var obj = {}, key_value, key;
+ foreach((keyValue || "").split('&'), function(keyValue){
+ if (keyValue) {
+ key_value = keyValue.split('=');
+ key = decodeURIComponent(key_value[0]);
+ obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true;
+ }
+ });
+ return obj;
+}
+
+function toKeyValue(obj) {
+ var parts = [];
+ foreach(obj, function(value, key){
+ parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
+ });
+ return parts.length ? parts.join('&') : '';
+}
+
+function angularInit(config){
+ if (config.autobind) {
+ var scope = compile(window.document, null, {'$config':config});
+ // TODO default to the source of angular.js
+ scope.$browser.addCss('css/angular.css');
+ scope.$init();
+ }
+}
+
+function angularJsConfig(document) {
+ var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/,
+ scripts = document.getElementsByTagName("SCRIPT"),
+ match;
+ for(var j = 0; j < scripts.length; j++) {
+ match = (scripts[j].src || "").match(filename);
+ if (match) {
+ return match[5];
+ }
+ }
+ return "";
+}
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
new file mode 100644
index 00000000..7230c3e5
--- /dev/null
+++ b/src/AngularPublic.js
@@ -0,0 +1,29 @@
+var browserSingleton;
+angularService('$browser', function browserFactory(){
+ if (!browserSingleton) {
+ browserSingleton = new Browser(window.location, window.document);
+ browserSingleton.startUrlWatcher();
+ browserSingleton.bind();
+ }
+ return browserSingleton;
+});
+
+extend(angular, {
+ 'element': jqLite,
+ 'compile': compile,
+ 'scope': createScope,
+ 'copy': copy,
+ 'extend': extend,
+ 'foreach': foreach,
+ 'noop':noop,
+ 'bind':bind,
+ 'identity':identity,
+ 'isUndefined': isUndefined,
+ 'isDefined': isDefined,
+ 'isString': isString,
+ 'isFunction': isFunction,
+ 'isObject': isObject,
+ 'isNumber': isNumber,
+ 'isArray': isArray
+});
+
diff --git a/src/Browser.js b/src/Browser.js
new file mode 100644
index 00000000..0552b3ae
--- /dev/null
+++ b/src/Browser.js
@@ -0,0 +1,130 @@
+//////////////////////////////
+// Browser
+//////////////////////////////
+
+function Browser(location, document) {
+ this.delay = 50;
+ this.expectedUrl = location.href;
+ this.urlListeners = [];
+ this.hoverListener = noop;
+ this.isMock = false;
+ this.outstandingRequests = { count: 0, callbacks:[]};
+
+ this.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.");
+ };
+ this.setTimeout = function(fn, delay) {
+ window.setTimeout(fn, delay);
+ };
+
+ this.location = location;
+ this.document = jqLite(document);
+ this.body = jqLite(document.body);
+}
+
+Browser.prototype = {
+
+ bind: function() {
+ var self = this;
+ self.document.bind("mouseover", function(event){
+ self.hoverListener(jqLite(msie ? event.srcElement : event.target), true);
+ return true;
+ });
+ self.document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){
+ self.hoverListener(jqLite(event.target), false);
+ return true;
+ });
+ },
+
+ hover: function(hoverListener) {
+ this.hoverListener = hoverListener;
+ },
+
+ addCss: function(url) {
+ var doc = this.document[0],
+ head = jqLite(doc.getElementsByTagName('head')[0]),
+ link = jqLite(doc.createElement('link'));
+ link.attr('rel', 'stylesheet');
+ link.attr('type', 'text/css');
+ link.attr('href', url);
+ head.append(link);
+ },
+
+ xhr: function(method, url, post, callback){
+ if (isFunction(post)) {
+ callback = post;
+ post = null;
+ }
+ var xhr = new this.XHR(),
+ self = this;
+ xhr.open(method, url, true);
+ this.outstandingRequests.count ++;
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ try {
+ callback(xhr.status || 200, xhr.responseText);
+ } finally {
+ self.outstandingRequests.count--;
+ self.processRequestCallbacks();
+ }
+ }
+ };
+ xhr.send(post || '');
+ },
+
+ processRequestCallbacks: function(){
+ if (this.outstandingRequests.count === 0) {
+ while(this.outstandingRequests.callbacks.length) {
+ try {
+ this.outstandingRequests.callbacks.pop()();
+ } catch (e) {
+ }
+ }
+ }
+ },
+
+ notifyWhenNoOutstandingRequests: function(callback){
+ if (this.outstandingRequests.count === 0) {
+ callback();
+ } else {
+ this.outstandingRequests.callbacks.push(callback);
+ }
+ },
+
+ watchUrl: function(fn){
+ this.urlListeners.push(fn);
+ },
+
+ startUrlWatcher: function() {
+ var self = this;
+ (function pull () {
+ if (self.expectedUrl !== self.location.href) {
+ foreach(self.urlListeners, function(listener){
+ try {
+ listener(self.location.href);
+ } catch (e) {
+ error(e);
+ }
+ });
+ self.expectedUrl = self.location.href;
+ }
+ self.setTimeout(pull, self.delay);
+ })();
+ },
+
+ setUrl: function(url) {
+ var existingURL = this.location.href;
+ if (!existingURL.match(/#/)) existingURL += '#';
+ if (!url.match(/#/)) url += '#';
+ if (existingURL != url) {
+ this.location.href = this.expectedUrl = url;
+ }
+ },
+
+ getUrl: function() {
+ return this.location.href;
+ }
+};
diff --git a/src/Compiler.js b/src/Compiler.js
new file mode 100644
index 00000000..c8910c27
--- /dev/null
+++ b/src/Compiler.js
@@ -0,0 +1,212 @@
+/**
+ * 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 || 0;
+}
+
+Template.prototype = {
+ init: function(element, scope) {
+ var inits = {};
+ this.collectInits(element, inits);
+ foreachSorted(inits, function(queue){
+ foreach(queue, function(fn){
+ fn(scope);
+ });
+ });
+ },
+
+ collectInits: function(element, inits) {
+ var queue = inits[this.priority];
+ if (!queue) {
+ inits[this.priority] = queue = [];
+ }
+ element = jqLite(element);
+ foreach(this.inits, function(fn) {
+ queue.push(function(scope) {
+ scope.$tryEval(fn, 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);
+ }
+ },
+
+
+ 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;
+ }
+};
+
+///////////////////////////////////
+//Compiler
+//////////////////////////////////
+function Compiler(textMarkup, attrMarkup, directives, widgets){
+ this.textMarkup = textMarkup;
+ 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 || {}, angularService);
+ return extend(scope, {
+ $element:element,
+ $init: function() {
+ template.init(element, scope);
+ scope.$eval();
+ delete scope.$init;
+ return scope;
+ }
+ });
+ };
+ },
+
+ templatize: function(element, elementIndex, priority){
+ var self = this,
+ widget,
+ directiveFns = self.directives,
+ descend = true,
+ directives = true,
+ 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;}
+ };
+ priority = element.attr('ng-eval-order') || priority || 0;
+ if (isString(priority)) {
+ priority = PRIORITY[uppercase(priority)] || 0;
+ }
+ template = new Template(priority);
+ eachAttribute(element, function(value, name){
+ if (!widget) {
+ if (widget = self.widgets['@' + name]) {
+ widget = bind(selfApi, widget, value, element);
+ }
+ }
+ });
+ if (!widget) {
+ if (widget = self.widgets[nodeName(element)]) {
+ 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
+ eachTextNode(element, function(textNode){
+ var text = textNode.text();
+ foreach(self.textMarkup, function(markup){
+ markup.call(selfApi, 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){
+ template.addInit((directiveFns[name]||noop).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 eachTextNode(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 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.replace(':', '-');
+ value = attr.value;
+ if (msie && name == 'href') {
+ value = decodeURIComponent(element[0].getAttribute(name, 2));
+ }
+ attrValue[name] = value;
+ }
+ foreachSorted(attrValue, fn);
+}
+
diff --git a/src/JSON.js b/src/JSON.js
new file mode 100644
index 00000000..340b075a
--- /dev/null
+++ b/src/JSON.js
@@ -0,0 +1,105 @@
+array = [].constructor;
+
+function toJson(obj, pretty){
+ var buf = [];
+ toJsonArray(buf, obj, pretty ? "\n " : null, []);
+ return buf.join('');
+}
+
+function toPrettyJson(obj) {
+ return toJson(obj, true);
+}
+
+function fromJson(json) {
+ if (!json) return json;
+ try {
+ var parser = new Parser(json, true);
+ var expression = parser.primary();
+ parser.assertAllConsumed();
+ return expression();
+ } catch (e) {
+ error("fromJson error: ", json, e);
+ throw e;
+ }
+}
+
+angular['toJson'] = toJson;
+angular['fromJson'] = fromJson;
+
+function toJsonArray(buf, obj, pretty, stack){
+ if (typeof obj == "object") {
+ if (includes(stack, obj)) {
+ buf.push("RECURSION");
+ return;
+ }
+ stack.push(obj);
+ }
+ var type = typeof obj;
+ if (obj === null) {
+ buf.push("null");
+ } else if (type === 'function') {
+ return;
+ } else if (type === 'boolean') {
+ buf.push('' + obj);
+ } else if (type === 'number') {
+ if (isNaN(obj)) {
+ buf.push('null');
+ } else {
+ buf.push('' + obj);
+ }
+ } else if (type === 'string') {
+ return buf.push(angular['String']['quoteUnicode'](obj));
+ } else if (type === 'object') {
+ if (obj instanceof Array) {
+ 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 (typeof item == 'function' || typeof item == 'undefined') {
+ buf.push("null");
+ } else {
+ toJsonArray(buf, item, pretty, stack);
+ }
+ sep = true;
+ }
+ buf.push("]");
+ } else if (obj instanceof Date) {
+ 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 (k.indexOf('$$') === 0 || obj[k] === undefined)
+ continue;
+ keys.push(k);
+ }
+ keys.sort();
+ for ( var keyIndex = 0; keyIndex < keys.length; keyIndex++) {
+ var key = keys[keyIndex];
+ try {
+ 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;
+ }
+ } catch (e) {
+ }
+ }
+ buf.push("}");
+ }
+ }
+ if (typeof obj == "object") {
+ stack.pop();
+ }
+}
diff --git a/src/Parser.js b/src/Parser.js
new file mode 100644
index 00000000..df270792
--- /dev/null
+++ b/src/Parser.js
@@ -0,0 +1,730 @@
+function Lexer(text, parsStrings){
+ this.text = text;
+ // UTC dates have 20 characters, we send them through parser
+ this.dateParseLength = parsStrings ? 20 : -1;
+ this.tokens = [];
+ this.index = 0;
+}
+
+Lexer.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;},
+ '=':function(self, a,b){return setter(self, 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 a|b;},
+ '|':function(self, a,b){return b(self, a);},
+ '!':function(self, a){return !a;}
+};
+Lexer.ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
+
+Lexer.prototype = {
+ peek: function() {
+ if (this.index + 1 < this.text.length) {
+ return this.text.charAt(this.index + 1);
+ } else {
+ return false;
+ }
+ },
+
+ parse: function() {
+ var tokens = this.tokens;
+ var OPERATORS = Lexer.OPERATORS;
+ var canStartRegExp = true;
+ while (this.index < this.text.length) {
+ var ch = this.text.charAt(this.index);
+ if (ch == '"' || ch == "'") {
+ this.readString(ch);
+ canStartRegExp = true;
+ } else if (ch == '(' || ch == '[') {
+ tokens.push({index:this.index, text:ch});
+ this.index++;
+ } else if (ch == '{' ) {
+ var peekCh = this.peek();
+ if (peekCh == ':' || peekCh == '(') {
+ tokens.push({index:this.index, text:ch + peekCh});
+ this.index++;
+ } else {
+ tokens.push({index:this.index, text:ch});
+ }
+ this.index++;
+ canStartRegExp = true;
+ } else if (ch == ')' || ch == ']' || ch == '}' ) {
+ tokens.push({index:this.index, text:ch});
+ this.index++;
+ canStartRegExp = false;
+ } else if ( ch == ':' || ch == '.' || ch == ',' || ch == ';') {
+ tokens.push({index:this.index, text:ch});
+ this.index++;
+ canStartRegExp = true;
+ } else if ( canStartRegExp && ch == '/' ) {
+ this.readRegexp();
+ canStartRegExp = false;
+ } else if ( this.isNumber(ch) ) {
+ this.readNumber();
+ canStartRegExp = false;
+ } else if (this.isIdent(ch)) {
+ this.readIdent();
+ canStartRegExp = false;
+ } else if (this.isWhitespace(ch)) {
+ this.index++;
+ } else {
+ var ch2 = ch + this.peek();
+ var fn = OPERATORS[ch];
+ var fn2 = OPERATORS[ch2];
+ if (fn2) {
+ tokens.push({index:this.index, text:ch2, fn:fn2});
+ this.index += 2;
+ } else if (fn) {
+ tokens.push({index:this.index, text:ch, fn:fn});
+ this.index += 1;
+ } else {
+ throw "Lexer Error: Unexpected next character [" +
+ this.text.substring(this.index) +
+ "] in expression '" + this.text +
+ "' at column '" + (this.index+1) + "'.";
+ }
+ canStartRegExp = true;
+ }
+ }
+ return tokens;
+ },
+
+ isNumber: function(ch) {
+ return '0' <= ch && ch <= '9';
+ },
+
+ isWhitespace: function(ch) {
+ return ch == ' ' || ch == '\r' || ch == '\t' ||
+ ch == '\n' || ch == '\v';
+ },
+
+ isIdent: function(ch) {
+ return 'a' <= ch && ch <= 'z' ||
+ 'A' <= ch && ch <= 'Z' ||
+ '_' == ch || ch == '$';
+ },
+
+ readNumber: function() {
+ var number = "";
+ var start = this.index;
+ while (this.index < this.text.length) {
+ var ch = this.text.charAt(this.index);
+ if (ch == '.' || this.isNumber(ch)) {
+ number += ch;
+ } else {
+ break;
+ }
+ this.index++;
+ }
+ number = 1 * number;
+ this.tokens.push({index:start, text:number,
+ fn:function(){return number;}});
+ },
+
+ readIdent: function() {
+ var ident = "";
+ var start = this.index;
+ while (this.index < this.text.length) {
+ var ch = this.text.charAt(this.index);
+ if (ch == '.' || this.isIdent(ch) || this.isNumber(ch)) {
+ ident += ch;
+ } else {
+ break;
+ }
+ this.index++;
+ }
+ var fn = Lexer.OPERATORS[ident];
+ if (!fn) {
+ fn = getterFn(ident);
+ fn.isAssignable = ident;
+ }
+ this.tokens.push({index:start, text:ident, fn:fn});
+ },
+
+ readString: function(quote) {
+ var start = this.index;
+ var dateParseLength = this.dateParseLength;
+ this.index++;
+ var string = "";
+ var rawString = quote;
+ var escape = false;
+ while (this.index < this.text.length) {
+ var ch = this.text.charAt(this.index);
+ rawString += ch;
+ if (escape) {
+ if (ch == 'u') {
+ var hex = this.text.substring(this.index + 1, this.index + 5);
+ this.index += 4;
+ string += String.fromCharCode(parseInt(hex, 16));
+ } else {
+ var rep = Lexer.ESCAPE[ch];
+ if (rep) {
+ string += rep;
+ } else {
+ string += ch;
+ }
+ }
+ escape = false;
+ } else if (ch == '\\') {
+ escape = true;
+ } else if (ch == quote) {
+ this.index++;
+ this.tokens.push({index:start, text:rawString, string:string,
+ fn:function(){
+ return (string.length == dateParseLength) ?
+ angular['String']['toDate'](string) : string;
+ }});
+ return;
+ } else {
+ string += ch;
+ }
+ this.index++;
+ }
+ throw "Lexer Error: Unterminated quote [" +
+ this.text.substring(start) + "] starting at column '" +
+ (start+1) + "' in expression '" + this.text + "'.";
+ },
+
+ readRegexp: function(quote) {
+ var start = this.index;
+ this.index++;
+ var regexp = "";
+ var escape = false;
+ while (this.index < this.text.length) {
+ var ch = this.text.charAt(this.index);
+ if (escape) {
+ regexp += ch;
+ escape = false;
+ } else if (ch === '\\') {
+ regexp += ch;
+ escape = true;
+ } else if (ch === '/') {
+ this.index++;
+ var flags = "";
+ if (this.isIdent(this.text.charAt(this.index))) {
+ this.readIdent();
+ flags = this.tokens.pop().text;
+ }
+ var compiledRegexp = new RegExp(regexp, flags);
+ this.tokens.push({index:start, text:regexp, flags:flags,
+ fn:function(){return compiledRegexp;}});
+ return;
+ } else {
+ regexp += ch;
+ }
+ this.index++;
+ }
+ throw "Lexer Error: Unterminated RegExp [" +
+ this.text.substring(start) + "] starting at column '" +
+ (start+1) + "' in expression '" + this.text + "'.";
+ }
+};
+
+/////////////////////////////////////////
+
+function Parser(text, parseStrings){
+ this.text = text;
+ this.tokens = new Lexer(text, parseStrings).parse();
+ this.index = 0;
+}
+
+Parser.ZERO = function(){
+ return 0;
+};
+
+Parser.prototype = {
+ error: function(msg, token) {
+ throw "Token '" + token.text +
+ "' is " + msg + " at column='" +
+ (token.index + 1) + "' of expression '" +
+ this.text + "' starting at '" + this.text.substring(token.index) + "'.";
+ },
+
+ peekToken: function() {
+ if (this.tokens.length === 0)
+ throw "Unexpected end of expression: " + this.text;
+ return this.tokens[0];
+ },
+
+ peek: function(e1, e2, e3, e4) {
+ var tokens = this.tokens;
+ 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;
+ },
+
+ expect: function(e1, e2, e3, e4){
+ var token = this.peek(e1, e2, e3, e4);
+ if (token) {
+ this.tokens.shift();
+ this.currentToken = token;
+ return token;
+ }
+ return false;
+ },
+
+ consume: function(e1){
+ if (!this.expect(e1)) {
+ var token = this.peek();
+ throw "Expecting '" + e1 + "' at column '" +
+ (token.index+1) + "' in '" +
+ this.text + "' got '" +
+ this.text.substring(token.index) + "'.";
+ }
+ },
+
+ _unary: function(fn, right) {
+ return function(self) {
+ return fn(self, right(self));
+ };
+ },
+
+ _binary: function(left, fn, right) {
+ return function(self) {
+ return fn(self, left(self), right(self));
+ };
+ },
+
+ hasTokens: function () {
+ return this.tokens.length > 0;
+ },
+
+ assertAllConsumed: function(){
+ if (this.tokens.length !== 0) {
+ throw "Did not understand '" + this.text.substring(this.tokens[0].index) +
+ "' while evaluating '" + this.text + "'.";
+ }
+ },
+
+ statements: function(){
+ var statements = [];
+ while(true) {
+ if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']'))
+ statements.push(this.filterChain());
+ if (!this.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;
+ };
+ }
+ }
+ },
+
+ filterChain: function(){
+ var left = this.expression();
+ var token;
+ while(true) {
+ if ((token = this.expect('|'))) {
+ left = this._binary(left, token.fn, this.filter());
+ } else {
+ return left;
+ }
+ }
+ },
+
+ filter: function(){
+ return this._pipeFunction(angularFilter);
+ },
+
+ validator: function(){
+ return this._pipeFunction(angularValidator);
+ },
+
+ _pipeFunction: function(fnScope){
+ var fn = this.functionIdent(fnScope);
+ var argsFn = [];
+ var token;
+ while(true) {
+ if ((token = this.expect(':'))) {
+ argsFn.push(this.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;
+ };
+ }
+ }
+ },
+
+ expression: function(){
+ return this.throwStmt();
+ },
+
+ throwStmt: function(){
+ if (this.expect('throw')) {
+ var throwExp = this.assignment();
+ return function (self) {
+ throw throwExp(self);
+ };
+ } else {
+ return this.assignment();
+ }
+ },
+
+ assignment: function(){
+ var left = this.logicalOR();
+ var token;
+ if (token = this.expect('=')) {
+ if (!left.isAssignable) {
+ throw "Left hand side '" +
+ this.text.substring(0, token.index) + "' of assignment '" +
+ this.text.substring(token.index) + "' is not assignable.";
+ }
+ var ident = function(){return left.isAssignable;};
+ return this._binary(ident, token.fn, this.logicalOR());
+ } else {
+ return left;
+ }
+ },
+
+ logicalOR: function(){
+ var left = this.logicalAND();
+ var token;
+ while(true) {
+ if ((token = this.expect('||'))) {
+ left = this._binary(left, token.fn, this.logicalAND());
+ } else {
+ return left;
+ }
+ }
+ },
+
+ logicalAND: function(){
+ var left = this.equality();
+ var token;
+ if ((token = this.expect('&&'))) {
+ left = this._binary(left, token.fn, this.logicalAND());
+ }
+ return left;
+ },
+
+ equality: function(){
+ var left = this.relational();
+ var token;
+ if ((token = this.expect('==','!='))) {
+ left = this._binary(left, token.fn, this.equality());
+ }
+ return left;
+ },
+
+ relational: function(){
+ var left = this.additive();
+ var token;
+ if (token = this.expect('<', '>', '<=', '>=')) {
+ left = this._binary(left, token.fn, this.relational());
+ }
+ return left;
+ },
+
+ additive: function(){
+ var left = this.multiplicative();
+ var token;
+ while(token = this.expect('+','-')) {
+ left = this._binary(left, token.fn, this.multiplicative());
+ }
+ return left;
+ },
+
+ multiplicative: function(){
+ var left = this.unary();
+ var token;
+ while(token = this.expect('*','/','%')) {
+ left = this._binary(left, token.fn, this.unary());
+ }
+ return left;
+ },
+
+ unary: function(){
+ var token;
+ if (this.expect('+')) {
+ return this.primary();
+ } else if (token = this.expect('-')) {
+ return this._binary(Parser.ZERO, token.fn, this.unary());
+ } else if (token = this.expect('!')) {
+ return this._unary(token.fn, this.unary());
+ } else {
+ return this.primary();
+ }
+ },
+
+ functionIdent: function(fnScope) {
+ var token = this.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') {
+ throw "Function '" + token.text + "' at column '" +
+ (token.index+1) + "' in '" + this.text + "' is not defined.";
+ }
+ return instance;
+ },
+
+ primary: function() {
+ var primary;
+ if (this.expect('(')) {
+ var expression = this.filterChain();
+ this.consume(')');
+ primary = expression;
+ } else if (this.expect('[')) {
+ primary = this.arrayDeclaration();
+ } else if (this.expect('{')) {
+ primary = this.object();
+ } else if (this.expect('{:')) {
+ primary = this.closure(false);
+ } else if (this.expect('{(')) {
+ primary = this.closure(true);
+ } else {
+ var token = this.expect();
+ primary = token.fn;
+ if (!primary) {
+ this.error("not a primary expression", token);
+ }
+ }
+ var next;
+ while (next = this.expect('(', '[', '.')) {
+ if (next.text === '(') {
+ primary = this.functionCall(primary);
+ } else if (next.text === '[') {
+ primary = this.objectIndex(primary);
+ } else if (next.text === '.') {
+ primary = this.fieldAccess(primary);
+ } else {
+ throw "IMPOSSIBLE";
+ }
+ }
+ return primary;
+ },
+
+ closure: function(hasArgs) {
+ var args = [];
+ if (hasArgs) {
+ if (!this.expect(')')) {
+ args.push(this.expect().text);
+ while(this.expect(',')) {
+ args.push(this.expect().text);
+ }
+ this.consume(')');
+ }
+ this.consume(":");
+ }
+ var statements = this.statements();
+ this.consume("}");
+ return function(self) {
+ return function($){
+ var scope = createScope(self);
+ scope['$'] = $;
+ for ( var i = 0; i < args.length; i++) {
+ setter(scope, args[i], arguments[i]);
+ }
+ return statements(scope);
+ };
+ };
+ },
+
+ fieldAccess: function(object) {
+ var field = this.expect().text;
+ var getter = getterFn(field);
+ var fn = function (self){
+ return getter(object(self));
+ };
+ fn.isAssignable = field;
+ return fn;
+ },
+
+ objectIndex: function(obj) {
+ var indexFn = this.expression();
+ this.consume(']');
+ if (this.expect('=')) {
+ var rhs = this.expression();
+ return function (self){
+ return obj(self)[indexFn(self)] = rhs(self);
+ };
+ } else {
+ return function (self){
+ var o = obj(self);
+ var i = indexFn(self);
+ return (o) ? o[i] : undefined;
+ };
+ }
+ },
+
+ functionCall: function(fn) {
+ var argsFn = [];
+ if (this.peekToken().text != ')') {
+ do {
+ argsFn.push(this.expression());
+ } while (this.expect(','));
+ }
+ this.consume(')');
+ return function (self){
+ var args = [];
+ for ( var i = 0; i < argsFn.length; i++) {
+ args.push(argsFn[i](self));
+ }
+ var fnPtr = fn(self);
+ if (typeof fnPtr === 'function') {
+ return fnPtr.apply(self, args);
+ } else {
+ throw "Expression '" + fn.isAssignable + "' is not a function.";
+ }
+ };
+ },
+
+ // This is used with json array declaration
+ arrayDeclaration: function () {
+ var elementFns = [];
+ if (this.peekToken().text != ']') {
+ do {
+ elementFns.push(this.expression());
+ } while (this.expect(','));
+ }
+ this.consume(']');
+ return function (self){
+ var array = [];
+ for ( var i = 0; i < elementFns.length; i++) {
+ array.push(elementFns[i](self));
+ }
+ return array;
+ };
+ },
+
+ object: function () {
+ var keyValues = [];
+ if (this.peekToken().text != '}') {
+ do {
+ var token = this.expect(),
+ key = token.string || token.text;
+ this.consume(":");
+ var value = this.expression();
+ keyValues.push({key:key, value:value});
+ } while (this.expect(','));
+ }
+ this.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;
+ };
+ },
+
+ entityDeclaration: function () {
+ var decl = [];
+ while(this.hasTokens()) {
+ decl.push(this.entityDecl());
+ if (!this.expect(';')) {
+ this.assertAllConsumed();
+ }
+ }
+ return function (self){
+ var code = "";
+ for ( var i = 0; i < decl.length; i++) {
+ code += decl[i](self);
+ }
+ return code;
+ };
+ },
+
+ entityDecl: function () {
+ var entity = this.expect().text;
+ var instance;
+ var defaults;
+ if (this.expect('=')) {
+ instance = entity;
+ entity = this.expect().text;
+ }
+ if (this.expect(':')) {
+ defaults = this.primary()(null);
+ }
+ return function(self) {
+ var Entity = self.datastore.entity(entity, defaults);
+ setter(self, entity, Entity);
+ if (instance) {
+ var document = Entity();
+ document['$$anchor'] = instance;
+ setter(self, instance, document);
+ return "$anchor." + instance + ":{" +
+ instance + "=" + entity + ".load($anchor." + instance + ");" +
+ instance + ".$$anchor=" + angular['String']['quote'](instance) + ";" +
+ "};";
+ } else {
+ return "";
+ }
+ };
+ },
+
+ watch: function () {
+ var decl = [];
+ while(this.hasTokens()) {
+ decl.push(this.watchDecl());
+ if (!this.expect(';')) {
+ this.assertAllConsumed();
+ }
+ }
+ this.assertAllConsumed();
+ return function (self){
+ for ( var i = 0; i < decl.length; i++) {
+ var d = decl[i](self);
+ self.addListener(d.name, d.fn);
+ }
+ };
+ },
+
+ watchDecl: function () {
+ var anchorName = this.expect().text;
+ this.consume(":");
+ var expression;
+ if (this.peekToken().text == '{') {
+ this.consume("{");
+ expression = this.statements();
+ this.consume("}");
+ } else {
+ expression = this.expression();
+ }
+ return function(self) {
+ return {name:anchorName, fn:expression};
+ };
+ }
+};
+
diff --git a/src/Resource.js b/src/Resource.js
new file mode 100644
index 00000000..ba460c30
--- /dev/null
+++ b/src/Resource.js
@@ -0,0 +1,140 @@
+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));
+ }
+ });
+ 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 isGet = action.method == 'GET';
+ var isPost = action.method == 'POST';
+ 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 (isPost) 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 = 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) {
+ if (action.cacheThenRetrieve)
+ value = [];
+ 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.cacheThenRetrieve
+ );
+ return value;
+ };
+
+ Resource.bind = function(additionalParamDefaults){
+ return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions);
+ };
+
+ if (!isGet) {
+ Resource.prototype['$' + name] = function(a1, a2){
+ var params = {};
+ 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 self = this;
+ Resource[name](params, this, function(response){
+ copy(response, self);
+ callback(self);
+ });
+ };
+ }
+ });
+ return Resource;
+ }
+};
diff --git a/src/Scope.js b/src/Scope.js
new file mode 100644
index 00000000..637fc25e
--- /dev/null
+++ b/src/Scope.js
@@ -0,0 +1,224 @@
+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) && !instance['$$factory']) {
+ 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 getterFnCache = {};
+function getterFn(path){
+ var fn = getterFnCache[path];
+ if (fn) return fn;
+
+ var code = 'function (self){\n';
+ code += ' var last, fn, type;\n';
+ foreach(path.split('.'), function(key) {
+ key = (key == 'this') ? '["this"]' : '.' + key;
+ code += ' if(!self) return self;\n';
+ code += ' last = self;\n';
+ code += ' self = self' + key + ';\n';
+ code += ' if(typeof self == "function") \n';
+ code += ' self = function(){ return last'+key+'.apply(last, arguments); };\n';
+ if (key.charAt(1) == '$') {
+ // special code for super-imposed functions
+ var name = key.substr(2);
+ code += ' if(!self) {\n';
+ code += ' type = angular.Global.typeOf(last);\n';
+ code += ' fn = (angular[type.charAt(0).toUpperCase() + type.substring(1)]||{})["' + name + '"];\n';
+ code += ' if (fn)\n';
+ code += ' self = function(){ return fn.apply(last, [last].concat(slice.call(arguments, 0, arguments.length))); };\n';
+ code += ' }\n';
+ }
+ });
+ code += ' return self;\n}';
+ fn = eval('(' + code + ')');
+ fn.toString = function(){ return code; };
+
+ return getterFnCache[path] = fn;
+}
+
+///////////////////////////////////
+
+var compileCache = {};
+function expressionCompile(exp){
+ if (isFunction(exp)) return exp;
+ var fn = compileCache[exp];
+ if (!fn) {
+ var parser = new Parser(exp);
+ var fnSelf = parser.statements();
+ parser.assertAllConsumed();
+ fn = compileCache[exp] = extend(
+ function(){ return fnSelf(this);},
+ {fnSelf: fnSelf});
+ }
+ return fn;
+}
+
+function rethrow(e) { throw e; }
+function errorHandlerFor(element, error) {
+ elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error);
+}
+
+var scopeId = 0;
+function createScope(parent, services, existing) {
+ function Parent(){}
+ function API(){}
+ function Behavior(){}
+
+ var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing);
+
+ parent = Parent.prototype = (parent || {});
+ api = API.prototype = new Parent();
+ behavior = Behavior.prototype = new API();
+ instance = new Behavior();
+
+ extend(api, {
+ 'this': instance,
+ $id: (scopeId++),
+ $parent: parent,
+ $bind: bind(instance, bind, instance),
+ $get: bind(instance, getter, instance),
+ $set: bind(instance, setter, instance),
+
+ $eval: function $eval(exp) {
+ if (exp !== undefined) {
+ return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));
+ } else {
+ for ( var i = 0, iSize = evalLists.sorted.length; i < iSize; i++) {
+ for ( var queue = evalLists.sorted[i],
+ jSize = queue.length,
+ j= 0; j < jSize; j++) {
+ instance.$tryEval(queue[j].fn, queue[j].handler);
+ }
+ }
+ }
+ },
+
+ $tryEval: function (expression, exceptionHandler) {
+ try {
+ return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length));
+ } catch (e) {
+ error(e);
+ if (isFunction(exceptionHandler)) {
+ exceptionHandler(e);
+ } else if (exceptionHandler) {
+ errorHandlerFor(exceptionHandler, e);
+ } else if (isFunction(instance.$exceptionHandler)) {
+ instance.$exceptionHandler(e);
+ }
+ }
+ },
+
+ $watch: function(watchExp, listener, exceptionHandler) {
+ var watch = expressionCompile(watchExp),
+ last;
+ function watcher(){
+ var value = watch.call(instance),
+ lastValue = last;
+ if (last !== value) {
+ last = value;
+ instance.$tryEval(listener, exceptionHandler, value, lastValue);
+ }
+ }
+ instance.$onEval(PRIORITY_WATCH, watcher);
+ watcher();
+ },
+
+ $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
+ });
+ },
+
+ $become: function(Class) {
+ // remove existing
+ foreach(behavior, function(value, key){ delete behavior[key]; });
+ foreach((Class || noop).prototype, function(fn, name){
+ behavior[name] = bind(instance, fn);
+ });
+ (Class || noop).call(instance);
+ }
+
+ });
+
+ if (!parent.$root) {
+ api.$root = instance;
+ api.$parent = instance;
+ }
+
+ function inject(name){
+ var service = servicesCache[name], factory, args = [];
+ if (isUndefined(service)) {
+ factory = services[name];
+ if (!isFunction(factory))
+ throw "Don't know how to inject '" + name + "'.";
+ foreach(factory.inject, function(dependency){
+ args.push(inject(dependency));
+ });
+ servicesCache[name] = service = factory.apply(instance, args);
+ }
+ return service;
+ }
+
+ foreach(services, function(_, name){
+ var service = inject(name);
+ if (service) {
+ setter(instance, name, service);
+ }
+ });
+
+ return instance;
+}
diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js
new file mode 100644
index 00000000..90e1104e
--- /dev/null
+++ b/src/angular-bootstrap.js
@@ -0,0 +1,70 @@
+/**
+ * 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(previousOnLoad){
+ var filename = /(.*)\/angular-(.*).js(#(.*))?/,
+ scripts = document.getElementsByTagName("SCRIPT"),
+ serverPath,
+ config,
+ match;
+ for(var j = 0; j < scripts.length; j++) {
+ match = (scripts[j].src || "").match(filename);
+ if (match) {
+ serverPath = match[1];
+ config = match[4];
+ }
+ }
+
+ function addScript(file){
+ document.write('<script type="text/javascript" src="' + serverPath + file +'"></script>');
+ }
+
+ addScript("/Angular.js");
+ addScript("/JSON.js");
+ addScript("/Compiler.js");
+ addScript("/Scope.js");
+ addScript("/jqLite.js");
+ addScript("/Parser.js");
+ addScript("/Resource.js");
+ addScript("/Browser.js");
+ addScript("/AngularPublic.js");
+
+ // Extension points
+ addScript("/services.js");
+ addScript("/apis.js");
+ addScript("/filters.js");
+ addScript("/formatters.js");
+ addScript("/validators.js");
+ addScript("/directives.js");
+ addScript("/markups.js");
+ addScript("/widgets.js");
+
+ window.onload = function(){
+ try {
+ if (previousOnLoad) previousOnLoad();
+ } catch(e) {}
+ angularInit(parseKeyValue(config));
+ };
+
+})(window.onload);
+
diff --git a/src/angular.prefix b/src/angular.prefix
new file mode 100644
index 00000000..a1b4e151
--- /dev/null
+++ b/src/angular.prefix
@@ -0,0 +1,24 @@
+/**
+ * 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){
diff --git a/src/angular.suffix b/src/angular.suffix
new file mode 100644
index 00000000..36d73df2
--- /dev/null
+++ b/src/angular.suffix
@@ -0,0 +1,9 @@
+
+ window.onload = function(){
+ try {
+ if (previousOnLoad) previousOnLoad();
+ } catch(e) {}
+ angularInit(parseKeyValue(angularJsConfig(document)));
+ };
+
+})(window, document, window.onload);
diff --git a/src/apis.js b/src/apis.js
new file mode 100644
index 00000000..306d0ce8
--- /dev/null
+++ b/src/apis.js
@@ -0,0 +1,338 @@
+var angularGlobal = {
+ 'typeOf':function(obj){
+ if (obj === null) return "null";
+ var type = typeof obj;
+ if (type == "object") {
+ if (obj instanceof Array) return "array";
+ if (obj instanceof Date) return "date";
+ if (obj.nodeType == 1) return "element";
+ }
+ return type;
+ }
+};
+
+var angularCollection = {
+ 'size': size
+};
+var angularObject = {
+ 'extend': extend
+};
+var angularArray = {
+ 'indexOf': indexOf,
+ 'include': includes,
+ 'includeIf':function(array, value, condition) {
+ var index = indexOf(array, value);
+ if (condition) {
+ if (index == -1)
+ array.push(value);
+ } else {
+ array.splice(index, 1);
+ }
+ return array;
+ },
+ '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;
+ },
+ 'remove':function(array, value) {
+ var index = indexOf(array, value);
+ if (index >=0)
+ array.splice(index, 1);
+ return value;
+ },
+ 'find':function(array, condition, defaultValue) {
+ if (!condition) return undefined;
+ var fn = angular['Function']['compile'](condition);
+ foreach(array, function($){
+ if (fn($)){
+ defaultValue = $;
+ return true;
+ }
+ });
+ return defaultValue;
+ },
+ 'findById':function(array, id) {
+ return angular.Array.find(array, function($){return $.$id == id;}, null);
+ },
+ '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;
+ },
+ 'add':function(array, value) {
+ array.push(isUndefined(value)? {} : value);
+ return array;
+ },
+ '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;
+ },
+ 'orderBy':function(array, expression, descend) {
+ 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;
+ }
+ }
+ expression = isArray(expression) ? expression: [expression];
+ expression = map(expression, function($){
+ var descending = false;
+ if (typeof $ == "string" && ($.charAt(0) == '+' || $.charAt(0) == '-')) {
+ descending = $.charAt(0) == '-';
+ $ = $.substring(1);
+ }
+ var get = $ ? expressionCompile($).fnSelf : identity;
+ return reverse(function(a,b){
+ return compare(get(a),get(b));
+ }, descending);
+ });
+ var comparator = function(o1, o2){
+ for ( var i = 0; i < expression.length; i++) {
+ var comp = expression[i](o1, o2);
+ if (comp !== 0) return comp;
+ }
+ return 0;
+ };
+ var arrayCopy = [];
+ for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); }
+ return arrayCopy.sort(reverse(comparator, descend));
+ },
+ 'orderByToggle':function(predicate, attribute) {
+ var STRIP = /^([+|-])?(.*)/;
+ var ascending = false;
+ var index = -1;
+ foreach(predicate, function($, i){
+ if (index == -1) {
+ if ($ == attribute) {
+ ascending = true;
+ index = i;
+ return true;
+ }
+ if (($.charAt(0)=='+'||$.charAt(0)=='-') && $.substring(1) == attribute) {
+ ascending = $.charAt(0) == '+';
+ index = i;
+ return true;
+ }
+ }
+ });
+ if (index >= 0) {
+ predicate.splice(index, 1);
+ }
+ predicate.unshift((ascending ? "-" : "+") + attribute);
+ return predicate;
+ },
+ 'orderByDirection':function(predicate, attribute, ascend, descend) {
+ ascend = ascend || 'ng-ascend';
+ descend = descend || 'ng-descend';
+ var att = predicate[0] || '';
+ var direction = true;
+ if (att.charAt(0) == '-') {
+ att = att.substring(1);
+ direction = false;
+ } else if(att.charAt(0) == '+') {
+ att = att.substring(1);
+ }
+ return att == attribute ? (direction ? ascend : descend) : "";
+ },
+ 'merge':function(array, index, mergeValue) {
+ var value = array[index];
+ if (!value) {
+ value = {};
+ array[index] = value;
+ }
+ merge(mergeValue, value);
+ return array;
+ }
+};
+
+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('');
+ },
+ 'toDate':function(string){
+ var match;
+ if (typeof string == 'string' &&
+ (match = string.match(/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/))){
+ var date = new Date(0);
+ date.setUTCFullYear(match[1], match[2] - 1, match[3]);
+ date.setUTCHours(match[4], match[5], match[6], 0);
+ return date;
+ }
+ return string;
+ }
+};
+
+var angularDate = {
+ 'toString':function(date){
+ function pad(n) { return n < 10 ? "0" + n : n; }
+ return !date ? date :
+ date.getUTCFullYear() + '-' +
+ pad(date.getUTCMonth() + 1) + '-' +
+ pad(date.getUTCDate()) + 'T' +
+ pad(date.getUTCHours()) + ':' +
+ pad(date.getUTCMinutes()) + ':' +
+ pad(date.getUTCSeconds()) + '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, underscoreNames){
+ if (_) {
+ var lastChain = _.last(chain);
+ foreach(underscoreNames, function(name){
+ lastChain[name] = _[name];
+ });
+ }
+ angular[dst] = angular[dst] || {};
+ foreach(chain, function(parent){
+ extend(angular[dst], parent);
+ });
+}
+defineApi('Global', [angularGlobal],
+ ['extend', 'clone','isEqual',
+ 'isElement', 'isArray', 'isFunction', 'isUndefined']);
+defineApi('Collection', [angularGlobal, angularCollection],
+ ['each', 'map', 'reduce', 'reduceRight', 'detect',
+ 'select', 'reject', 'all', 'any', 'include',
+ 'invoke', 'pluck', 'max', 'min', 'sortBy',
+ 'sortedIndex', 'toArray', 'size']);
+defineApi('Array', [angularGlobal, angularCollection, angularArray],
+ ['first', 'last', 'compact', 'flatten', 'without',
+ 'uniq', 'intersect', 'zip', 'indexOf', 'lastIndexOf']);
+defineApi('Object', [angularGlobal, angularCollection, angularObject],
+ ['keys', 'values']);
+defineApi('String', [angularGlobal, angularString], []);
+defineApi('Date', [angularGlobal, angularDate], []);
+//IE bug
+angular['Date']['toString'] = angularDate['toString'];
+defineApi('Function', [angularGlobal, angularCollection, angularFunction],
+ ['bind', 'bindAll', 'delay', 'defer', 'wrap', 'compose']);
diff --git a/src/delete/Binder.js b/src/delete/Binder.js
new file mode 100644
index 00000000..9fc32513
--- /dev/null
+++ b/src/delete/Binder.js
@@ -0,0 +1,356 @@
+function Binder(doc, widgetFactory, datastore, location, config) {
+ this.doc = doc;
+ this.location = location;
+ this.datastore = datastore;
+ this.anchor = {};
+ this.widgetFactory = widgetFactory;
+ this.config = config || {};
+ this.updateListeners = [];
+}
+
+Binder.parseBindings = function(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;
+};
+
+Binder.hasBinding = function(string) {
+ var bindings = Binder.parseBindings(string);
+ return bindings.length > 1 || Binder.binding(bindings[0]) !== null;
+};
+
+Binder.binding = function(string) {
+ var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/);
+ return binding ? binding[1] : null;
+};
+
+
+Binder.prototype = {
+ parseQueryString: function(query) {
+ var params = {};
+ query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,
+ function (match, left, right) {
+ if (left) params[decodeURIComponent(left)] = decodeURIComponent(right);
+ });
+ return params;
+ },
+
+ parseAnchor: function() {
+ var self = this, url = this.location['get']() || "";
+
+ var anchorIndex = url.indexOf('#');
+ if (anchorIndex < 0) return;
+ var anchor = url.substring(anchorIndex + 1);
+
+ var anchorQuery = this.parseQueryString(anchor);
+ foreach(self.anchor, function(newValue, key) {
+ delete self.anchor[key];
+ });
+ foreach(anchorQuery, function(newValue, key) {
+ self.anchor[key] = newValue;
+ });
+ },
+
+ onUrlChange: function() {
+ this.parseAnchor();
+ this.updateView();
+ },
+
+ updateAnchor: function() {
+ var url = this.location['get']() || "";
+ var anchorIndex = url.indexOf('#');
+ if (anchorIndex > -1)
+ url = url.substring(0, anchorIndex);
+ url += "#";
+ var sep = '';
+ for (var key in this.anchor) {
+ var value = this.anchor[key];
+ if (typeof value === 'undefined' || value === null) {
+ delete this.anchor[key];
+ } else {
+ url += sep + encodeURIComponent(key);
+ if (value !== true)
+ url += "=" + encodeURIComponent(value);
+ sep = '&';
+ }
+ }
+ this.location['set'](url);
+ return url;
+ },
+
+ updateView: function() {
+ var start = new Date().getTime();
+ var scope = jQuery(this.doc).scope();
+ scope.clearInvalid();
+ scope.updateView();
+ var end = new Date().getTime();
+ this.updateAnchor();
+ foreach(this.updateListeners, function(fn) {fn();});
+ },
+
+ docFindWithSelf: function(exp){
+ var doc = jQuery(this.doc);
+ var selection = doc.find(exp);
+ if (doc.is(exp)){
+ selection = selection.andSelf();
+ }
+ return selection;
+ },
+
+ executeInit: function() {
+ this.docFindWithSelf("[ng-init]").each(function() {
+ var jThis = jQuery(this);
+ var scope = jThis.scope();
+ try {
+ scope.eval(jThis.attr('ng-init'));
+ } catch (e) {
+ alert("EVAL ERROR:\n" + jThis.attr('ng-init') + '\n' + toJson(e, true));
+ }
+ });
+ },
+
+ entity: function (scope) {
+ var self = this;
+ this.docFindWithSelf("[ng-entity]").attr("ng-watch", function() {
+ try {
+ var jNode = jQuery(this);
+ var decl = scope.entity(jNode.attr("ng-entity"), self.datastore);
+ return decl + (jNode.attr('ng-watch') || "");
+ } catch (e) {
+ log(e);
+ alert(e);
+ }
+ });
+ },
+
+ compile: function() {
+ var jNode = jQuery(this.doc);
+ if (this.config['autoSubmit']) {
+ var submits = this.docFindWithSelf(":submit").not("[ng-action]");
+ submits.attr("ng-action", "$save()");
+ submits.not(":disabled").not("ng-bind-attr").attr("ng-bind-attr", '{disabled:"{{$invalidWidgets}}"}');
+ }
+ this.precompile(this.doc)(this.doc, jNode.scope(), "");
+ this.docFindWithSelf("a[ng-action]").live('click', function (event) {
+ var jNode = jQuery(this);
+ var scope = jNode.scope();
+ try {
+ scope.eval(jNode.attr('ng-action'));
+ jNode.removeAttr('ng-error');
+ jNode.removeClass("ng-exception");
+ } catch (e) {
+ jNode.addClass("ng-exception");
+ jNode.attr('ng-error', toJson(e, true));
+ }
+ scope.get('$updateView')();
+ return false;
+ });
+ },
+
+ translateBinding: function(node, parentPath, factories) {
+ var path = parentPath.concat();
+ var offset = path.pop();
+ var parts = Binder.parseBindings(node.nodeValue);
+ if (parts.length > 1 || Binder.binding(parts[0])) {
+ var parent = node.parentNode;
+ if (isLeafNode(parent)) {
+ parent.setAttribute('ng-bind-template', node.nodeValue);
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ return new BindUpdater(node, node.getAttribute('ng-bind-template'));
+ }});
+ } else {
+ for (var i = 0; i < parts.length; i++) {
+ var part = parts[i];
+ var binding = Binder.binding(part);
+ var newNode;
+ if (binding) {
+ newNode = document.createElement("span");
+ var jNewNode = jQuery(newNode);
+ jNewNode.attr("ng-bind", binding);
+ if (i === 0) {
+ factories.push({path:path.concat(offset + i), fn:this.ng_bind});
+ }
+ } else if (msie && part.charAt(0) == ' ') {
+ newNode = document.createElement("span");
+ newNode.innerHTML = '&nbsp;' + part.substring(1);
+ } else {
+ newNode = document.createTextNode(part);
+ }
+ parent.insertBefore(newNode, node);
+ }
+ }
+ parent.removeChild(node);
+ }
+ },
+
+ precompile: function(root) {
+ var factories = [];
+ this.precompileNode(root, [], factories);
+ return function (template, scope, prefix) {
+ var len = factories.length;
+ for (var i = 0; i < len; i++) {
+ var factory = factories[i];
+ var node = template;
+ var path = factory.path;
+ for (var j = 0; j < path.length; j++) {
+ node = node.childNodes[path[j]];
+ }
+ try {
+ scope.addWidget(factory.fn(node, scope, prefix));
+ } catch (e) {
+ alert(e);
+ }
+ }
+ };
+ },
+
+ precompileNode: function(node, path, factories) {
+ var nodeType = node.nodeType;
+ if (nodeType == Node.TEXT_NODE) {
+ this.translateBinding(node, path, factories);
+ return;
+ } else if (nodeType != Node.ELEMENT_NODE && nodeType != Node.DOCUMENT_NODE) {
+ return;
+ }
+
+ if (!node.getAttribute) return;
+ var nonBindable = node.getAttribute('ng-non-bindable');
+ if (nonBindable || nonBindable === "") return;
+
+ var attributes = node.attributes;
+ if (attributes) {
+ var bindings = node.getAttribute('ng-bind-attr');
+ node.removeAttribute('ng-bind-attr');
+ bindings = bindings ? fromJson(bindings) : {};
+ var attrLen = attributes.length;
+ for (var i = 0; i < attrLen; i++) {
+ var attr = attributes[i];
+ var attrName = attr.name;
+ // http://www.glennjones.net/Post/809/getAttributehrefbug.htm
+ var attrValue = msie && attrName == 'href' ?
+ decodeURI(node.getAttribute(attrName, 2)) : attr.value;
+ if (Binder.hasBinding(attrValue)) {
+ bindings[attrName] = attrValue;
+ }
+ }
+ var json = toJson(bindings);
+ if (json.length > 2) {
+ node.setAttribute("ng-bind-attr", json);
+ }
+ }
+
+ if (!node.getAttribute) log(node);
+ var repeaterExpression = node.getAttribute('ng-repeat');
+ if (repeaterExpression) {
+ node.removeAttribute('ng-repeat');
+ var precompiled = this.precompile(node);
+ var view = document.createComment("ng-repeat: " + repeaterExpression);
+ var parentNode = node.parentNode;
+ parentNode.insertBefore(view, node);
+ parentNode.removeChild(node);
+ function template(childScope, prefix, i) {
+ var clone = jQuery(node).clone();
+ clone.css('display', '');
+ clone.attr('ng-repeat-index', "" + i);
+ clone.data('scope', childScope);
+ precompiled(clone[0], childScope, prefix + i + ":");
+ return clone;
+ }
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ return new RepeaterUpdater(jQuery(node), repeaterExpression, template, prefix);
+ }});
+ return;
+ }
+
+ if (node.getAttribute('ng-eval')) factories.push({path:path, fn:this.ng_eval});
+ if (node.getAttribute('ng-bind')) factories.push({path:path, fn:this.ng_bind});
+ if (node.getAttribute('ng-bind-attr')) factories.push({path:path, fn:this.ng_bind_attr});
+ if (node.getAttribute('ng-hide')) factories.push({path:path, fn:this.ng_hide});
+ if (node.getAttribute('ng-show')) factories.push({path:path, fn:this.ng_show});
+ if (node.getAttribute('ng-class')) factories.push({path:path, fn:this.ng_class});
+ if (node.getAttribute('ng-class-odd')) factories.push({path:path, fn:this.ng_class_odd});
+ if (node.getAttribute('ng-class-even')) factories.push({path:path, fn:this.ng_class_even});
+ if (node.getAttribute('ng-style')) factories.push({path:path, fn:this.ng_style});
+ if (node.getAttribute('ng-watch')) factories.push({path:path, fn:this.ng_watch});
+ var nodeName = node.nodeName;
+ if ((nodeName == 'INPUT' ) ||
+ nodeName == 'TEXTAREA' ||
+ nodeName == 'SELECT' ||
+ nodeName == 'BUTTON') {
+ var self = this;
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ node.name = prefix + node.name.split(":").pop();
+ return self.widgetFactory.createController(jQuery(node), scope);
+ }});
+ }
+ if (nodeName == 'OPTION') {
+ var html = jQuery('<select/>').append(jQuery(node).clone()).html();
+ if (!html.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\s*option\s*>/gi)) {
+ if (Binder.hasBinding(node.text)) {
+ jQuery(node).attr('ng-bind-attr', angular.toJson({'value':node.text}));
+ } else {
+ node.value = node.text;
+ }
+ }
+ }
+
+ var children = node.childNodes;
+ for (var k = 0; k < children.length; k++) {
+ this.precompileNode(children[k], path.concat(k), factories);
+ }
+ },
+
+ ng_eval: function(node) {
+ return new EvalUpdater(node, node.getAttribute('ng-eval'));
+ },
+
+ ng_bind: function(node) {
+ return new BindUpdater(node, "{{" + node.getAttribute('ng-bind') + "}}");
+ },
+
+ ng_bind_attr: function(node) {
+ return new BindAttrUpdater(node, fromJson(node.getAttribute('ng-bind-attr')));
+ },
+
+ ng_hide: function(node) {
+ return new HideUpdater(node, node.getAttribute('ng-hide'));
+ },
+
+ ng_show: function(node) {
+ return new ShowUpdater(node, node.getAttribute('ng-show'));
+ },
+
+ ng_class: function(node) {
+ return new ClassUpdater(node, node.getAttribute('ng-class'));
+ },
+
+ ng_class_even: function(node) {
+ return new ClassEvenUpdater(node, node.getAttribute('ng-class-even'));
+ },
+
+ ng_class_odd: function(node) {
+ return new ClassOddUpdater(node, node.getAttribute('ng-class-odd'));
+ },
+
+ ng_style: function(node) {
+ return new StyleUpdater(node, node.getAttribute('ng-style'));
+ },
+
+ ng_watch: function(node, scope) {
+ scope.watch(node.getAttribute('ng-watch'));
+ }
+};
diff --git a/src/delete/Model.js b/src/delete/Model.js
new file mode 100644
index 00000000..b09efd0e
--- /dev/null
+++ b/src/delete/Model.js
@@ -0,0 +1,65 @@
+// Single $ is special and does not get searched
+// Double $$ is special an is client only (does not get sent to server)
+
+function Model(entity, initial) {
+ this['$$entity'] = entity;
+ this['$loadFrom'](initial||{});
+ this['$entity'] = entity['title'];
+ this['$migrate']();
+};
+
+Model.copyDirectFields = function(src, dst) {
+ if (src === dst || !src || !dst) return;
+ var isDataField = function(src, dst, field) {
+ return (field.substring(0,2) !== '$$') &&
+ (typeof src[field] !== 'function') &&
+ (typeof dst[field] !== 'function');
+ };
+ for (var field in dst) {
+ if (isDataField(src, dst, field))
+ delete dst[field];
+ }
+ for (field in src) {
+ if (isDataField(src, dst, field))
+ dst[field] = src[field];
+ }
+};
+
+extend(Model.prototype, {
+ '$migrate': function() {
+ merge(this['$$entity']['defaults'], this);
+ return this;
+ },
+
+ '$merge': function(other) {
+ merge(other, this);
+ return this;
+ },
+
+ '$save': function(callback) {
+ this['$$entity'].datastore.save(this, callback === true ? undefined : callback);
+ if (callback === true) this['$$entity'].datastore.flush();
+ return this;
+ },
+
+ '$delete': function(callback) {
+ this['$$entity'].datastore.remove(this, callback === true ? undefined : callback);
+ if (callback === true) this['$$entity'].datastore.flush();
+ return this;
+ },
+
+ '$loadById': function(id, callback) {
+ this['$$entity'].datastore.load(this, id, callback);
+ return this;
+ },
+
+ '$loadFrom': function(other) {
+ Model.copyDirectFields(other, this);
+ return this;
+ },
+
+ '$saveTo': function(other) {
+ Model.copyDirectFields(this, other);
+ return this;
+ }
+}); \ No newline at end of file
diff --git a/src/delete/Scope.js b/src/delete/Scope.js
new file mode 100644
index 00000000..ae3f9f11
--- /dev/null
+++ b/src/delete/Scope.js
@@ -0,0 +1,407 @@
+function Scope(initialState, name) {
+ var self = this;
+ self.widgets = [];
+ self.evals = [];
+ self.watchListeners = {};
+ self.name = name;
+ initialState = initialState || {};
+ var State = function(){};
+ State.prototype = initialState;
+ self.state = new State();
+ extend(self.state, {
+ '$parent': initialState,
+ '$watch': bind(self, self.addWatchListener),
+ '$eval': bind(self, self.eval),
+ '$bind': bind(self, bind, self),
+ // change name to autoEval?
+ '$addEval': bind(self, self.addEval),
+ '$updateView': bind(self, self.updateView)
+ });
+ if (name == "ROOT") {
+ self.state['$root'] = self.state;
+ }
+};
+
+Scope.expressionCache = {};
+Scope.getter = function(instance, path) {
+ 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(fn, lastInstance, lastInstance);
+ return instance;
+ }
+ }
+ }
+ if (typeof instance === 'function' && !instance['$$factory']) {
+ return bind(lastInstance, instance);
+ }
+ return instance;
+};
+
+Scope.setter = function(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;
+};
+
+Scope.prototype = {
+ // TODO: rename to update? or eval?
+ updateView: function() {
+ var self = this;
+ this.fireWatchers();
+ foreach(this.widgets, function(widget){
+ self.evalWidget(widget, "", {}, function(){
+ this.updateView(self);
+ });
+ });
+ foreach(this.evals, bind(this, this.apply));
+ },
+
+ addWidget: function(controller) {
+ if (controller) this.widgets.push(controller);
+ },
+
+ addEval: function(fn, listener) {
+ // todo: this should take a function/string and a listener
+ // todo: this is a hack, which will need to be cleaned up.
+ var self = this,
+ listenFn = listener || noop,
+ expr = self.compile(fn);
+ this.evals.push(function(){
+ self.apply(listenFn, expr());
+ });
+ },
+
+ isProperty: function(exp) {
+ for ( var i = 0; i < exp.length; i++) {
+ var ch = exp.charAt(i);
+ if (ch!='.' && !Lexer.prototype.isIdent(ch)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ get: function(path) {
+// log('SCOPE.get', path, Scope.getter(this.state, path));
+ return Scope.getter(this.state, path);
+ },
+
+ set: function(path, value) {
+// log('SCOPE.set', path, value);
+ var instance = this.state;
+ return Scope.setter(instance, path, value);
+ },
+
+ setEval: function(expressionText, value) {
+ this.eval(expressionText + "=" + toJson(value));
+ },
+
+ compile: function(exp) {
+ if (isFunction(exp)) return bind(this.state, exp);
+ var expFn = Scope.expressionCache[exp], self = this;
+ if (!expFn) {
+ var parser = new Parser(exp);
+ expFn = parser.statements();
+ parser.assertAllConsumed();
+ Scope.expressionCache[exp] = expFn;
+ }
+ return function(context){
+ context = context || {};
+ context.self = self.state;
+ context.scope = self;
+ return expFn.call(self, context);
+ };
+ },
+
+ eval: function(exp, context) {
+// log('Scope.eval', expressionText);
+ return this.compile(exp)(context);
+ },
+
+ //TODO: Refactor. This function needs to be an execution closure for widgets
+ // move to widgets
+ // remove expression, just have inner closure.
+ evalWidget: function(widget, expression, context, onSuccess, onFailure) {
+ try {
+ var value = this.eval(expression, context);
+ if (widget.hasError) {
+ widget.hasError = false;
+ jQuery(widget.view).
+ removeClass('ng-exception').
+ removeAttr('ng-error');
+ }
+ if (onSuccess) {
+ value = onSuccess.apply(widget, [value]);
+ }
+ return true;
+ } catch (e){
+ var jsonError = toJson(e, true);
+ error('Eval Widget Error:', jsonError);
+ widget.hasError = true;
+ jQuery(widget.view).
+ addClass('ng-exception').
+ attr('ng-error', jsonError);
+ if (onFailure) {
+ onFailure.apply(widget, [e, jsonError]);
+ }
+ return false;
+ }
+ },
+
+ validate: function(expressionText, value, element) {
+ var expression = Scope.expressionCache[expressionText];
+ if (!expression) {
+ expression = new Parser(expressionText).validator();
+ Scope.expressionCache[expressionText] = expression;
+ }
+ var self = {scope:this, self:this.state, '$element':element};
+ return expression(self)(self, value);
+ },
+
+ entity: function(entityDeclaration, datastore) {
+ var expression = new Parser(entityDeclaration).entityDeclaration();
+ return expression({scope:this, datastore:datastore});
+ },
+
+ clearInvalid: function() {
+ var invalid = this.state['$invalidWidgets'];
+ while(invalid.length > 0) {invalid.pop();}
+ },
+
+ markInvalid: function(widget) {
+ this.state['$invalidWidgets'].push(widget);
+ },
+
+ watch: function(declaration) {
+ var self = this;
+ new Parser(declaration).watch()({
+ scope:this,
+ addListener:function(watch, exp){
+ self.addWatchListener(watch, function(n,o){
+ try {
+ return exp({scope:self}, n, o);
+ } catch(e) {
+ alert(e);
+ }
+ });
+ }
+ });
+ },
+
+ addWatchListener: function(watchExpression, listener) {
+ // TODO: clean me up!
+ if (!isFunction(listener)) {
+ listener = this.compile(listener);
+ }
+ var watcher = this.watchListeners[watchExpression];
+ if (!watcher) {
+ watcher = {listeners:[], expression:watchExpression};
+ this.watchListeners[watchExpression] = watcher;
+ }
+ watcher.listeners.push(listener);
+ },
+
+ fireWatchers: function() {
+ var self = this, fired = false;
+ foreach(this.watchListeners, function(watcher) {
+ var value = self.eval(watcher.expression);
+ if (value !== watcher.lastValue) {
+ foreach(watcher.listeners, function(listener){
+ listener(value, watcher.lastValue);
+ fired = true;
+ });
+ watcher.lastValue = value;
+ }
+ });
+ return fired;
+ },
+
+ apply: function(fn) {
+ fn.apply(this.state, slice.call(arguments, 1, arguments.length));
+ }
+};
+
+//////////////////////////////
+
+function getter(instance, path) {
+ 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(fn, lastInstance, lastInstance);
+ return instance;
+ }
+ }
+ }
+ if (typeof instance === 'function' && !instance['$$factory']) {
+ 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 compileCache = {};
+function expressionCompile(exp){
+ if (isFunction(exp)) return exp;
+ var expFn = compileCache[exp];
+ if (!expFn) {
+ var parser = new Parser(exp);
+ expFn = parser.statements();
+ parser.assertAllConsumed();
+ compileCache[exp] = expFn;
+ }
+ // return expFn
+ // TODO(remove this hack)
+ return function(){
+ return expFn({
+ scope: {
+ set: this.$set,
+ get: this.$get
+ }
+ });
+ };
+};
+
+var NON_RENDERABLE_ELEMENTS = {
+ '#text': 1, '#comment':1, 'TR':1, 'TH':1
+};
+
+function isRenderableElement(element){
+ return element && element[0] && !NON_RENDERABLE_ELEMENTS[element[0].nodeName];
+}
+
+function rethrow(e) { throw e; }
+function errorHandlerFor(element) {
+ while (!isRenderableElement(element)) {
+ element = element.parent() || jqLite(document.body);
+ }
+ return function(error) {
+ element.attr('ng-error', angular.toJson(error));
+ element.addClass('ng-exception');
+ };
+}
+
+function createScope(parent, Class) {
+ function Parent(){}
+ function API(){}
+ function Behavior(){}
+
+ var instance, behavior, api, watchList = [], evalList = [];
+
+ Class = Class || noop;
+ parent = Parent.prototype = parent || {};
+ api = API.prototype = new Parent();
+ behavior = Behavior.prototype = extend(new API(), Class.prototype);
+ instance = new Behavior();
+
+ extend(api, {
+ $parent: parent,
+ $bind: bind(instance, bind, instance),
+ $get: bind(instance, getter, instance),
+ $set: bind(instance, setter, instance),
+
+ $eval: function(exp) {
+ if (isDefined(exp)) {
+ return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));
+ } else {
+ foreach(evalList, function(eval) {
+ instance.$tryEval(eval.fn, eval.handler);
+ });
+ foreach(watchList, function(watch) {
+ var value = instance.$tryEval(watch.watch, watch.handler);
+ if (watch.last !== value) {
+ instance.$tryEval(watch.listener, watch.handler, value, watch.last);
+ watch.last = value;
+ }
+ });
+ }
+ },
+
+ $tryEval: function (expression, exceptionHandler) {
+ try {
+ return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length));
+ } catch (e) {
+ error(e);
+ if (isFunction(exceptionHandler)) {
+ exceptionHandler(e);
+ } else if (exceptionHandler) {
+ errorHandlerFor(exceptionHandler)(e);
+ }
+ }
+ },
+
+ $watch: function(watchExp, listener, exceptionHandler) {
+ var watch = expressionCompile(watchExp);
+ watchList.push({
+ watch: watch,
+ last: watch.call(instance),
+ handler: exceptionHandler,
+ listener:expressionCompile(listener)
+ });
+ },
+
+ $onEval: function(expr, exceptionHandler){
+ evalList.push({
+ fn: expressionCompile(expr),
+ handler: exceptionHandler
+ });
+ }
+ });
+
+ Class.apply(instance, slice.call(arguments, 2, arguments.length));
+
+ return instance;
+}
diff --git a/src/delete/Widgets.js b/src/delete/Widgets.js
new file mode 100644
index 00000000..96b63793
--- /dev/null
+++ b/src/delete/Widgets.js
@@ -0,0 +1,806 @@
+function WidgetFactory(serverUrl, database) {
+ this.nextUploadId = 0;
+ this.serverUrl = serverUrl;
+ this.database = database;
+ if (window['swfobject']) {
+ this.createSWF = window['swfobject']['createSWF'];
+ } else {
+ this.createSWF = function(){
+ alert("ERROR: swfobject not loaded!");
+ };
+ }
+};
+
+WidgetFactory.prototype = {
+ createController: function(input, scope) {
+ var controller;
+ var type = input.attr('type').toLowerCase();
+ var exp = input.attr('name');
+ if (exp) exp = exp.split(':').pop();
+ var event = "change";
+ var bubbleEvent = true;
+ var formatter = angularFormatter[input.attr('ng-format')] || angularFormatter['noop'];
+ if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') {
+ controller = new ButtonController(input[0], exp, formatter);
+ event = "click";
+ bubbleEvent = false;
+ } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') {
+ controller = new TextController(input[0], exp, formatter);
+ event = "keyup change";
+ } else if (type == 'checkbox') {
+ controller = new CheckboxController(input[0], exp, formatter);
+ event = "click";
+ } else if (type == 'radio') {
+ controller = new RadioController(input[0], exp, formatter);
+ event="click";
+ } else if (type == 'select-one') {
+ controller = new SelectController(input[0], exp, formatter);
+ } else if (type == 'select-multiple') {
+ controller = new MultiSelectController(input[0], exp, formatter);
+ } else if (type == 'file') {
+ controller = this.createFileController(input, exp, formatter);
+ } else {
+ throw 'Unknown type: ' + type;
+ }
+ input.data('controller', controller);
+ var updateView = scope.get('$updateView');
+ var action = function() {
+ if (controller.updateModel(scope)) {
+ var action = jQuery(controller.view).attr('ng-action') || "";
+ if (scope.evalWidget(controller, action)) {
+ updateView(scope);
+ }
+ }
+ return bubbleEvent;
+ };
+ jQuery(controller.view, ":input").
+ bind(event, action);
+ return controller;
+ },
+
+ createFileController: function(fileInput) {
+ var uploadId = '__uploadWidget_' + (this.nextUploadId++);
+ var view = FileController.template(uploadId);
+ fileInput.after(view);
+ var att = {
+ 'data':this.serverUrl + "/admin/ServerAPI.swf",
+ 'width':"95", 'height':"20", 'align':"top",
+ 'wmode':"transparent"};
+ var par = {
+ 'flashvars':"uploadWidgetId=" + uploadId,
+ 'allowScriptAccess':"always"};
+ var swfNode = this.createSWF(att, par, uploadId);
+ fileInput.remove();
+ var cntl = new FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database);
+ jQuery(swfNode).parent().data('controller', cntl);
+ return cntl;
+ }
+};
+/////////////////////
+// FileController
+///////////////////////
+
+function FileController(view, scopeName, uploader, databaseUrl) {
+ this.view = view;
+ this.uploader = uploader;
+ this.scopeName = scopeName;
+ this.attachmentsPath = databaseUrl + '/_attachments';
+ this.value = null;
+ this.lastValue = undefined;
+};
+
+angularCallbacks['flashEvent'] = function(id, event, args) {
+ var object = document.getElementById(id);
+ var jobject = jQuery(object);
+ var controller = jobject.parent().data("controller");
+ FileController.prototype[event].apply(controller, args);
+ _.defer(jobject.scope().get('$updateView'));
+};
+
+FileController.template = function(id) {
+ return jQuery('<span class="ng-upload-widget">' +
+ '<input type="checkbox" ng-non-bindable="true"/>' +
+ '<object id="' + id + '" />' +
+ '<a></a>' +
+ '<span/>' +
+ '</span>');
+};
+
+extend(FileController.prototype, {
+ 'cancel': noop,
+ 'complete': noop,
+ 'httpStatus': function(status) {
+ alert("httpStatus:" + this.scopeName + " status:" + status);
+ },
+ 'ioError': function() {
+ alert("ioError:" + this.scopeName);
+ },
+ 'open': function() {
+ alert("open:" + this.scopeName);
+ },
+ 'progress':noop,
+ 'securityError': function() {
+ alert("securityError:" + this.scopeName);
+ },
+ 'uploadCompleteData': function(data) {
+ var value = fromJson(data);
+ value.url = this.attachmentsPath + '/' + value.id + '/' + value.text;
+ this.view.find("input").attr('checked', true);
+ var scope = this.view.scope();
+ this.value = value;
+ this.updateModel(scope);
+ this.value = null;
+ },
+ 'select': function(name, size, type) {
+ this.name = name;
+ this.view.find("a").text(name).attr('href', name);
+ this.view.find("span").text(angular['filter']['bytes'](size));
+ this.upload();
+ },
+
+ updateModel: function(scope) {
+ var isChecked = this.view.find("input").attr('checked');
+ var value = isChecked ? this.value : null;
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.set(this.scopeName, value);
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var modelValue = scope.get(this.scopeName);
+ if (modelValue && this.value !== modelValue) {
+ this.value = modelValue;
+ this.view.find("a").
+ attr("href", this.value.url).
+ text(this.value.text);
+ this.view.find("span").text(angular['filter']['bytes'](this.value.size));
+ }
+ this.view.find("input").attr('checked', !!modelValue);
+ },
+
+ upload: function() {
+ if (this.name) {
+ this.uploader['uploadFile'](this.attachmentsPath);
+ }
+ }
+});
+
+///////////////////////
+// NullController
+///////////////////////
+function NullController(view) {this.view = view;};
+NullController.prototype = {
+ updateModel: function() { return true; },
+ updateView: noop
+};
+NullController.instance = new NullController();
+
+
+///////////////////////
+// ButtonController
+///////////////////////
+var ButtonController = NullController;
+
+///////////////////////
+// TextController
+///////////////////////
+function TextController(view, exp, formatter) {
+ this.view = view;
+ this.formatter = formatter;
+ this.exp = exp;
+ this.validator = view.getAttribute('ng-validate');
+ this.required = typeof view.attributes['ng-required'] != "undefined";
+ this.lastErrorText = null;
+ this.lastValue = undefined;
+ this.initialValue = this.formatter['parse'](view.value);
+ var widget = view.getAttribute('ng-widget');
+ if (widget === 'datepicker') {
+ jQuery(view).datepicker();
+ }
+};
+
+TextController.prototype = {
+ updateModel: function(scope) {
+ var value = this.formatter['parse'](this.view.value);
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var view = this.view;
+ var value = scope.get(this.exp);
+ if (typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ value = value ? value : '';
+ if (!_(this.lastValue).isEqual(value)) {
+ view.value = this.formatter['format'](value);
+ this.lastValue = value;
+ }
+
+ var isValidationError = false;
+ view.removeAttribute('ng-error');
+ if (this.required) {
+ isValidationError = !(value && $.trim("" + value).length > 0);
+ }
+ var errorText = isValidationError ? "Required Value" : null;
+ if (!isValidationError && this.validator && value) {
+ errorText = scope.validate(this.validator, value, view);
+ isValidationError = !!errorText;
+ }
+ if (this.lastErrorText !== errorText) {
+ this.lastErrorText = isValidationError;
+ if (errorText && isVisible(view)) {
+ view.setAttribute('ng-error', errorText);
+ scope.markInvalid(this);
+ }
+ jQuery(view).toggleClass('ng-validation-error', isValidationError);
+ }
+ }
+};
+
+///////////////////////
+// CheckboxController
+///////////////////////
+function CheckboxController(view, exp, formatter) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.formatter = formatter;
+ this.initialValue = this.formatter['parse'](view.checked ? view.value : "");
+};
+
+CheckboxController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ var value = input.checked ? input.value : '';
+ value = this.formatter['parse'](value);
+ value = this.formatter['format'](value);
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, this.formatter['parse'](value));
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.eval(this.exp);
+ if (typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ input.checked = this.formatter['parse'](input.value) == value;
+ }
+};
+
+///////////////////////
+// SelectController
+///////////////////////
+function SelectController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.initialValue = view.value;
+};
+
+SelectController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ if (input.selectedIndex < 0) {
+ scope.setEval(this.exp, null);
+ } else {
+ var value = this.view.value;
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.get(this.exp);
+ if (typeof value === 'undefined') {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ if (value !== this.lastValue) {
+ input.value = value ? value : "";
+ this.lastValue = value;
+ }
+ }
+};
+
+///////////////////////
+// MultiSelectController
+///////////////////////
+function MultiSelectController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.initialValue = this.selected();
+};
+
+MultiSelectController.prototype = {
+ selected: function () {
+ var value = [];
+ var options = this.view.options;
+ for ( var i = 0; i < options.length; i++) {
+ var option = options[i];
+ if (option.selected) {
+ value.push(option.value);
+ }
+ }
+ return value;
+ },
+
+ updateModel: function(scope) {
+ var value = this.selected();
+ // TODO: This is wrong! no caching going on here as we are always comparing arrays
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var selected = scope.get(this.exp);
+ if (typeof selected === "undefined") {
+ selected = this.initialValue;
+ scope.setEval(this.exp, selected);
+ }
+ if (selected !== this.lastValue) {
+ var options = input.options;
+ for ( var i = 0; i < options.length; i++) {
+ var option = options[i];
+ option.selected = _.include(selected, option.value);
+ }
+ this.lastValue = selected;
+ }
+ }
+};
+
+///////////////////////
+// RadioController
+///////////////////////
+function RadioController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastChecked = undefined;
+ this.lastValue = undefined;
+ this.inputValue = view.value;
+ this.initialValue = view.checked ? view.value : null;
+};
+
+RadioController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ if (this.lastChecked) {
+ return false;
+ } else {
+ input.checked = true;
+ this.lastValue = scope.setEval(this.exp, this.inputValue);
+ this.lastChecked = true;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.get(this.exp);
+ if (this.initialValue && typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ if (this.lastValue != value) {
+ this.lastChecked = input.checked = this.inputValue == (''+value);
+ this.lastValue = value;
+ }
+ }
+};
+
+///////////////////////
+//ElementController
+///////////////////////
+function BindUpdater(view, exp) {
+ this.view = view;
+ this.exp = Binder.parseBindings(exp);
+ this.hasError = false;
+};
+
+BindUpdater.toText = function(obj) {
+ var e = escapeHtml;
+ switch(typeof obj) {
+ case "string":
+ case "boolean":
+ case "number":
+ return e(obj);
+ case "function":
+ return BindUpdater.toText(obj());
+ case "object":
+ if (isNode(obj)) {
+ return outerHTML(obj);
+ } else if (obj instanceof angular.filter.Meta) {
+ switch(typeof obj.html) {
+ case "string":
+ case "number":
+ return obj.html;
+ case "function":
+ return obj.html();
+ case "object":
+ if (isNode(obj.html))
+ return outerHTML(obj.html);
+ default:
+ break;
+ }
+ switch(typeof obj.text) {
+ case "string":
+ case "number":
+ return e(obj.text);
+ case "function":
+ return e(obj.text());
+ default:
+ break;
+ }
+ }
+ if (obj === null)
+ return "";
+ return e(toJson(obj, true));
+ default:
+ return "";
+ }
+};
+
+BindUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ var html = [];
+ var parts = this.exp;
+ var length = parts.length;
+ for(var i=0; i<length; i++) {
+ var part = parts[i];
+ var binding = Binder.binding(part);
+ if (binding) {
+ scope.evalWidget(this, binding, {$element:this.view}, function(value){
+ html.push(BindUpdater.toText(value));
+ }, function(e, text){
+ setHtml(this.view, text);
+ });
+ if (this.hasError) {
+ return;
+ }
+ } else {
+ html.push(escapeHtml(part));
+ }
+ }
+ setHtml(this.view, html.join(''));
+ }
+};
+
+function BindAttrUpdater(view, attrs) {
+ this.view = view;
+ this.attrs = attrs;
+};
+
+BindAttrUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ var jNode = jQuery(this.view);
+ var attributeTemplates = this.attrs;
+ if (this.hasError) {
+ this.hasError = false;
+ jNode.
+ removeClass('ng-exception').
+ removeAttr('ng-error');
+ }
+ var isImage = jNode.is('img');
+ for (var attrName in attributeTemplates) {
+ var attributeTemplate = Binder.parseBindings(attributeTemplates[attrName]);
+ var attrValues = [];
+ for ( var i = 0; i < attributeTemplate.length; i++) {
+ var binding = Binder.binding(attributeTemplate[i]);
+ if (binding) {
+ try {
+ var value = scope.eval(binding, {$element:jNode[0], attrName:attrName});
+ if (value && (value.constructor !== array || value.length !== 0))
+ attrValues.push(value);
+ } catch (e) {
+ this.hasError = true;
+ error('BindAttrUpdater', e);
+ var jsonError = toJson(e, true);
+ attrValues.push('[' + jsonError + ']');
+ jNode.
+ addClass('ng-exception').
+ attr('ng-error', jsonError);
+ }
+ } else {
+ attrValues.push(attributeTemplate[i]);
+ }
+ }
+ var attrValue = attrValues.length ? attrValues.join('') : null;
+ if(isImage && attrName == 'src' && !attrValue)
+ attrValue = scope.get('$config.blankImage');
+ jNode.attr(attrName, attrValue);
+ }
+ }
+};
+
+function EvalUpdater(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.hasError = false;
+};
+EvalUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp);
+ }
+};
+
+function HideUpdater(view, exp) { this.view = view; this.exp = exp; };
+HideUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(hideValue){
+ var view = jQuery(this.view);
+ if (toBoolean(hideValue)) {
+ view.hide();
+ } else {
+ view.show();
+ }
+ });
+ }
+};
+
+function ShowUpdater(view, exp) { this.view = view; this.exp = exp; };
+ShowUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(hideValue){
+ var view = jQuery(this.view);
+ if (toBoolean(hideValue)) {
+ view.show();
+ } else {
+ view.hide();
+ }
+ });
+ }
+};
+
+function ClassUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ if (classValue !== null && classValue !== undefined) {
+ this.view.className = classValue;
+ }
+ });
+ }
+};
+
+function ClassEvenUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassEvenUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ var index = scope.get('$index');
+ jQuery(this.view).toggleClass(classValue, index % 2 === 1);
+ });
+ }
+};
+
+function ClassOddUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassOddUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ var index = scope.get('$index');
+ jQuery(this.view).toggleClass(classValue, index % 2 === 0);
+ });
+ }
+};
+
+function StyleUpdater(view, exp) { this.view = view; this.exp = exp; };
+StyleUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(styleValue){
+ jQuery(this.view).attr('style', "").css(styleValue);
+ });
+ }
+};
+
+///////////////////////
+// RepeaterUpdater
+///////////////////////
+function RepeaterUpdater(view, repeaterExpression, template, prefix) {
+ this.view = view;
+ this.template = template;
+ this.prefix = prefix;
+ this.children = [];
+ var match = repeaterExpression.match(/^\s*(.+)\s+in\s+(.*)\s*$/);
+ if (! match) {
+ throw "Expected ng-repeat in form of 'item in collection' but got '" +
+ repeaterExpression + "'.";
+ }
+ var keyValue = match[1];
+ this.iteratorExp = match[2];
+ match = keyValue.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
+ if (!match) {
+ throw "'item' in 'item in collection' should be identifier or (key, value) but get '" +
+ keyValue + "'.";
+ }
+ this.valueExp = match[3] || match[1];
+ this.keyExp = match[2];
+};
+
+RepeaterUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.iteratorExp, {}, function(iterator){
+ var self = this;
+ if (!iterator) {
+ iterator = [];
+ if (scope.isProperty(this.iteratorExp)) {
+ scope.set(this.iteratorExp, iterator);
+ }
+ }
+ var childrenLength = this.children.length;
+ var cursor = this.view;
+ var time = 0;
+ var child = null;
+ var keyExp = this.keyExp;
+ var valueExp = this.valueExp;
+ var iteratorCounter = 0;
+ foreach(iterator, function(value, key){
+ if (iteratorCounter < childrenLength) {
+ // reuse children
+ child = self.children[iteratorCounter];
+ child.scope.set(valueExp, value);
+ } else {
+ // grow children
+ var name = self.prefix +
+ valueExp + " in " + self.iteratorExp + "[" + iteratorCounter + "]";
+ var childScope = new Scope(scope.state, name);
+ childScope.set('$index', iteratorCounter);
+ if (keyExp)
+ childScope.set(keyExp, key);
+ childScope.set(valueExp, value);
+ child = { scope:childScope, element:self.template(childScope, self.prefix, iteratorCounter) };
+ cursor.after(child.element);
+ self.children.push(child);
+ }
+ cursor = child.element;
+ var s = new Date().getTime();
+ child.scope.updateView();
+ time += new Date().getTime() - s;
+ iteratorCounter++;
+ });
+ // shrink children
+ for ( var r = childrenLength; r > iteratorCounter; --r) {
+ this.children.pop().element.remove();
+ }
+ // Special case for option in select
+ if (child && child.element[0].nodeName === "OPTION") {
+ var select = jQuery(child.element[0].parentNode);
+ var cntl = select.data('controller');
+ if (cntl) {
+ cntl.lastValue = undefined;
+ cntl.updateView(scope);
+ }
+ }
+ });
+ }
+};
+
+//////////////////////////////////
+// PopUp
+//////////////////////////////////
+
+function PopUp(doc) {
+ this.doc = doc;
+};
+
+PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup";
+
+PopUp.onOver = function(e) {
+ PopUp.onOut();
+ var jNode = jQuery(this);
+ jNode.bind(PopUp.OUT_EVENT, PopUp.onOut);
+ var position = jNode.position();
+ var de = document.documentElement;
+ var w = self.innerWidth || (de && de.clientWidth) || document.body.clientWidth;
+ var hasArea = w - position.left;
+ var width = 300;
+ var title = jNode.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error...";
+ var msg = jNode.attr("ng-error");
+
+ var x;
+ var arrowPos = hasArea>(width+75) ? "left" : "right";
+ var tip = jQuery(
+ "<div id='ng-callout' style='width:"+width+"px'>" +
+ "<div class='ng-arrow-"+arrowPos+"'/>" +
+ "<div class='ng-title'>"+title+"</div>" +
+ "<div class='ng-content'>"+msg+"</div>" +
+ "</div>");
+ jQuery("body").append(tip);
+ if(arrowPos === 'left'){
+ x = position.left + this.offsetWidth + 11;
+ }else{
+ x = position.left - (width + 15);
+ tip.find('.ng-arrow-right').css({left:width+1});
+ }
+
+ tip.css({left: x+"px", top: (position.top - 3)+"px"});
+ return true;
+};
+
+PopUp.onOut = function() {
+ jQuery('#ng-callout').
+ unbind(PopUp.OUT_EVENT, PopUp.onOut).
+ remove();
+ return true;
+};
+
+PopUp.prototype = {
+ bind: function () {
+ var self = this;
+ this.doc.find('.ng-validation-error,.ng-exception').
+ live("mouseover", PopUp.onOver);
+ }
+};
+
+//////////////////////////////////
+// Status
+//////////////////////////////////
+
+function NullStatus(body) {
+};
+
+NullStatus.prototype = {
+ beginRequest:function(){},
+ endRequest:function(){}
+};
+
+function Status(body) {
+ this.requestCount = 0;
+ this.body = body;
+};
+
+Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>';
+
+Status.prototype = {
+ beginRequest: function () {
+ if (this.requestCount === 0) {
+ (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show();
+ }
+ this.requestCount++;
+ },
+
+ endRequest: function () {
+ this.requestCount--;
+ if (this.requestCount === 0) {
+ this.loader.hide("fold");
+ }
+ }
+};
diff --git a/src/directives.js b/src/directives.js
new file mode 100644
index 00000000..cabf0c23
--- /dev/null
+++ b/src/directives.js
@@ -0,0 +1,261 @@
+angularDirective("ng-init", function(expression){
+ return function(element){
+ this.$tryEval(expression, element);
+ };
+});
+
+angularDirective("ng-controller", function(expression){
+ 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);
+ (this.init || noop)();
+ };
+});
+
+angularDirective("ng-eval", function(expression){
+ return function(element){
+ this.$onEval(expression, element);
+ };
+});
+
+angularDirective("ng-bind", function(expression){
+ return function(element) {
+ var lastValue = noop, lastError = noop;
+ this.$onEval(function() {
+ var error,
+ value = this.$tryEval(expression, function(e){
+ error = toJson(e);
+ }),
+ isHtml,
+ isDomElement;
+ if (lastValue === value && lastError == error) return;
+ isHtml = value instanceof HTML,
+ 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(value.html);
+ } else if (isDomElement) {
+ element.html('');
+ element.append(value);
+ } else {
+ element.text(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;
+ 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);
+ };
+ return parts.join('');
+ };
+ }
+ return fn;
+}
+
+angularDirective("ng-bind-template", function(expression){
+ 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'
+};
+angularDirective("ng-bind-attr", function(expression){
+ return function(element){
+ var lastValue = {};
+ this.$onEval(function(){
+ var values = this.$eval(expression);
+ 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 (element[specialName] = toBoolean(value)) {
+ element.attr(specialName, value);
+ } else {
+ element.removeAttr(key);
+ }
+ (element.data('$validate')||noop)();
+ } else {
+ element.attr(key, value);
+ }
+ }
+ };
+ }, element);
+ };
+});
+
+angularWidget("@ng-non-bindable", noop);
+
+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 "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 "'item' in 'item in collection' should be identifier or (key, value) but got '" +
+ keyValue + "'.";
+ }
+ valueIdent = match[3] || match[1];
+ keyIdent = match[2];
+
+ if (isUndefined(this.$eval(rhs))) this.$set(rhs, []);
+
+ var children = [], currentScope = this;
+ this.$onEval(function(){
+ var index = 0, childCount = children.length, childScope, lastElement = reference,
+ collection = this.$tryEval(rhs, reference);
+ for ( var key in collection) {
+ if (index < childCount) {
+ // reuse existing child
+ childScope = children[index];
+ childScope[valueIdent] = collection[key];
+ if (keyIdent) childScope[keyIdent] = key;
+ } else {
+ // grow children
+ childScope = template(element.clone(), createScope(currentScope));
+ childScope[valueIdent] = collection[key];
+ if (keyIdent) childScope[keyIdent] = key;
+ lastElement.after(childScope.$element);
+ childScope.$index = index;
+ 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);
+ };
+});
+
+angularDirective("ng-click", function(expression, element){
+ return function(element){
+ var self = this;
+ element.bind('click', function(){
+ self.$tryEval(expression, element);
+ self.$root.$eval();
+ return false;
+ });
+ };
+});
+
+angularDirective("ng-watch", function(expression, element){
+ return function(element){
+ var self = this;
+ new 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(){
+ var value = this.$eval(expression);
+ if (selector(this.$index)) {
+ if (isArray(value)) value = value.join(' ');
+ element[0].className = trim(existing + value);
+ }
+ }, element);
+ };
+ };
+}
+
+angularDirective("ng-class", ngClass(function(){return true;}));
+angularDirective("ng-class-odd", ngClass(function(i){return i % 2 === 0;}));
+angularDirective("ng-class-even", ngClass(function(i){return i % 2 === 1;}));
+
+angularDirective("ng-show", function(expression, element){
+ return function(element){
+ this.$onEval(function(){
+ element.css('display', toBoolean(this.$eval(expression)) ? '' : 'none');
+ }, element);
+ };
+});
+
+angularDirective("ng-hide", function(expression, element){
+ return function(element){
+ this.$onEval(function(){
+ element.css('display', toBoolean(this.$eval(expression)) ? 'none' : '');
+ }, element);
+ };
+});
+
+angularDirective("ng-style", function(expression, element){
+ return function(element){
+ this.$onEval(function(){
+ element.css(this.$eval(expression));
+ }, element);
+ };
+});
+
diff --git a/src/filters.js b/src/filters.js
new file mode 100644
index 00000000..a911b935
--- /dev/null
+++ b/src/filters.js
@@ -0,0 +1,298 @@
+var angularFilterGoogleChartApi;
+
+foreach({
+ 'currency': function(amount){
+ this.$element.toggleClass('ng-format-negative', amount < 0);
+ return '$' + angularFilter['number'].apply(this, [amount, 2]);
+ },
+
+ 'number': function(amount, fractionSize){
+ if (isNaN(amount) || !isFinite(amount)) {
+ return '';
+ }
+ fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize;
+ var isNegative = amount < 0;
+ amount = Math.abs(amount);
+ var pow = Math.pow(10, fractionSize);
+ var text = "" + Math.round(amount * 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;
+ },
+
+ 'date': function(amount) {
+ },
+
+ 'json': function(object) {
+ this.$element.addClass("ng-monospace");
+ return toJson(object, true);
+ },
+
+ 'trackPackage': (function(){
+ var MATCHERS = [
+ { name: "UPS",
+ url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=",
+ regexp: [
+ /^1Z[0-9A-Z]{16}$/i]},
+ { name: "FedEx",
+ url: "http://www.fedex.com/Tracking?tracknumbers=",
+ regexp: [
+ /^96\d{10}?$/i,
+ /^96\d{17}?$/i,
+ /^96\d{20}?$/i,
+ /^\d{15}$/i,
+ /^\d{12}$/i]},
+ { name: "USPS",
+ url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=",
+ regexp: [
+ /^(91\d{20})$/i,
+ /^(91\d{18})$/i]}];
+ return function(trackingNo, noMatch) {
+ trackingNo = trim(trackingNo);
+ var tNo = trackingNo.replace(/ /g, '');
+ var returnValue;
+ foreach(MATCHERS, function(carrier){
+ foreach(carrier.regexp, function(regexp){
+ if (!returnValue && regexp.test(tNo)) {
+ var text = carrier.name + ": " + trackingNo;
+ var url = carrier.url + trackingNo;
+ returnValue = jqLite('<a></a>');
+ returnValue.text(text);
+ returnValue.attr('href', url);
+ }
+ });
+ });
+ if (returnValue)
+ return returnValue;
+ else if (trackingNo)
+ return noMatch || trackingNo + " is not recognized";
+ else
+ return null;
+ };})(),
+
+ 'link': function(obj, title) {
+ if (obj) {
+ var text = title || obj.text || obj;
+ var url = obj.url || obj;
+ if (url) {
+ if (angular.validator.email(url) === null) {
+ url = "mailto:" + url;
+ }
+ var a = jqLite('<a></a>');
+ a.attr('href', url);
+ a.text(text);
+ return a;
+ }
+ }
+ return obj;
+ },
+
+
+ 'bytes': (function(){
+ var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ return function(size) {
+ if(size === null) return "";
+
+ var suffix = 0;
+ while (size > 1000) {
+ size = size / 1024;
+ suffix++;
+ }
+ var txt = "" + size;
+ var dot = txt.indexOf('.');
+ if (dot > -1 && dot + 2 < txt.length) {
+ txt = txt.substring(0, dot + 2);
+ }
+ return txt + " " + SUFFIX[suffix];
+ };
+ })(),
+
+ 'image': function(obj, width, height) {
+ if (obj && obj.url) {
+ var style = "", img = jqLite('<img>');
+ if (width) {
+ img.css('max-width', width + 'px');
+ img.css('max-height', (height || width) + 'px');
+ }
+ img.attr('src', obj.url);
+ return img;
+ }
+ return null;
+ },
+
+ 'lowercase': lowercase,
+
+ 'uppercase': uppercase,
+
+ 'linecount': function (obj) {
+ if (isString(obj)) {
+ if (obj==='') return 1;
+ return obj.split(/\n|\f/).length;
+ }
+ return 1;
+ },
+
+ 'if': function (result, expression) {
+ return expression ? result : undefined;
+ },
+
+ 'unless': function (result, expression) {
+ return expression ? undefined : result;
+ },
+
+ 'googleChartApi': extend(
+ function(type, data, width, height) {
+ data = data || {};
+ var chart = {
+ 'cht':type,
+ 'chco':angularFilterGoogleChartApi['collect'](data, 'color'),
+ 'chtt':angularFilterGoogleChartApi['title'](data),
+ 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'),
+ 'chd':angularFilterGoogleChartApi['values'](data),
+ 'chf':'bg,s,FFFFFF00'
+ };
+ if (_.isArray(data['xLabels'])) {
+ chart['chxt']='x';
+ chart['chxl']='0:|' + data.xLabels.join('|');
+ }
+ return angularFilterGoogleChartApi['encode'](chart, width, height);
+ },
+ {
+ 'values': function(data){
+ var seriesValues = [];
+ foreach(data['series']||[], function(serie){
+ var values = [];
+ foreach(serie['values']||[], function(value){
+ values.push(value);
+ });
+ seriesValues.push(values.join(','));
+ });
+ var values = seriesValues.join('|');
+ return values === "" ? null : "t:" + values;
+ },
+
+ 'title': function(data){
+ var titles = [];
+ var title = data['title'] || [];
+ foreach(_.isArray(title)?title:[title], function(text){
+ titles.push(encodeURIComponent(text));
+ });
+ return titles.join('|');
+ },
+
+ 'collect': function(data, key){
+ var outterValues = [];
+ var count = 0;
+ foreach(data['series']||[], function(serie){
+ var innerValues = [];
+ var value = serie[key] || [];
+ foreach(_.isArray(value)?value:[value], function(color){
+ innerValues.push(encodeURIComponent(color));
+ count++;
+ });
+ outterValues.push(innerValues.join('|'));
+ });
+ return count?outterValues.join(','):null;
+ },
+
+ 'encode': function(params, width, height) {
+ width = width || 200;
+ height = height || width;
+ var url = "http://chart.apis.google.com/chart?",
+ urlParam = [],
+ img = jqLite('<img>');
+ params['chs'] = width + "x" + height;
+ foreach(params, function(value, key){
+ if (value) {
+ urlParam.push(key + "=" + value);
+ }
+ });
+ urlParam.sort();
+ url += urlParam.join("&");
+ img.attr('src', url);
+ img.css({width: width + 'px', height: height + 'px'});
+ return img;
+ }
+ }
+ ),
+
+
+ 'qrcode': function(value, width, height) {
+ return angularFilterGoogleChartApi['encode']({
+ 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height);
+ },
+ 'chart': {
+ 'pie':function(data, width, height) {
+ return angularFilterGoogleChartApi('p', data, width, height);
+ },
+ 'pie3d':function(data, width, height) {
+ return angularFilterGoogleChartApi('p3', data, width, height);
+ },
+ 'pieConcentric':function(data, width, height) {
+ return angularFilterGoogleChartApi('pc', data, width, height);
+ },
+ 'barHorizontalStacked':function(data, width, height) {
+ return angularFilterGoogleChartApi('bhs', data, width, height);
+ },
+ 'barHorizontalGrouped':function(data, width, height) {
+ return angularFilterGoogleChartApi('bhg', data, width, height);
+ },
+ 'barVerticalStacked':function(data, width, height) {
+ return angularFilterGoogleChartApi('bvs', data, width, height);
+ },
+ 'barVerticalGrouped':function(data, width, height) {
+ return angularFilterGoogleChartApi('bvg', data, width, height);
+ },
+ 'line':function(data, width, height) {
+ return angularFilterGoogleChartApi('lc', data, width, height);
+ },
+ 'sparkline':function(data, width, height) {
+ return angularFilterGoogleChartApi('ls', data, width, height);
+ },
+ 'scatter':function(data, width, height) {
+ return angularFilterGoogleChartApi('s', data, width, height);
+ }
+ },
+
+ 'html': function(html){
+ return new HTML(html);
+ },
+
+ 'linky': function(text){
+ if (!text) return text;
+ function regExpEscape(text) {
+ return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1');
+ }
+ var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/;
+ var match;
+ var raw = text;
+ var html = [];
+ while (match=raw.match(URL)) {
+ var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,'');
+ var i = raw.indexOf(url);
+ html.push(escapeHtml(raw.substr(0, i)));
+ html.push('<a href="' + url + '">');
+ html.push(url);
+ html.push('</a>');
+ raw = raw.substring(i + url.length);
+ }
+ html.push(escapeHtml(raw));
+ return new HTML(html.join(''));
+ }
+}, function(v,k){angularFilter[k] = v;});
+
+angularFilterGoogleChartApi = angularFilter['googleChartApi'];
diff --git a/src/formatters.js b/src/formatters.js
new file mode 100644
index 00000000..40462cf3
--- /dev/null
+++ b/src/formatters.js
@@ -0,0 +1,32 @@
+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*$/;
+
+extend(angularFormatter, {
+ 'noop':formatter(identity, identity),
+ 'boolean':formatter(toString, toBoolean),
+ 'number':formatter(toString,
+ function(obj){
+ if (isString(obj) && NUMBER.exec(obj)) {
+ return obj ? 1*obj : null;
+ }
+ throw "Not a number";
+ }),
+
+ '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;
+ }
+ ),
+
+ 'trim':formatter(
+ function(obj) { return obj ? trim("" + obj) : ""; }
+ )
+});
diff --git a/src/jqLite.js b/src/jqLite.js
new file mode 100644
index 00000000..68172fd8
--- /dev/null
+++ b/src/jqLite.js
@@ -0,0 +1,248 @@
+//////////////////////////////////
+//JQLite
+//////////////////////////////////
+
+var jqCache = {};
+var jqName = 'ng-' + new Date().getTime();
+var jqId = 1;
+function jqNextId() { return (jqId++); }
+
+var addEventListener = window.document.attachEvent ?
+ function(element, type, fn) {
+ element.attachEvent('on' + type, fn);
+ } : function(element, type, fn) {
+ element.addEventListener(type, fn, false);
+ };
+
+var removeEventListener = window.document.detachEvent ?
+ function(element, type, fn) {
+ element.detachEvent('on' + type, fn);
+ } : function(element, type, fn) {
+ element.removeEventListener(type, fn, false);
+ };
+
+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 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) {
+ var bubbleEvent = false;
+ foreach(eventHandler.fns, function(fn){
+ bubbleEvent = bubbleEvent || fn.call(self, event);
+ });
+ if (!bubbleEvent) {
+ if (msie) {
+ event.returnValue = false;
+ event.cancelBubble = true;
+ } else {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+ };
+ eventHandler.fns = [];
+ addEventListener(element, type, eventHandler);
+ }
+ eventHandler.fns.push(fn);
+ });
+ },
+
+ trigger: function(type) {
+ var evnt = document.createEvent('MouseEvent');
+ evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ this[0].dispatchEvent(evnt);
+ },
+
+ 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 {
+ var attributes = e.attributes,
+ item = attributes ? attributes.getNamedItem(name) : undefined;
+ return item && item.specified ? item.value : undefined;
+ }
+ },
+
+ 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;
+ }
+ },
+
+ trigger: function(type) {
+ this[0].fireEvent('on' + type);
+ }
+ });
+}
diff --git a/src/markups.js b/src/markups.js
new file mode 100644
index 00000000..74b293b8
--- /dev/null
+++ b/src/markups.js
@@ -0,0 +1,85 @@
+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>&nbsp;</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);
+ }
+ }
+});
+
+var NG_BIND_ATTR = 'ng-bind-attr';
+angularAttrMarkup('{{}}', function(value, name, element){
+ if (name.substr(0, 3) != 'ng-') {
+ 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[name] = value;
+ element.attr(NG_BIND_ATTR, toJson(bindAttr));
+ }
+ }
+});
diff --git a/src/moveToAngularCom/ControlBar.js b/src/moveToAngularCom/ControlBar.js
new file mode 100644
index 00000000..685beeb2
--- /dev/null
+++ b/src/moveToAngularCom/ControlBar.js
@@ -0,0 +1,72 @@
+function ControlBar(document, serverUrl, database) {
+ this._document = document;
+ this.serverUrl = serverUrl;
+ this.database = database;
+ this._window = window;
+ this.callbacks = [];
+};
+
+ControlBar.HTML =
+ '<div>' +
+ '<div class="ui-widget-overlay"></div>' +
+ '<div id="ng-login" ng-non-bindable="true">' +
+ '<div class="ng-login-container"></div>' +
+ '</div>' +
+ '</div>';
+
+
+ControlBar.FORBIDEN =
+ '<div ng-non-bindable="true" title="Permission Error:">' +
+ 'Sorry, you do not have permission for this!'+
+ '</div>';
+
+ControlBar.prototype = {
+ bind: function () {
+ },
+
+ login: function (loginSubmitFn) {
+ this.callbacks.push(loginSubmitFn);
+ if (this.callbacks.length == 1) {
+ this.doTemplate("/user_session/new.mini?database="+encodeURIComponent(this.database)+"&return_url=" + encodeURIComponent(this.urlWithoutAnchor()));
+ }
+ },
+
+ logout: function (loginSubmitFn) {
+ this.callbacks.push(loginSubmitFn);
+ if (this.callbacks.length == 1) {
+ this.doTemplate("/user_session/do_destroy.mini");
+ }
+ },
+
+ urlWithoutAnchor: function (path) {
+ return this._window['location']['href'].split("#")[0];
+ },
+
+ doTemplate: function (path) {
+ var self = this;
+ var id = new Date().getTime();
+ var url = this.urlWithoutAnchor() + "#$iframe_notify=" + id;
+ var iframeHeight = 330;
+ var loginView = jQuery('<div style="overflow:hidden; padding:2px 0 0 0;"><iframe name="'+ url +'" src="'+this.serverUrl + path + '" width="500" height="'+ iframeHeight +'"/></div>');
+ this._document.append(loginView);
+ loginView['dialog']({
+ 'height':iframeHeight + 33, 'width':500,
+ 'resizable': false, 'modal':true,
+ 'title': 'Authentication: <a href="http://www.getangular.com"><tt>&lt;angular/&gt;</tt></a>'
+ });
+ angularCallbacks["_iframe_notify_" + id] = function() {
+ loginView['dialog']("destroy");
+ loginView['remove']();
+ foreach(self.callbacks, function(callback){
+ callback();
+ });
+ self.callbacks = [];
+ };
+ },
+
+ notAuthorized: function () {
+ if (this.forbidenView) return;
+ this.forbidenView = jQuery(ControlBar.FORBIDEN);
+ this.forbidenView.dialog({bgiframe:true, height:70, modal:true});
+ }
+}; \ No newline at end of file
diff --git a/src/moveToAngularCom/DataStore.js b/src/moveToAngularCom/DataStore.js
new file mode 100644
index 00000000..70bcc623
--- /dev/null
+++ b/src/moveToAngularCom/DataStore.js
@@ -0,0 +1,330 @@
+function DataStore(post, users, anchor) {
+ this.post = post;
+ this.users = users;
+ this._cache_collections = [];
+ this._cache = {'$collections':this._cache_collections};
+ this.anchor = anchor;
+ this.bulkRequest = [];
+};
+
+DataStore.NullEntity = extend(function(){}, {
+ 'all': function(){return [];},
+ 'query': function(){return [];},
+ 'load': function(){return {};},
+ 'title': undefined
+});
+
+DataStore.prototype = {
+ cache: function(document) {
+ if (! document.datastore === this) {
+ throw "Parameter must be an instance of Entity! " + toJson(document);
+ }
+ var key = document['$entity'] + '/' + document['$id'];
+ var cachedDocument = this._cache[key];
+ if (cachedDocument) {
+ Model.copyDirectFields(document, cachedDocument);
+ } else {
+ this._cache[key] = document;
+ cachedDocument = document;
+ }
+ return cachedDocument;
+ },
+
+ load: function(instance, id, callback, failure) {
+ if (id && id !== '*') {
+ var self = this;
+ this._jsonRequest(["GET", instance['$entity'] + "/" + id], function(response) {
+ instance['$loadFrom'](response);
+ instance['$migrate']();
+ var clone = instance['$$entity'](instance);
+ self.cache(clone);
+ (callback||noop)(instance);
+ }, failure);
+ }
+ return instance;
+ },
+
+ loadMany: function(entity, ids, callback) {
+ var self=this;
+ var list = [];
+ var callbackCount = 0;
+ foreach(ids, function(id){
+ list.push(self.load(entity(), id, function(){
+ callbackCount++;
+ if (callbackCount == ids.length) {
+ (callback||noop)(list);
+ }
+ }));
+ });
+ return list;
+ },
+
+ loadOrCreate: function(instance, id, callback) {
+ var self=this;
+ return this.load(instance, id, callback, function(response){
+ if (response['$status_code'] == 404) {
+ instance['$id'] = id;
+ (callback||noop)(instance);
+ } else {
+ throw response;
+ }
+ });
+ },
+
+ loadAll: function(entity, callback) {
+ var self = this;
+ var list = [];
+ list['$$accept'] = function(doc){
+ return doc['$entity'] == entity['title'];
+ };
+ this._cache_collections.push(list);
+ this._jsonRequest(["GET", entity['title']], function(response) {
+ var rows = response;
+ for ( var i = 0; i < rows.length; i++) {
+ var document = entity();
+ document['$loadFrom'](rows[i]);
+ list.push(self.cache(document));
+ }
+ (callback||noop)(list);
+ });
+ return list;
+ },
+
+ save: function(document, callback) {
+ var self = this;
+ var data = {};
+ document['$saveTo'](data);
+ this._jsonRequest(["POST", "", data], function(response) {
+ document['$loadFrom'](response);
+ var cachedDoc = self.cache(document);
+ _.each(self._cache_collections, function(collection){
+ if (collection['$$accept'](document)) {
+ angularArray['includeIf'](collection, cachedDoc, true);
+ }
+ });
+ if (document['$$anchor']) {
+ self.anchor[document['$$anchor']] = document['$id'];
+ }
+ if (callback)
+ callback(document);
+ });
+ },
+
+ remove: function(document, callback) {
+ var self = this;
+ var data = {};
+ document['$saveTo'](data);
+ this._jsonRequest(["DELETE", "", data], function(response) {
+ delete self._cache[document['$entity'] + '/' + document['$id']];
+ _.each(self._cache_collections, function(collection){
+ for ( var i = 0; i < collection.length; i++) {
+ var item = collection[i];
+ if (item['$id'] == document['$id']) {
+ collection.splice(i, 1);
+ }
+ }
+ });
+ (callback||noop)(response);
+ });
+ },
+
+ _jsonRequest: function(request, callback, failure) {
+ request['$$callback'] = callback;
+ request['$$failure'] = failure||function(response){
+ throw response;
+ };
+ this.bulkRequest.push(request);
+ },
+
+ flush: function() {
+ if (this.bulkRequest.length === 0) return;
+ var self = this;
+ var bulkRequest = this.bulkRequest;
+ this.bulkRequest = [];
+ log('REQUEST:', bulkRequest);
+ function callback(code, bulkResponse){
+ log('RESPONSE[' + code + ']: ', bulkResponse);
+ if(bulkResponse['$status_code'] == 401) {
+ self.users['login'](function(){
+ self.post(bulkRequest, callback);
+ });
+ } else if(bulkResponse['$status_code']) {
+ alert(toJson(bulkResponse));
+ } else {
+ for ( var i = 0; i < bulkResponse.length; i++) {
+ var response = bulkResponse[i];
+ var request = bulkRequest[i];
+ var responseCode = response['$status_code'];
+ if(responseCode) {
+ if(responseCode == 403) {
+ self.users['notAuthorized']();
+ } else {
+ request['$$failure'](response);
+ }
+ } else {
+ request['$$callback'](response);
+ }
+ }
+ }
+ }
+ this.post(bulkRequest, callback);
+ },
+
+ saveScope: function(scope, callback) {
+ var saveCounter = 1;
+ function onSaveDone() {
+ saveCounter--;
+ if (saveCounter === 0 && callback)
+ callback();
+ }
+ for(var key in scope) {
+ var item = scope[key];
+ if (item && item['$save'] == Model.prototype['$save']) {
+ saveCounter++;
+ item['$save'](onSaveDone);
+ }
+ }
+ onSaveDone();
+ },
+
+ query: function(type, query, arg, callback){
+ var self = this;
+ var queryList = [];
+ queryList['$$accept'] = function(doc){
+ return false;
+ };
+ this._cache_collections.push(queryList);
+ var request = type['title'] + '/' + query + '=' + arg;
+ this._jsonRequest(["GET", request], function(response){
+ var list = response;
+ foreach(list, function(item){
+ var document = type()['$loadFrom'](item);
+ queryList.push(self.cache(document));
+ });
+ (callback||noop)(queryList);
+ });
+ return queryList;
+ },
+
+ entities: function(callback) {
+ var entities = [];
+ var self = this;
+ this._jsonRequest(["GET", "$entities"], function(response) {
+ foreach(response, function(value, entityName){
+ entities.push(self.entity(entityName));
+ });
+ entities.sort(function(a,b){return a.title > b.title ? 1 : -1;});
+ (callback||noop)(entities);
+ });
+ return entities;
+ },
+
+ documentCountsByUser: function(){
+ var counts = {};
+ var self = this;
+ self.post([["GET", "$users"]], function(code, response){
+ extend(counts, response[0]);
+ });
+ return counts;
+ },
+
+ userDocumentIdsByEntity: function(user){
+ var ids = {};
+ var self = this;
+ self.post([["GET", "$users/" + user]], function(code, response){
+ extend(ids, response[0]);
+ });
+ return ids;
+ },
+
+ entity: function(name, defaults){
+ if (!name) {
+ return DataStore.NullEntity;
+ }
+ var self = this;
+ var entity = extend(function(initialState){
+ return new Model(entity, initialState);
+ }, {
+ // entity.name does not work as name seems to be reserved for functions
+ 'title': name,
+ '$$factory': true,
+ datastore: this, //private, obfuscate
+ 'defaults': defaults || {},
+ 'load': function(id, callback){
+ return self.load(entity(), id, callback);
+ },
+ 'loadMany': function(ids, callback){
+ return self.loadMany(entity, ids, callback);
+ },
+ 'loadOrCreate': function(id, callback){
+ return self.loadOrCreate(entity(), id, callback);
+ },
+ 'all': function(callback){
+ return self.loadAll(entity, callback);
+ },
+ 'query': function(query, queryArgs, callback){
+ return self.query(entity, query, queryArgs, callback);
+ },
+ 'properties': function(callback) {
+ self._jsonRequest(["GET", name + "/$properties"], callback);
+ }
+ });
+ return entity;
+ },
+
+ join: function(join){
+ function fn(){
+ throw "Joined entities can not be instantiated into a document.";
+ };
+ function base(name){return name ? name.substring(0, name.indexOf('.')) : undefined;}
+ function next(name){return name.substring(name.indexOf('.') + 1);}
+ var joinOrder = _(join).chain().
+ map(function($, name){
+ return name;}).
+ sortBy(function(name){
+ var path = [];
+ do {
+ if (_(path).include(name)) throw "Infinite loop in join: " + path.join(" -> ");
+ path.push(name);
+ if (!join[name]) throw _("Named entity '<%=name%>' is undefined.").template({name:name});
+ name = base(join[name].on);
+ } while(name);
+ return path.length;
+ }).
+ value();
+ if (_(joinOrder).select(function($){return join[$].on;}).length != joinOrder.length - 1)
+ throw "Exactly one entity needs to be primary.";
+ fn['query'] = function(exp, value) {
+ var joinedResult = [];
+ var baseName = base(exp);
+ if (baseName != joinOrder[0]) throw _("Named entity '<%=name%>' is not a primary entity.").template({name:baseName});
+ var Entity = join[baseName].join;
+ var joinIndex = 1;
+ Entity['query'](next(exp), value, function(result){
+ var nextJoinName = joinOrder[joinIndex++];
+ var nextJoin = join[nextJoinName];
+ var nextJoinOn = nextJoin.on;
+ var joinIds = {};
+ _(result).each(function(doc){
+ var row = {};
+ joinedResult.push(row);
+ row[baseName] = doc;
+ var id = Scope.getter(row, nextJoinOn);
+ joinIds[id] = id;
+ });
+ nextJoin.join.loadMany(_.toArray(joinIds), function(result){
+ var byId = {};
+ _(result).each(function(doc){
+ byId[doc.$id] = doc;
+ });
+ _(joinedResult).each(function(row){
+ var id = Scope.getter(row, nextJoinOn);
+ row[nextJoinName] = byId[id];
+ });
+ });
+ });
+ return joinedResult;
+ };
+ return fn;
+ }
+};
diff --git a/src/moveToAngularCom/Server.js b/src/moveToAngularCom/Server.js
new file mode 100644
index 00000000..5c4ec3c6
--- /dev/null
+++ b/src/moveToAngularCom/Server.js
@@ -0,0 +1,68 @@
+function Server(url, getScript) {
+ this.url = url;
+ this.nextId = 0;
+ this.getScript = getScript;
+ this.uuid = "_" + ("" + Math.random()).substr(2) + "_";
+ this.maxSize = 1800;
+};
+
+Server.prototype = {
+ base64url: function(txt) {
+ return Base64.encode(txt);
+ },
+
+ request: function(method, url, request, callback) {
+ var requestId = this.uuid + (this.nextId++);
+ var payload = this.base64url(toJson({'u':url, 'm':method, 'p':request}));
+ var totalPockets = Math.ceil(payload.length / this.maxSize);
+ var baseUrl = this.url + "/$/" + requestId + "/" + totalPockets + "/";
+ angularCallbacks[requestId] = function(response) {
+ delete angularCallbacks[requestId];
+ callback(200, response);
+ };
+ for ( var pocketNo = 0; pocketNo < totalPockets; pocketNo++) {
+ var pocket = payload.substr(pocketNo * this.maxSize, this.maxSize);
+ this.getScript(baseUrl + (pocketNo+1) + "?h=" + pocket, noop);
+ }
+ }
+};
+
+function FrameServer(frame) {
+ this.frame = frame;
+};
+FrameServer.PREFIX = "$DATASET:";
+
+FrameServer.prototype = {
+ read:function(){
+ this.data = fromJson(this.frame.name.substr(FrameServer.PREFIX.length));
+ },
+ write:function(){
+ this.frame.name = FrameServer.PREFIX + toJson(this.data);
+ },
+ request: function(method, url, request, callback) {
+ //alert(method + " " + url + " " + toJson(request) + " " + toJson(callback));
+ }
+};
+
+
+function VisualServer(delegate, status, update) {
+ this.delegate = delegate;
+ this.update = update;
+ this.status = status;
+};
+
+VisualServer.prototype = {
+ request:function(method, url, request, callback) {
+ var self = this;
+ this.status.beginRequest(request);
+ this.delegate.request(method, url, request, function() {
+ self.status.endRequest();
+ try {
+ callback.apply(this, arguments);
+ } catch (e) {
+ alert(toJson(e));
+ }
+ self.update();
+ });
+ }
+};
diff --git a/src/moveToAngularCom/Users.js b/src/moveToAngularCom/Users.js
new file mode 100644
index 00000000..fb5845d3
--- /dev/null
+++ b/src/moveToAngularCom/Users.js
@@ -0,0 +1,35 @@
+function Users(server, controlBar) {
+ this.server = server;
+ this.controlBar = controlBar;
+};
+
+extend(Users.prototype, {
+ 'fetchCurrentUser':function(callback) {
+ var self = this;
+ this.server.request("GET", "/account.json", {}, function(code, response){
+ self['current'] = response['user'];
+ callback(response['user']);
+ });
+ },
+
+ 'logout': function(callback) {
+ var self = this;
+ this.controlBar.logout(function(){
+ delete self['current'];
+ (callback||noop)();
+ });
+ },
+
+ 'login': function(callback) {
+ var self = this;
+ this.controlBar.login(function(){
+ self['fetchCurrentUser'](function(){
+ (callback||noop)();
+ });
+ });
+ },
+
+ 'notAuthorized': function(){
+ this.controlBar.notAuthorized();
+ }
+});
diff --git a/src/moveToAngularCom/directivesAngularCom.js b/src/moveToAngularCom/directivesAngularCom.js
new file mode 100644
index 00000000..84032bdd
--- /dev/null
+++ b/src/moveToAngularCom/directivesAngularCom.js
@@ -0,0 +1,29 @@
+
+angular.directive("auth", function(expression, element){
+ return function(){
+ if(expression == "eager") {
+ this.$users.fetchCurrent();
+ }
+ };
+});
+
+
+//expression = "book=Book:{year=2000}"
+angular.directive("entity", function(expression, element){
+ //parse expression, ignore element
+ var entityName; // "Book";
+ var instanceName; // "book";
+ var defaults; // {year: 2000};
+
+ parse(expression);
+
+ return function(){
+ this[entityName] = this.$datastore.entity(entityName, defaults);
+ this[instanceName] = this[entityName]();
+ this.$watch("$anchor."+instanceName, function(newAnchor){
+ this[instanceName] = this[entityName].get(this.$anchor[instanceName]);
+ });
+ };
+});
+
+
diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js
new file mode 100644
index 00000000..194a28d6
--- /dev/null
+++ b/src/scenario/DSL.js
@@ -0,0 +1,63 @@
+angular.scenario.dsl.browser = {
+ navigateTo: function(url){
+ $scenario.addStep('Navigate to: ' + url, function(done){
+ var self = this;
+ this.testFrame.load(function(){
+ self.testFrame.unbind();
+ self.testWindow = self.testFrame[0].contentWindow;
+ self.testDocument = jQuery(self.testWindow.document);
+ self.$browser = self.testWindow.angular.service.$browser();
+ self.notifyWhenNoOutstandingRequests = bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests);
+ self.notifyWhenNoOutstandingRequests(done);
+ });
+ if (this.testFrame.attr('src') == url) {
+ this.testFrame[0].contentWindow.location.reload();
+ } else {
+ this.testFrame.attr('src', url);
+ }
+ });
+ }
+};
+
+angular.scenario.dsl.input = function(selector) {
+ return {
+ enter: function(value){
+ $scenario.addStep("Set input text of '" + selector + "' to '" +
+ value + "'", function(done){
+ var input = this.testDocument.find('input[name=' + selector + ']');
+ input.val(value);
+ this.testWindow.angular.element(input[0]).trigger('change');
+ done();
+ });
+ },
+ select: function(value){
+ $scenario.addStep("Select radio '" + selector + "' to '" +
+ value + "'", function(done){
+ var input = this.testDocument.
+ find(':radio[name$=@' + selector + '][value=' + value + ']');
+ var event = this.testWindow.document.createEvent('MouseEvent');
+ event.initMouseEvent('click', true, true, this.testWindow, 0,0,0,0,0, false, false, false, false, 0, null);
+ input[0].dispatchEvent(event);
+ done();
+ });
+ }
+ };
+};
+
+angular.scenario.dsl.expect = {
+ repeater: function(selector) {
+ return {
+ count: {
+ toEqual: function(number) {
+ $scenario.addStep("Expect that there are " + number + " items in Repeater with selector '" + selector + "'", function(done) {
+ var items = this.testDocument.find(selector);
+ if (items.length != number) {
+ this.result.fail("Expected " + number + " but was " + items.length);
+ }
+ done();
+ });
+ }
+ }
+ };
+ }
+};
diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js
new file mode 100644
index 00000000..8e0cc909
--- /dev/null
+++ b/src/scenario/Runner.js
@@ -0,0 +1,161 @@
+angular['scenario'] = angular['scenario'] || (angular['scenario'] = {});
+angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {});
+
+angular.scenario.Runner = function(scope, jQuery){
+ var self = scope.$scenario = this;
+ this.scope = scope;
+ this.jQuery = jQuery;
+
+ var specs = this.specs = {};
+ var path = [];
+ this.scope.describe = function(name, body){
+ path.push(name);
+ body();
+ path.pop();
+ };
+ var beforeEach = noop;
+ var afterEach = noop;
+ this.scope.beforeEach = function(body) {
+ beforeEach = body;
+ };
+ this.scope.afterEach = function(body) {
+ afterEach = body;
+ };
+ this.scope.it = function(name, body) {
+ var specName = path.join(' ') + ': it ' + name;
+ self.currentSpec = specs[specName] = {
+ name: specName,
+ steps:[]
+ };
+ try {
+ beforeEach();
+ body();
+ } catch(err) {
+ self.addStep(err.message || 'ERROR', function(){
+ throw err;
+ });
+ } finally {
+ afterEach();
+ }
+ self.currentSpec = null;
+ };
+ this.logger = function returnNoop(){
+ return extend(returnNoop, {close:noop, fail:noop});;
+ };
+};
+
+angular.scenario.Runner.prototype = {
+ run: function(body){
+ var jQuery = this.jQuery;
+ body.append(
+ '<div id="runner">' +
+ '<div class="console"></div>' +
+ '</div>' +
+ '<div id="testView">' +
+ '<iframe></iframe>' +
+ '</div>');
+ var console = body.find('#runner .console');
+ console.find('li').live('click', function(){
+ jQuery(this).toggleClass('collapsed');
+ });
+ this.testFrame = body.find('#testView iframe');
+ function logger(parent) {
+ var container;
+ return function(type, text) {
+ if (!container) {
+ container = jQuery('<ul></ul>');
+ parent.append(container);
+ }
+ var element = jQuery('<li class="running '+type+'"><span></span></li>');
+ element.find('span').text(text);
+ container.append(element);
+ return extend(logger(element), {
+ close: function(){
+ element.removeClass('running');
+ if(!element.hasClass('fail'))
+ element.addClass('collapsed');
+ console.scrollTop(console[0].scrollHeight);
+ },
+ fail: function(){
+ element.removeClass('running');
+ var current = element;
+ while (current[0] != console[0]) {
+ if (current.is('li'))
+ current.addClass('fail');
+ current = current.parent();
+ }
+ }
+ });
+ };
+ }
+ this.logger = logger(console);
+ var specNames = [];
+ foreach(this.specs, function(spec, name){
+ specNames.push(name);
+ }, this);
+ specNames.sort();
+ var self = this;
+ function callback(){
+ var next = specNames.shift();
+ if(next) {
+ self.execute(next, callback);
+ }
+ };
+ callback();
+ },
+
+ addStep: function(name, step) {
+ this.currentSpec.steps.push({name:name, fn:step});
+ },
+
+ execute: function(name, callback) {
+ var spec = this.specs[name],
+ self = this,
+ result = {
+ passed: false,
+ failed: false,
+ finished: false,
+ fail: function(error) {
+ result.passed = false;
+ result.failed = true;
+ result.error = error;
+ result.log('fail', isString(error) ? error : toJson(error)).fail();
+ }
+ },
+ specThis = createScope({
+ result: result,
+ testFrame: this.testFrame,
+ testWindow: this.testWindow
+ }, angularService, {});
+ this.self = specThis;
+ var stepLogger = this.logger('spec', name);
+ spec.nextStepIndex = 0;
+ function done() {
+ result.finished = true;
+ stepLogger.close();
+ self.self = null;
+ (callback||noop).call(specThis);
+ }
+ function next(){
+ var step = spec.steps[spec.nextStepIndex];
+ (result.log || {close:noop}).close();
+ result.log = null;
+ if (step) {
+ spec.nextStepIndex ++;
+ result.log = stepLogger('step', step.name);
+ try {
+ step.fn.call(specThis, next);
+ } catch (e) {
+ console.error(e);
+ result.fail(e);
+ done();
+ }
+ } else {
+ result.passed = !result.failed;
+ done();
+ }
+ };
+ next();
+ return specThis;
+ }
+}; \ No newline at end of file
diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix
new file mode 100644
index 00000000..5b44e17c
--- /dev/null
+++ b/src/scenario/angular.prefix
@@ -0,0 +1,30 @@
+/**
+ * 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){
+ window.angular = {
+ scenario: {
+ dsl: window
+ }
+ };
+
diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix
new file mode 100644
index 00000000..fc861cbf
--- /dev/null
+++ b/src/scenario/angular.suffix
@@ -0,0 +1,11 @@
+
+ var $scenarioRunner = new angular.scenario.Runner(window, jQuery);
+
+ window.onload = function(){
+ try {
+ if (previousOnLoad) previousOnLoad();
+ } catch(e) {}
+ $scenarioRunner.run(jQuery(window.document.body));
+ };
+
+})(window, document, window.onload);
diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js
new file mode 100644
index 00000000..694d0e97
--- /dev/null
+++ b/src/scenario/bootstrap.js
@@ -0,0 +1,44 @@
+(function(onLoadDelegate){
+ var prefix = (function(){
+ var filename = /(.*\/)bootstrap.js(#(.*))?/;
+ var scripts = document.getElementsByTagName("script");
+ for(var j = 0; j < scripts.length; j++) {
+ var src = scripts[j].src;
+ if (src && src.match(filename)) {
+ var parts = src.match(filename);
+ return parts[1];
+ }
+ }
+ })();
+ function addScript(path) {
+ document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
+ }
+
+ function addCSS(path) {
+ document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
+ }
+
+ window.angular = {
+ scenario: {
+ dsl: window
+ }
+ };
+
+ window.onload = function(){
+ _.defer(function(){
+ $scenarioRunner.run(jQuery(window.document.body));
+ });
+ (onLoadDelegate||function(){})();
+ };
+ addCSS("../../css/angular-scenario.css");
+ addScript("../../lib/underscore/underscore.js");
+ addScript("../../lib/jquery/jquery-1.4.2.js");
+ addScript("Runner.js");
+ addScript("../Angular.js");
+ addScript("../JSON.js");
+ addScript("DSL.js");
+ document.write('<script type="text/javascript">' +
+ '$scenarioRunner = new angular.scenario.Runner(window, jQuery);' +
+ '</script>');
+})(window.onload);
+
diff --git a/src/services.js b/src/services.js
new file mode 100644
index 00000000..64f2ea4f
--- /dev/null
+++ b/src/services.js
@@ -0,0 +1,361 @@
+angularService("$window", bind(window, identity, window));
+angularService("$document", function(window){
+ return jqLite(window.document);
+}, {inject:['$window']});
+
+var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/;
+var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/;
+var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21};
+angularService("$location", function(browser){
+ var scope = this, location = {parse:parseUrl, toString:toString};
+ var lastHash, lastUrl;
+ function parseUrl(url){
+ if (isDefined(url)) {
+ var match = URL_MATCH.exec(url);
+ if (match) {
+ location.href = url;
+ location.protocol = match[1];
+ location.host = match[3] || '';
+ location.port = match[5] || DEFAULT_PORTS[location.href] || null;
+ location.path = match[6];
+ location.search = parseKeyValue(match[8]);
+ location.hash = match[9] || '';
+ if (location.hash)
+ location.hash = location.hash.substr(1);
+ parseHash(location.hash);
+ }
+ }
+ }
+ function parseHash(hash) {
+ var match = HASH_MATCH.exec(hash);
+ location.hashPath = match[1] || '';
+ location.hashSearch = parseKeyValue(match[3]);
+ lastHash = hash;
+ }
+ function toString() {
+ if (lastHash === location.hash) {
+ var hashKeyValue = toKeyValue(location.hashSearch),
+ hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''),
+ url = location.href.split('#')[0] + '#' + (hash ? hash : '');
+ if (url !== location.href) parseUrl(url);
+ return url;
+ } else {
+ parseUrl(location.href.split('#')[0] + '#' + location.hash);
+ return toString();
+ }
+ }
+ browser.watchUrl(function(url){
+ parseUrl(url);
+ scope.$root.$eval();
+ });
+ parseUrl(browser.getUrl());
+ this.$onEval(PRIORITY_FIRST, function(){
+ if (location.hash != lastHash) {
+ parseHash(location.hash);
+ }
+ });
+ this.$onEval(PRIORITY_LAST, function(){
+ var url = toString();
+ if (lastUrl != url) {
+ browser.setUrl(url);
+ lastUrl = url;
+ }
+ });
+ return location;
+}, {inject: ['$browser']});
+
+angularService("$log", function($window){
+ var console = $window.console,
+ log = console && console.log || noop;
+ return {
+ log: log,
+ warn: console && console.warn || log,
+ info: console && console.info || log,
+ error: console && console.error || log
+ };
+}, {inject:['$window']});
+
+angularService("$hover", function(browser) {
+ var tooltip, self = this, error, width = 300, arrowWidth = 10;
+ 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);
+ self.$browser.body.append(tooltip.callout);
+ }
+ var docRect = self.$browser.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;
+ }
+ });
+}, {inject:['$browser']});
+
+angularService("$invalidWidgets", function(){
+ var invalidWidgets = [];
+ invalidWidgets.markValid = function(element){
+ var index = indexOf(invalidWidgets, element);
+ if (index != -1)
+ invalidWidgets.splice(index, 1);
+ };
+ invalidWidgets.markInvalid = function(element){
+ var index = indexOf(invalidWidgets, element);
+ if (index === -1)
+ invalidWidgets.push(element);
+ };
+ invalidWidgets.visible = function() {
+ var count = 0;
+ foreach(invalidWidgets, function(widget){
+ count = count + (isVisible(widget) ? 1 : 0);
+ });
+ return count;
+ };
+ invalidWidgets.clearOrphans = function() {
+ for(var i = 0; i < invalidWidgets.length;) {
+ var widget = invalidWidgets[i];
+ if (isOrphan(widget[0])) {
+ invalidWidgets.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ };
+ function isOrphan(widget) {
+ if (widget == window.document) return false;
+ var parent = widget.parentNode;
+ return !parent || isOrphan(parent);
+ }
+ return invalidWidgets;
+});
+
+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;
+}
+
+angularService('$route', function(location, params){
+ var routes = {},
+ onChange = [],
+ matcher = switchRouteMatcher,
+ parentScope = this,
+ dirty = 0,
+ $route = {
+ routes: routes,
+ onChange: bind(onChange, onChange.push),
+ 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);
+ parentScope.$tryEval(childScope.init);
+ }
+ }
+ this.$watch(function(){return dirty + location.hash;}, updateRoute);
+ return $route;
+}, {inject: ['$location']});
+
+angularService('$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);
+ }
+ 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();
+ }
+ });
+ };
+}, {inject:['$browser', '$xhr.error', '$log']});
+
+angularService('$xhr.error', function($log){
+ return function(request, response){
+ $log.error('ERROR: XHR: ' + request.url, request, response);
+ };
+}, {inject:['$log']});
+
+angularService('$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;
+}, {inject:['$xhr', '$xhr.error', '$log']});
+
+angularService('$xhr.cache', function($xhr){
+ var inflight = {}, self = this;;
+ function cache(method, url, post, callback, cacheThenRetrieve){
+ if (isFunction(post)) {
+ callback = post;
+ post = null;
+ }
+ if (method == 'GET') {
+ var data;
+ if (data = cache.data[url]) {
+ callback(200, copy(data.value));
+ if (!cacheThenRetrieve)
+ 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;
+}, {inject:['$xhr.bulk']});
+
+angularService('$resource', function($xhr){
+ var resource = new ResourceFactory($xhr);
+ return bind(resource, resource.route);
+}, {inject: ['$xhr.cache']});
diff --git a/src/validators.js b/src/validators.js
new file mode 100644
index 00000000..5c7fc952
--- /dev/null
+++ b/src/validators.js
@@ -0,0 +1,132 @@
+foreach({
+ 'noop': function() { return null; },
+
+ 'regexp': function(value, regexp, msg) {
+ if (!value.match(regexp)) {
+ return msg ||
+ "Value does not match expected format " + regexp + ".";
+ } else {
+ return null;
+ }
+ },
+
+ '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";
+ }
+ },
+
+ '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;
+ },
+
+ 'date': function(value, min, max) {
+ if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) {
+ return null;
+ }
+ return "Value is not a date. (Expecting format: 12/31/2009).";
+ },
+
+ 'ssn': function(value) {
+ if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) {
+ return null;
+ }
+ return "SSN needs to be in 999-99-9999 format.";
+ },
+
+ '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.";
+ },
+
+ '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.";
+ },
+
+ '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.";
+ },
+
+ 'json': function(value) {
+ try {
+ fromJson(value);
+ return null;
+ } catch (e) {
+ return e.toString();
+ }
+ },
+
+ /*
+ * 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;
+ }
+
+}, function(v,k) {angularValidator[k] = v;});
diff --git a/src/widgets.js b/src/widgets.js
new file mode 100644
index 00000000..efafa9c5
--- /dev/null
+++ b/src/widgets.js
@@ -0,0 +1,328 @@
+function modelAccessor(scope, element) {
+ var expr = element.attr('name');
+ if (!expr) throw "Required field 'name' not found.";
+ 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.";
+ return {
+ get: function() {
+ return formatter.format(accessor.get());
+ },
+ set: function(value) {
+ return accessor.set(formatter.parse(value));
+ }
+ };
+}
+
+function compileValidator(expr) {
+ return new Parser(expr).validator()();
+}
+
+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 = extend(new (extend(function(){}, {prototype: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()),
+ 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);
+}
+
+function inputWidget(events, modelAccessor, viewAccessor, initFn) {
+ return function(element) {
+ var scope = this,
+ model = modelAccessor(scope, element),
+ view = viewAccessor(scope, element),
+ action = element.attr('ng-change') || '',
+ lastValue;
+ 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(){
+ model.set(view.get());
+ lastValue = model.get();
+ scope.$tryEval(action, element);
+ scope.$root.$eval();
+ // if we have noop initFn than we are just a button,
+ // therefore we want to prevent default action
+ return initFn != noop;
+ });
+ }
+ view.set(lastValue = model.get());
+ 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);
+});
+
+
+angularWidget('NG:INCLUDE', function(element){
+ var compiler = this,
+ srcExp = element.attr("src"),
+ scopeExp = element.attr("scope") || '';
+ if (element[0]['ng-compiled']) {
+ this.descend(true);
+ this.directives(true);
+ } else {
+ element[0]['ng-compiled'] = true;
+ return function(element){
+ var scope = this, childScope;
+ var changeCounter = 0;
+ function incrementChange(){ changeCounter++;}
+ this.$watch(srcExp, incrementChange);
+ this.$watch(scopeExp, incrementChange);
+ scope.$onEval(function(){
+ if (childScope) childScope.$eval();
+ });
+ this.$watch(function(){return changeCounter;}, function(){
+ var src = this.$eval(srcExp),
+ useScope = this.$eval(scopeExp);
+ if (src) {
+ scope.$xhr.cache('GET', src, function(code, response){
+ element.html(response);
+ childScope = useScope || createScope(scope);
+ compiler.compile(element)(element, childScope);
+ childScope.$init();
+ });
+ }
+ });
+ };
+ }
+});
+
+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.";
+ eachNode(element, function(caseElement){
+ var when = caseElement.attr('ng-switch-when');
+ if (when) {
+ cases.push({
+ when: function(scope, value){
+ var args = [value, when];
+ foreach(usingExprParams, function(arg){
+ args.push(arg);
+ });
+ return usingFn.apply(scope, args);
+ },
+ change: changeExpr,
+ element: caseElement,
+ template: compiler.compile(caseElement)
+ });
+ }
+ });
+
+ // 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){
+ element.html('');
+ childScope = createScope(scope);
+ foreach(cases, function(switchCase){
+ if (switchCase.when(childScope, value)) {
+ var caseElement = switchCase.element.clone();
+ element.append(caseElement);
+ childScope.$tryEval(switchCase.change, element);
+ switchCase.template(caseElement, childScope);
+ if (scope.$invalidWidgets)
+ scope.$invalidWidgets.clearOrphans();
+ childScope.$init();
+ }
+ });
+ });
+ scope.$onEval(function(){
+ if (childScope) childScope.$eval();
+ });
+ };
+}, {
+ equals: function(on, when) {
+ return on == when;
+ },
+ route: switchRouteMatcher
+});