diff options
| author | Rob Spies | 2010-06-22 17:09:55 -0700 |
|---|---|---|
| committer | Rob Spies | 2010-06-22 17:09:55 -0700 |
| commit | 1500e91defa4020bfe9608749b25e585cd1d8e3d (patch) | |
| tree | 8c2872252b62567dc4eb00f7d7547661d5674c55 /src | |
| parent | eaa397c76b7d28343cde9f3a0338b9b0e79197c8 (diff) | |
| parent | b129a1094e6b42ed82c3ccecc2f40daaa0a6cb6a (diff) | |
| download | angular.js-1500e91defa4020bfe9608749b25e585cd1d8e3d.tar.bz2 | |
Merge http://github.com/angular/angular.js into angular
Conflicts:
.gitignore
Diffstat (limited to 'src')
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, '&'). + replace(/</g, '<'). + replace(/>/g, '>'); +} + + +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, '<').replace(/>/g, '>').replace(/\"/g, + '"'); +} + +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 = ' ' + 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> </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><angular/></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 +}); |
