diff options
| author | Adam Abrons | 2010-01-05 16:36:58 -0800 |
|---|---|---|
| committer | Adam Abrons | 2010-01-05 16:36:58 -0800 |
| commit | c9c176a53b1632ca2b1c6ed27382ab72ac21d45d (patch) | |
| tree | b5f719a095c03ee9f8b2721ffdaf1e5ff8c11b41 /src | |
| download | angular.js-c9c176a53b1632ca2b1c6ed27382ab72ac21d45d.tar.bz2 | |
angular.js
Diffstat (limited to 'src')
| -rw-r--r-- | src/API.js | 318 | ||||
| -rw-r--r-- | src/Binder.js | 341 | ||||
| -rw-r--r-- | src/ControlBar.js | 71 | ||||
| -rw-r--r-- | src/DataStore.js | 332 | ||||
| -rw-r--r-- | src/Filters.js | 290 | ||||
| -rw-r--r-- | src/JSON.js | 92 | ||||
| -rw-r--r-- | src/Loader.js | 389 | ||||
| -rw-r--r-- | src/Model.js | 65 | ||||
| -rw-r--r-- | src/Parser.js | 741 | ||||
| -rw-r--r-- | src/Scope.js | 198 | ||||
| -rw-r--r-- | src/Server.js | 69 | ||||
| -rw-r--r-- | src/Users.js | 36 | ||||
| -rw-r--r-- | src/Validators.js | 80 | ||||
| -rw-r--r-- | src/Widgets.js | 774 | ||||
| -rw-r--r-- | src/Widgets.js.orig | 764 | ||||
| -rw-r--r-- | src/XSitePost.js | 100 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 100 | ||||
| -rw-r--r-- | src/test/Runner.js | 160 | ||||
| -rw-r--r-- | src/test/Steps.js | 57 | ||||
| -rw-r--r-- | src/test/_namespace.js | 5 |
20 files changed, 4982 insertions, 0 deletions
diff --git a/src/API.js b/src/API.js new file mode 100644 index 00000000..c51fe01d --- /dev/null +++ b/src/API.js @@ -0,0 +1,318 @@ +angular.Global = { + typeOf:function(obj){ + var type = typeof obj; + switch(type) { + case "object": + if (obj === null) return "null"; + if (obj instanceof Array) return "array"; + if (obj instanceof Date) return "date"; + if (obj.nodeType == 1) return "element"; + } + return type; + } +}; + +angular.Collection = {}; +angular.Object = {}; +angular.Array = { + 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); + _.detect(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 getter = nglr.Scope.getter; + 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); + return _.reduce(array, 0, function(count, $){return count + (fn($)?1:0);}); + }, + orderBy:function(array, expression, descend) { + function reverse(comp, descending) { + return nglr.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 = $ ? angular.Function.compile($) : _.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; + }; + return _.clone(array).sort(reverse(comparator, descend)); + }, + orderByToggle:function(predicate, attribute) { + var STRIP = /^([+|-])?(.*)/; + var ascending = false; + var index = -1; + _.detect(predicate, function($, i){ + 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; + } + nglr.merge(mergeValue, value); + return array; + } +}; +angular.String = { + 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; + } +}; +angular.Date = { + toString:function(date){ + function pad(n) { return n < 10 ? "0" + n : n; } + return (date.getUTCFullYear()) + '-' + + pad(date.getUTCMonth() + 1) + '-' + + pad(date.getUTCDate()) + 'T' + + pad(date.getUTCHours()) + ':' + + pad(date.getUTCMinutes()) + ':' + + pad(date.getUTCSeconds()) + 'Z'; + } + }; +angular.Function = { + compile:function(expression) { + if (_.isFunction(expression)){ + return expression; + } else if (expression){ + var scope = new nglr.Scope(); + return function($) { + scope.state = $; + return scope.eval(expression); + }; + } else { + return function($){return $;}; + } + } +}; + +(function(){ + function extend(dst, src, names){ + _.extend(dst, src); + _.each((names||[]), function(name){ + dst[name] = _[name]; + }); + }; + extend(angular.Global, {}, + ['extend', 'clone','isEqual', + 'isElement', 'isArray', 'isFunction', 'isUndefined']); + extend(angular.Collection, angular.Global, + ['each', 'map', 'reduce', 'reduceRight', 'detect', + 'select', 'reject', 'all', 'any', 'include', + 'invoke', 'pluck', 'max', 'min', 'sortBy', + 'sortedIndex', 'toArray', 'size']); + extend(angular.Array, angular.Collection, + ['first', 'last', 'compact', 'flatten', 'without', + 'uniq', 'intersect', 'zip', 'indexOf', 'lastIndexOf']); + extend(angular.Object, angular.Collection, + ['keys', 'values']); + extend(angular.String, angular.Global); + extend(angular.Function, angular.Global, + ['bind', 'bindAll', 'delay', 'defer', 'wrap', 'compose']); +})();
\ No newline at end of file diff --git a/src/Binder.js b/src/Binder.js new file mode 100644 index 00000000..86e99fb8 --- /dev/null +++ b/src/Binder.js @@ -0,0 +1,341 @@ +// Copyright (C) 2009 BRAT Tech LLC +nglr.Binder = function(doc, widgetFactory, urlWatcher, config) { + this.doc = doc; + this.urlWatcher = urlWatcher; + this.anchor = {}; + this.widgetFactory = widgetFactory; + this.config = config || {}; + this.updateListeners = []; +}; + +nglr.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; +}; + +nglr.Binder.hasBinding = function(string) { + var bindings = nglr.Binder.parseBindings(string); + return bindings.length > 1 || nglr.Binder.binding(bindings[0]) !== null; +}; + +nglr.Binder.binding = function(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +}; + + +nglr.Binder.prototype.parseQueryString = function(query) { + var params = {}; + query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, + function (match, left, right) { + if (left) params[decodeURIComponent(left)] = decodeURIComponent(right); + }); + return params; +}; + +nglr.Binder.prototype.parseAnchor = function(url) { + var self = this; + url = url || this.urlWatcher.getUrl(); + + var anchorIndex = url.indexOf('#'); + if (anchorIndex < 0) return; + var anchor = url.substring(anchorIndex + 1); + + var anchorQuery = this.parseQueryString(anchor); + jQuery.each(self.anchor, function(key, newValue) { + delete self.anchor[key]; + }); + jQuery.each(anchorQuery, function(key, newValue) { + self.anchor[key] = newValue; + }); +}; + +nglr.Binder.prototype.onUrlChange = function (url) { + console.log("URL change detected", url); + this.parseAnchor(url); + this.updateView(); +}; + +nglr.Binder.prototype.updateAnchor = function() { + var url = this.urlWatcher.getUrl(); + 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.urlWatcher.setUrl(url); + return url; +}; + +nglr.Binder.prototype.updateView = function() { + var start = new Date().getTime(); + var scope = jQuery(this.doc).scope(); + scope.set("$invalidWidgets", []); + scope.updateView(); + var end = new Date().getTime(); + this.updateAnchor(); + _.each(this.updateListeners, function(fn) {fn();}); +}; + +nglr.Binder.prototype.executeInit = function() { + jQuery("[ng-init]", this.doc).each(function() { + var jThis = jQuery(this); + var scope = jThis.scope(); + try { + scope.eval(jThis.attr('ng-init')); + } catch (e) { + nglr.alert("EVAL ERROR:\n" + jThis.attr('ng-init') + '\n' + nglr.toJson(e, true)); + } + }); +}; + +nglr.Binder.prototype.entity = function (scope) { + jQuery("[ng-entity]", this.doc).attr("ng-watch", function() { + try { + var jNode = jQuery(this); + var decl = scope.entity(jNode.attr("ng-entity")); + return decl + (jNode.attr('ng-watch') || ""); + } catch (e) { + nglr.alert(e); + } + }); +}; + +nglr.Binder.prototype.compile = function() { + var jNode = jQuery(this.doc); + var self = this; + if (this.config.autoSubmit) { + var submits = jQuery(":submit", this.doc).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(), ""); + jQuery("a[ng-action]", this.doc).live('click', function (event) { + var jNode = jQuery(this); + try { + jNode.scope().eval(jNode.attr('ng-action')); + jNode.removeAttr('ng-error'); + jNode.removeClass("ng-exception"); + } catch (e) { + jNode.addClass("ng-exception"); + jNode.attr('ng-error', nglr.toJson(e, true)); + } + self.updateView(); + return false; + }); +}; + +nglr.Binder.prototype.translateBinding = function(node, parentPath, factories) { + var path = parentPath.concat(); + var offset = path.pop(); + var parts = nglr.Binder.parseBindings(node.nodeValue); + if (parts.length > 1 || nglr.Binder.binding(parts[0])) { + var parent = node.parentNode; + if (nglr.isLeafNode(parent)) { + parent.setAttribute('ng-bind-template', node.nodeValue); + factories.push({path:path, fn:function(node, scope, prefix) { + return new nglr.BindUpdater(node, node.getAttribute('ng-bind-template')); + }}); + } else { + for (var i = 0; i < parts.length; i++) { + var part = parts[i]; + var binding = nglr.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:nglr.Binder.prototype.ng_bind}); + } + } else if (nglr.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); + } +}; + +nglr.Binder.prototype.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) { + nglr.alert(e); + } + } + }; +}; + +nglr.Binder.prototype.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 ? nglr.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 = nglr.msie && attrName == 'href' ? + decodeURI(node.getAttribute(attrName, 2)) : attr.value; + if (nglr.Binder.hasBinding(attrValue)) { + bindings[attrName] = attrValue; + } + } + var json = nglr.toJson(bindings); + if (json.length > 2) { + node.setAttribute("ng-bind-attr", json); + } + } + + if (!node.getAttribute) console.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); + var template = function(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 nglr.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)) { + node.value = node.text; + } + } + + var children = node.childNodes; + for (var k = 0; k < children.length; k++) { + this.precompileNode(children[k], path.concat(k), factories); + } +}; + +nglr.Binder.prototype.ng_eval = function(node) { + return new nglr.EvalUpdater(node, node.getAttribute('ng-eval')); +}; + +nglr.Binder.prototype.ng_bind = function(node) { + return new nglr.BindUpdater(node, "{{" + node.getAttribute('ng-bind') + "}}"); +}; + +nglr.Binder.prototype.ng_bind_attr = function(node) { + return new nglr.BindAttrUpdater(node, nglr.fromJson(node.getAttribute('ng-bind-attr'))); +}; + +nglr.Binder.prototype.ng_hide = function(node) { + return new nglr.HideUpdater(node, node.getAttribute('ng-hide')); +}; + +nglr.Binder.prototype.ng_show = function(node) { + return new nglr.ShowUpdater(node, node.getAttribute('ng-show')); +}; + +nglr.Binder.prototype.ng_class = function(node) { + return new nglr.ClassUpdater(node, node.getAttribute('ng-class')); +}; + +nglr.Binder.prototype.ng_class_even = function(node) { + return new nglr.ClassEvenUpdater(node, node.getAttribute('ng-class-even')); +}; + +nglr.Binder.prototype.ng_class_odd = function(node) { + return new nglr.ClassOddUpdater(node, node.getAttribute('ng-class-odd')); +}; + +nglr.Binder.prototype.ng_style = function(node) { + return new nglr.StyleUpdater(node, node.getAttribute('ng-style')); +}; + +nglr.Binder.prototype.ng_watch = function(node, scope) { + scope.watch(node.getAttribute('ng-watch')); +}; diff --git a/src/ControlBar.js b/src/ControlBar.js new file mode 100644 index 00000000..3e1f0b57 --- /dev/null +++ b/src/ControlBar.js @@ -0,0 +1,71 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC + +nglr.ControlBar = function (document, serverUrl) { + this.document = document; + this.serverUrl = serverUrl; + this.window = window; + this.callbacks = []; +}; + +nglr.ControlBar.prototype.bind = function () { +}; + +nglr.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>'; + +nglr.ControlBar.prototype.login = function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/new.mini?return_url=" + encodeURIComponent(this.urlWithoutAnchor())); + } +}; + +nglr.ControlBar.prototype.logout = function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/do_destroy.mini"); + } +}; + +nglr.ControlBar.prototype.urlWithoutAnchor = function (path) { + return this.window.location.href.split("#")[0]; +}; + +nglr.ControlBar.prototype.doTemplate = function (path) { + var self = this; + var id = new Date().getTime(); + var url = this.urlWithoutAnchor(); + url += "#$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>' + }); + nglr["_iframe_notify_" + id] = function() { + loginView.dialog("destroy"); + loginView.remove(); + jQuery.each(self.callbacks, function(i, callback){ + callback(); + }); + self.callbacks = []; + }; +}; + +nglr.ControlBar.FORBIDEN = + '<div ng-non-bindable="true" title="Permission Error:">' + + 'Sorry, you do not have permission for this!'+ + '</div>'; + +nglr.ControlBar.prototype.notAuthorized = function () { + if (this.forbidenView) return; + this.forbidenView = jQuery(nglr.ControlBar.FORBIDEN); + this.forbidenView.dialog({bgiframe:true, height:70, modal:true}); +}; diff --git a/src/DataStore.js b/src/DataStore.js new file mode 100644 index 00000000..97ab92ff --- /dev/null +++ b/src/DataStore.js @@ -0,0 +1,332 @@ +// Copyright (C) 2009 BRAT Tech LLC + +nglr.DataStore = function(post, users, anchor) { + this.post = post; + this.users = users; + this._cache = {$collections:[]}; + this.anchor = anchor; + this.bulkRequest = []; +}; + +nglr.DataStore.prototype.cache = function(document) { + if (document.constructor != nglr.Model) { + throw "Parameter must be an instance of Entity! " + nglr.toJson(document); + } + var key = document.$entity + '/' + document.$id; + var cachedDocument = this._cache[key]; + if (cachedDocument) { + nglr.Model.copyDirectFields(document, cachedDocument); + } else { + this._cache[key] = document; + cachedDocument = document; + } + return cachedDocument; +}; + +nglr.DataStore.prototype.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||nglr.noop)(instance); + }, failure); + } + return instance; +}; + +nglr.DataStore.prototype.loadMany = function(entity, ids, callback) { + var self=this; + var list = []; + var callbackCount = 0; + jQuery.each(ids, function(i, id){ + list.push(self.load(entity(), id, function(){ + callbackCount++; + if (callbackCount == ids.length) { + (callback||nglr.noop)(list); + } + })); + }); + return list; +} + +nglr.DataStore.prototype.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||nglr.noop)(instance); + } else { + throw response; + } + }); +}; + +nglr.DataStore.prototype.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||nglr.noop)(list); + }); + return list; +}; + +nglr.DataStore.prototype.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)) { + angular.Array.includeIf(collection, cachedDoc, true); + } + }); + if (document.$$anchor) { + self.anchor[document.$$anchor] = document.$id; + } + if (callback) + callback(document); + }); +}; + +nglr.DataStore.prototype.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||nglr.noop)(response); + }); +}; + +nglr.DataStore.prototype._jsonRequest = function(request, callback, failure) { + request.$$callback = callback; + request.$$failure = failure||function(response){ + throw response; + }; + this.bulkRequest.push(request); +}; + +nglr.DataStore.prototype.flush = function() { + if (this.bulkRequest.length === 0) return; + var self = this; + var bulkRequest = this.bulkRequest; + this.bulkRequest = []; + console.log('REQUEST:', bulkRequest); + function callback(code, bulkResponse){ + console.log('RESPONSE[' + code + ']: ', bulkResponse); + if(bulkResponse.$status_code == 401) { + self.users.login(function(){ + self.post(bulkRequest, callback); + }); + } else if(bulkResponse.$status_code) { + nglr.alert(nglr.toJson(bulkResponse)); + } else { + for ( var i = 0; i < bulkResponse.length; i++) { + var response = bulkResponse[i]; + var request = bulkRequest[i]; + var code = response.$status_code; + if(code) { + if(code == 403) { + self.users.notAuthorized(); + } else { + request.$$failure(response); + } + } else { + request.$$callback(response); + } + } + } + } + this.post(bulkRequest, callback); +}; + +nglr.DataStore.prototype.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 == nglr.Model.prototype.$save) { + saveCounter++; + item.$save(onSaveDone); + } + } + onSaveDone(); +}; + +nglr.DataStore.prototype.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; + for(var i = 0; i < list.length; i++) { + var document = new type().$loadFrom(list[i]); + queryList.push(self.cache(document)); + } + if (callback) + callback(queryList); + }); + return queryList; +}; + +nglr.DataStore.prototype.entities = function(callback) { + var entities = []; + var self = this; + this._jsonRequest(["GET", "$entities"], function(response) { + for (var entityName in response) { + entities.push(self.entity(entityName)); + } + entities.sort(function(a,b){return a.title > b.title ? 1 : -1;}); + if (callback) callback(entities); + }); + return entities; +}; + +nglr.DataStore.prototype.documentCountsByUser = function(){ + var counts = {}; + var self = this; + self.post([["GET", "$users"]], function(code, response){ + jQuery.each(response[0], function(key, value){ + counts[key] = value; + }); + }); + return counts; +}; + +nglr.DataStore.prototype.userDocumentIdsByEntity = function(user){ + var ids = {}; + var self = this; + self.post([["GET", "$users/" + user]], function(code, response){ + jQuery.each(response[0], function(key, value){ + ids[key] = value; + }); + }); + return ids; +}; + +nglr.DataStore.NullEntity = function(){}; +nglr.DataStore.NullEntity.all = function(){return [];}; +nglr.DataStore.NullEntity.query = function(){return [];}; +nglr.DataStore.NullEntity.load = function(){return {};}; +nglr.DataStore.NullEntity.title = undefined; + +nglr.DataStore.prototype.entity = function(name, defaults){ + if (!name) { + return nglr.DataStore.NullEntity; + } + var self = this; + var entity = function(initialState){ + return new nglr.Model(entity, initialState); + }; + // entity.name does not work as name seems to be reserved for functions + entity.title = name; + entity.$$factory = true; + entity.datastore = this; + entity.defaults = defaults || {}; + entity.load = function(id, callback){ + return self.load(entity(), id, callback); + }; + entity.loadMany = function(ids, callback){ + return self.loadMany(entity, ids, callback); + }; + entity.loadOrCreate = function(id, callback){ + return self.loadOrCreate(entity(), id, callback); + }; + entity.all = function(callback){ + return self.loadAll(entity, callback); + }; + entity.query = function(query, queryArgs, callback){ + return self.query(entity, query, queryArgs, callback); + }; + entity.properties = function(callback) { + self._jsonRequest(["GET", name + "/$properties"], callback); + }; + return entity; +}; + +nglr.DataStore.prototype.join = function(join){ + var fn = function(){ + 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 = nglr.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 = nglr.Scope.getter(row, nextJoinOn); + row[nextJoinName] = byId[id]; + }); + }); + }); + return joinedResult; + }; + return fn; +}; diff --git a/src/Filters.js b/src/Filters.js new file mode 100644 index 00000000..f75f3603 --- /dev/null +++ b/src/Filters.js @@ -0,0 +1,290 @@ +// Copyright (C) 2009 BRAT Tech LLC + +angular.filter.Meta = function(obj){ + if (obj) { + for ( var key in obj) { + this[key] = obj[key]; + } + } +}; +angular.filter.Meta.get = function(obj, attr){ + attr = attr || 'text'; + switch(typeof obj) { + case "string": + return attr == "text" ? obj : undefined; + case "object": + if (obj && typeof obj[attr] !== "undefined") { + return obj[attr]; + } + return undefined; + default: + return obj; + } +}; + +angular.filter.currency = function(amount){ + jQuery(this.element).toggleClass('ng-format-negative', amount < 0); + return '$' + angular.filter.number.apply(this, [amount, 2]); +}; + +angular.filter.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; +}; + +angular.filter.date = function(amount) { +}; + +angular.filter.json = function(object) { + jQuery(this.element).addClass("ng-monospace"); + return nglr.toJson(object, true); +}; + +angular.filter.trackPackage = function(trackingNo, noMatch) { + trackingNo = nglr.trim(trackingNo); + var tNo = trackingNo.replace(/ /g, ''); + var MATCHERS = angular.filter.trackPackage.MATCHERS; + for ( var i = 0; i < MATCHERS.length; i++) { + var carrier = MATCHERS[i]; + for ( var j = 0; j < carrier.regexp.length; j++) { + var regexp = carrier.regexp[j]; + if (regexp.test(tNo)) { + var text = carrier.name + ": " + trackingNo; + var url = carrier.url + trackingNo; + return new angular.filter.Meta({ + text:text, + url:url, + html: '<a href="' + nglr.escapeAttr(url) + '">' + text + '</a>', + trackingNo:trackingNo}); + } + } + } + if (trackingNo) + return noMatch || + new angular.filter.Meta({text:trackingNo + " is not recognized"}); + else + return null; +}; + +angular.filter.trackPackage.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]}]; + +angular.filter.link = function(obj, title) { + var text = title || angular.filter.Meta.get(obj); + var url = angular.filter.Meta.get(obj, "url") || angular.filter.Meta.get(obj); + if (url) { + if (angular.validator.email(url) === null) { + url = "mailto:" + url; + } + var html = '<a href="' + nglr.escapeHtml(url) + '">' + text + '</a>'; + return new angular.filter.Meta({text:text, url:url, html:html}); + } + return obj; +}; + + +angular.filter.bytes = 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 + " " + angular.filter.bytes.SUFFIX[suffix]; +}; +angular.filter.bytes.SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + +angular.filter.image = function(obj, width, height) { + if (obj && obj.url) { + var style = ""; + if (width) { + style = ' style="max-width: ' + width + + 'px; max-height: ' + (height || width) + 'px;"'; + } + return new angular.filter.Meta({url:obj.url, text:obj.url, + html:'<img src="'+obj.url+'"' + style + '/>'}); + } + return null; +}; + +angular.filter.lowercase = function (obj) { + var text = angular.filter.Meta.get(obj); + return text ? ("" + text).toLowerCase() : text; +}; + +angular.filter.uppercase = function (obj) { + var text = angular.filter.Meta.get(obj); + return text ? ("" + text).toUpperCase() : text; +}; + +angular.filter.linecount = function (obj) { + var text = angular.filter.Meta.get(obj); + if (text==='' || !text) return 1; + return text.split(/\n|\f/).length; +}; + +angular.filter['if'] = function (result, expression) { + return expression ? result : undefined; +}; + +angular.filter.unless = function (result, expression) { + return expression ? undefined : result; +}; + +angular.filter.googleChartApi = function(type, data, width, height) { + data = data || {}; + var api = angular.filter.googleChartApi; + var chart = { + cht:type, + chco:api.collect(data, 'color'), + chtt:api.title(data), + chdl:api.collect(data, 'label'), + chd:api.values(data), + chf:'bg,s,FFFFFF00' + }; + if (_.isArray(data.xLabels)) { + chart.chxt='x'; + chart.chxl='0:|' + data.xLabels.join('|'); + } + return angular.filter.googleChartApi.encode(chart, width, height); +}; + +angular.filter.googleChartApi.values = function(data){ + var seriesValues = []; + _.each(data.series||[], function(serie){ + var values = []; + _.each(serie.values||[], function(value){ + values.push(value); + }); + seriesValues.push(values.join(',')); + }); + var values = seriesValues.join('|'); + return values === "" ? null : "t:" + values; +}; + +angular.filter.googleChartApi.title = function(data){ + var titles = []; + var title = data.title || []; + _.each(_.isArray(title)?title:[title], function(text){ + titles.push(encodeURIComponent(text)); + }); + return titles.join('|'); +}; + +angular.filter.googleChartApi.collect = function(data, key){ + var outterValues = []; + var count = 0; + _.each(data.series||[], function(serie){ + var innerValues = []; + var value = serie[key] || []; + _.each(_.isArray(value)?value:[value], function(color){ + innerValues.push(encodeURIComponent(color)); + count++; + }); + outterValues.push(innerValues.join('|')); + }); + return count?outterValues.join(','):null; +}; + +angular.filter.googleChartApi.encode= function(params, width, height) { + width = width || 200; + height = height || width; + var url = "http://chart.apis.google.com/chart?"; + var urlParam = []; + params.chs = width + "x" + height; + for ( var key in params) { + var value = params[key]; + if (value) { + urlParam.push(key + "=" + value); + } + } + urlParam.sort(); + url += urlParam.join("&"); + return new angular.filter.Meta({url:url, text:value, + html:'<img width="' + width + '" height="' + height + '" src="'+url+'"/>'}); +}; + +angular.filter.qrcode = function(value, width, height) { + return angular.filter.googleChartApi.encode({cht:'qr', chl:encodeURIComponent(value)}, width, height); +}; +angular.filter.chart = { + pie:function(data, width, height) { + return angular.filter.googleChartApi('p', data, width, height); + }, + pie3d:function(data, width, height) { + return angular.filter.googleChartApi('p3', data, width, height); + }, + pieConcentric:function(data, width, height) { + return angular.filter.googleChartApi('pc', data, width, height); + }, + barHorizontalStacked:function(data, width, height) { + return angular.filter.googleChartApi('bhs', data, width, height); + }, + barHorizontalGrouped:function(data, width, height) { + return angular.filter.googleChartApi('bhg', data, width, height); + }, + barVerticalStacked:function(data, width, height) { + return angular.filter.googleChartApi('bvs', data, width, height); + }, + barVerticalGrouped:function(data, width, height) { + return angular.filter.googleChartApi('bvg', data, width, height); + }, + line:function(data, width, height) { + return angular.filter.googleChartApi('lc', data, width, height); + }, + sparkline:function(data, width, height) { + return angular.filter.googleChartApi('ls', data, width, height); + }, + scatter:function(data, width, height) { + return angular.filter.googleChartApi('s', data, width, height); + } +}; + +angular.filter.html = function(html){ + return new angular.filter.Meta({html:html}); +}; diff --git a/src/JSON.js b/src/JSON.js new file mode 100644 index 00000000..2b6393bf --- /dev/null +++ b/src/JSON.js @@ -0,0 +1,92 @@ +nglr.array = [].constructor; + +nglr.toJson = function(obj, pretty){ + var buf = []; + nglr.toJsonArray(buf, obj, pretty ? "\n " : null); + return buf.join(''); +}; + +nglr.toPrettyJson = function(obj) { + return nglr.toJson(obj, true); +}; + +nglr.fromJson = function(json) { + try { + var parser = new nglr.Parser(json, true); + var expression = parser.primary(); + parser.assertAllConsumed(); + return expression(); + } catch (e) { + console.error("fromJson error: ", json, e); + throw e; + } +}; + + +nglr.toJsonArray = function(buf, obj, pretty){ + 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 { + nglr.toJsonArray(buf, item, pretty); + } + 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) + 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(":"); + nglr.toJsonArray(buf, value, childPretty); + comma = true; + } + } catch (e) { + } + } + buf.push("}"); + } + } +}; diff --git a/src/Loader.js b/src/Loader.js new file mode 100644 index 00000000..fdcfa3cc --- /dev/null +++ b/src/Loader.js @@ -0,0 +1,389 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC + +// IE compatibility + +if (typeof document.getAttribute == 'undefined') + document.getAttribute = function() { + }; +if (typeof Node == 'undefined') { + Node = { + ELEMENT_NODE : 1, + ATTRIBUTE_NODE : 2, + TEXT_NODE : 3, + CDATA_SECTION_NODE : 4, + ENTITY_REFERENCE_NODE : 5, + ENTITY_NODE : 6, + PROCESSING_INSTRUCTION_NODE : 7, + COMMENT_NODE : 8, + DOCUMENT_NODE : 9, + DOCUMENT_TYPE_NODE : 10, + DOCUMENT_FRAGMENT_NODE : 11, + NOTATION_NODE : 12 + }; +} + +if (_.isUndefined(window.nglr)) nglr = {}; +if (_.isUndefined(window.angular)) angular = {}; +if (_.isUndefined(angular.validator)) angular.validator = {}; +if (_.isUndefined(angular.filter)) angular.filter = {}; +if (_.isUndefined(window.console)) + window.console = { + log:function() {}, + error:function() {} + }; +if (_.isUndefined(nglr.alert)) { + nglr.alert = function(){console.log(arguments); window.alert.apply(window, arguments); }; +} + +nglr.consoleLog = function(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 : nglr.toJson(obj)); + sep = " "; + } + log.appendChild(document.createTextNode(msg)); + nglr.consoleNode.appendChild(log); +}; + +nglr.isNode = function(inp) { + return inp && + inp.tagName && + inp.nodeName && + inp.ownerDocument && + inp.removeAttribute; +}; + +nglr.isLeafNode = function(node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + default: + return false; + } +}; + +nglr.noop = function() { +}; +nglr.setHtml = function(node, html) { + if (nglr.isLeafNode(node)) { + if (nglr.msie) { + node.innerText = html; + } else { + node.textContent = html; + } + } else { + node.innerHTML = html; + } +}; + +nglr.escapeHtml = function(html) { + if (!html || !html.replace) + return html; + return html. + replace(/&/g, '&'). + replace(/</g, '<'). + replace(/>/g, '>'); +}; + +nglr.escapeAttr = function(html) { + if (!html || !html.replace) + return html; + return html.replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, + '"'); +}; + +nglr.bind = function(_this, _function) { + if (!_this) + throw "Missing this"; + if (!_.isFunction(_function)) + throw "Missing function"; + return function() { + return _function.apply(_this, arguments); + }; +}; + +nglr.shiftBind = function(_this, _function) { + return function() { + var args = [ this ]; + for ( var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); + } + return _function.apply(_this, args); + }; +}; + +nglr.outerHTML = function(node) { + var temp = document.createElement('div'); + temp.appendChild(node); + var outerHTML = temp.innerHTML; + temp.removeChild(node); + return outerHTML; +}; + +nglr.trim = function(str) { + return str.replace(/^ */, '').replace(/ *$/, ''); +}; + +nglr.toBoolean = function(value) { + var v = ("" + value).toLowerCase(); + if (v == 'f' || v == '0' || v == 'false' || v == 'no') + value = false; + return !!value; +}; + +nglr.merge = function(src, dst) { + for ( var key in src) { + var value = dst[key]; + var type = typeof value; + if (type == 'undefined') { + dst[key] = nglr.fromJson(nglr.toJson(src[key])); + } else if (type == 'object' && value.constructor != nglr.array && + key.substring(0, 1) != "$") { + nglr.merge(src[key], value); + } + } +}; + +// //////////////////////////// +// Loader +// //////////////////////////// + +nglr.Loader = function(document, head, config) { + this.document = jQuery(document); + this.head = jQuery(head); + this.config = config; + this.location = window.location; +}; + +nglr.Loader.prototype.load = function() { + this.configureLogging(); + this.loadCss('/stylesheets/jquery-ui/smoothness/jquery-ui-1.7.1.css'); + this.loadCss('/stylesheets/nglr.css'); + console.log("Server: " + this.config.server); + jQuery.noConflict(); + nglr.msie = jQuery.browser.msie; + this.configureJQueryPlugins(); + this.computeConfiguration(); + this.bindHtml(); +}; + +nglr.Loader.prototype.configureJQueryPlugins = function() { + console.log('Loader.configureJQueryPlugins()'); + jQuery.fn.removeNode = function() { + var node = this.get(0); + node.parentNode.removeChild(node); + }; + jQuery.fn.scope = function() { + var element = this; + while (element && element.get(0)) { + var scope = element.data("scope"); + if (scope) + return scope; + element = element.parent(); + } + return null; + }; + jQuery.fn.controller = function() { + return this.data('controller') || nglr.NullController.instance; + }; +}; + +nglr.Loader.prototype.uid = function() { + return "" + new Date().getTime(); +}; + +nglr.Loader.prototype.computeConfiguration = function() { + var config = this.config; + if (!config.database) { + var match = config.server.match(/https?:\/\/([\w]*)/) + config.database = match ? match[1] : "$MEMORY"; + } +}; + +nglr.Loader.prototype.bindHtml = function() { + console.log('Loader.bindHtml()'); + var watcher = new nglr.UrlWatcher(this.location); + var document = this.document; + var widgetFactory = new nglr.WidgetFactory(this.config.server, this.config.database); + var binder = new nglr.Binder(document[0], widgetFactory, watcher, this.config); + widgetFactory.onChangeListener = nglr.shiftBind(binder, binder.updateModel); + var controlBar = new nglr.ControlBar(document.find('body'), this.config.server); + var onUpdate = function(){binder.updateView();}; + var server = this.config.database=="$MEMORY" ? + new nglr.FrameServer(this.window) : + new nglr.Server(this.config.server, jQuery.getScript); + server = new nglr.VisualServer(server, new nglr.Status(jQuery(document.body)), onUpdate); + var users = new nglr.Users(server, controlBar); + var databasePath = '/data/' + this.config.database; + var post = function(request, callback){ + server.request("POST", databasePath, request, callback); + }; + var datastore = new nglr.DataStore(post, users, binder.anchor); + binder.updateListeners.push(function(){datastore.flush();}); + var scope = new nglr.Scope( { + $anchor : binder.anchor, + $binder : binder, + $config : this.config, + $console : window.console, + $datastore : datastore, + $save : function(callback) { + datastore.saveScope(scope.state, callback, binder.anchor); + }, + $window : window, + $uid : this.uid, + $users : users + }, "ROOT"); + + jQuery.each(["get", "set", "eval", "addWatchListener", "updateView"], + function(i, method){ + angular[method] = nglr.bind(scope, scope[method]); + }); + + document.data('scope', scope); + console.log('$binder.entity()'); + binder.entity(scope); + + console.log('$binder.compile()'); + binder.compile(); + + console.log('ControlBar.bind()'); + controlBar.bind(); + + console.log('$users.fetchCurrentUser()'); + function fetchCurrentUser() { + users.fetchCurrentUser(function(u) { + if (!u && document.find("[ng-auth=eager]").length) { + users.login(); + } + }); + } + fetchCurrentUser(); + + console.log('PopUp.bind()'); + new nglr.PopUp(document).bind(); + + console.log('$binder.parseAnchor()'); + binder.parseAnchor(); + + console.log('$binder.executeInit()'); + binder.executeInit(); + + console.log('$binder.updateView()'); + binder.updateView(); + + watcher.listener = nglr.bind(binder, binder.onUrlChange, watcher); + watcher.onUpdate = function(){nglr.alert("update");}; + watcher.watch(); + document.find("body").show(); + console.log('ready()'); + +}; + +nglr.Loader.prototype.visualPost = function(delegate) { + var status = new nglr.Status(jQuery(document.body)); + return function(request, delegateCallback) { + status.beginRequest(request); + var callback = function() { + status.endRequest(); + try { + delegateCallback.apply(this, arguments); + } catch (e) { + nglr.alert(nglr.toJson(e)); + } + }; + delegate(request, callback); + }; +}; + +nglr.Loader.prototype.configureLogging = function() { + var url = window.location.href + '#'; + url = url.split('#')[1]; + var config = { + debug : null + }; + var configs = url.split('&'); + for ( var i = 0; i < configs.length; i++) { + var part = (configs[i] + '=').split('='); + config[part[0]] = part[1]; + } + if (config.debug == 'console') { + nglr.consoleNode = document.createElement("div"); + nglr.consoleNode.id = 'ng-console'; + document.getElementsByTagName('body')[0].appendChild(nglr.consoleNode); + console.log = function() { + nglr.consoleLog('ng-console-info', arguments); + }; + console.error = function() { + nglr.consoleLog('ng-console-error', arguments); + }; + } +}; + +nglr.Loader.prototype.loadCss = function(css) { + var cssTag = document.createElement('link'); + cssTag.rel = "stylesheet"; + cssTag.type = "text/css"; + if (!css.match(/^http:/)) + css = this.config.server + css; + cssTag.href = css; + this.head[0].appendChild(cssTag); +}; + +nglr.UrlWatcher = function(location) { + this.location = location; + this.delay = 25; + this.setTimeout = function(fn, delay) { + window.setTimeout(fn, delay); + }; + this.listener = function(url) { + return url; + }; + this.expectedUrl = location.href; +}; + +nglr.UrlWatcher.prototype.watch = function() { + var self = this; + var pull = function() { + if (self.expectedUrl !== self.location.href) { + var notify = self.location.hash.match(/^#\$iframe_notify=(.*)$/); + if (notify) { + if (!self.expectedUrl.match(/#/)) { + self.expectedUrl += "#"; + } + self.location.href = self.expectedUrl; + var id = '_iframe_notify_' + notify[1]; + var notifyFn = nglr[id]; + delete nglr[id]; + try { + (notifyFn||nglr.noop)(); + } catch (e) { + nglr.alert(e); + } + } else { + self.listener(self.location.href); + self.expectedUrl = self.location.href; + } + } + self.setTimeout(pull, self.delay); + }; + pull(); +}; + +nglr.UrlWatcher.prototype.setUrl = function(url) { + var existingURL = window.location.href; + if (!existingURL.match(/#/)) + existingURL += '#'; + if (existingURL != url) + window.location.href = url; + self.existingURL = url; +}; + +nglr.UrlWatcher.prototype.getUrl = function() { + return window.location.href; +}; diff --git a/src/Model.js b/src/Model.js new file mode 100644 index 00000000..5e48251f --- /dev/null +++ b/src/Model.js @@ -0,0 +1,65 @@ +// Copyright (C) 2009 BRAT Tech LLC + +// Single $ is special and does not get searched +// Double $$ is special an is client only (does not get sent to server) + +nglr.Model = function(entity, initial) { + this.$$entity = entity; + this.$loadFrom(initial||{}); + this.$entity = entity.title; + this.$migrate(); +}; + +nglr.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]; + } +}; + +nglr.Model.prototype.$migrate = function() { + nglr.merge(this.$$entity.defaults, this); + return this; +}; + +nglr.Model.prototype.$merge = function(other) { + nglr.merge(other, this); + return this; +}; + +nglr.Model.prototype.$save = function(callback) { + this.$$entity.datastore.save(this, callback === true ? undefined : callback); + if (callback === true) this.$$entity.datastore.flush(); + return this; +}; + +nglr.Model.prototype.$delete = function(callback) { + this.$$entity.datastore.remove(this, callback === true ? undefined : callback); + if (callback === true) this.$$entity.datastore.flush(); + return this; +}; + +nglr.Model.prototype.$loadById = function(id, callback) { + this.$$entity.datastore.load(this, id, callback); + return this; +}; + +nglr.Model.prototype.$loadFrom = function(other) { + nglr.Model.copyDirectFields(other, this); + return this; +}; + +nglr.Model.prototype.$saveTo = function(other) { + nglr.Model.copyDirectFields(this, other); + return this; +}; diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 00000000..3d72bebf --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,741 @@ +nglr.Lexer = function(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; +}; + +nglr.Lexer.OPERATORS = { + 'null':function(self){return null;}, + 'true':function(self){return true;}, + 'false':function(self){return false;}, + '+':function(self, a,b){return (a||0)+(b||0);}, + '-':function(self, a,b){return (a||0)-(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 self.scope.set(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;} +}; + +nglr.Lexer.prototype.peek = function() { + if (this.index + 1 < this.text.length) { + return this.text.charAt(this.index + 1); + } else { + return false; + } +}; + +nglr.Lexer.prototype.parse = function() { + var tokens = this.tokens; + var OPERATORS = nglr.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; +}; + +nglr.Lexer.prototype.isNumber = function(ch) { + return '0' <= ch && ch <= '9'; +}; + +nglr.Lexer.prototype.isWhitespace = function(ch) { + return ch == ' ' || ch == '\r' || ch == '\t' || + ch == '\n' || ch == '\v'; +}; + +nglr.Lexer.prototype.isIdent = function(ch) { + return 'a' <= ch && ch <= 'z' || + 'A' <= ch && ch <= 'Z' || + '_' == ch || ch == '$'; +}; + +nglr.Lexer.prototype.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;}}); +}; + +nglr.Lexer.prototype.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 = nglr.Lexer.OPERATORS[ident]; + if (!fn) { + fn = function(self){ + return self.scope.get(ident); + }; + fn.isAssignable = ident; + } + this.tokens.push({index:start, text:ident, fn:fn}); +}; +nglr.Lexer.ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; +nglr.Lexer.prototype.readString = function(quote) { + var start = this.index; + var dateParseLength = this.dateParseLength; + this.index++; + var string = ""; + var escape = false; + while (this.index < this.text.length) { + var ch = this.text.charAt(this.index); + 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 = nglr.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: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 + "'."; +}; + +nglr.Lexer.prototype.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 + "'."; +}; + + +nglr.Parser = function(text, parseStrings){ + this.text = text; + this.tokens = new nglr.Lexer(text, parseStrings).parse(); + this.index = 0; +}; + +nglr.Parser.ZERO = function(){ + return 0; +}; + +nglr.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) + "'."; +}; + +nglr.Parser.prototype.peekToken = function() { + if (this.tokens.length === 0) + throw "Unexpected end of expression: " + this.text; + return this.tokens[0]; +}; + +nglr.Parser.prototype.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; +}; + +nglr.Parser.prototype.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; +}; + +nglr.Parser.prototype.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) + "'."; + } +}; + +nglr.Parser.prototype._unary = function(fn, parse) { + var right = parse.apply(this); + return function(self) { + return fn(self, right(self)); + }; +}; + +nglr.Parser.prototype._binary = function(left, fn, parse) { + var right = parse.apply(this); + return function(self) { + return fn(self, left(self), right(self)); + }; +}; + +nglr.Parser.prototype.hasTokens = function () { + return this.tokens.length > 0; +}; + +nglr.Parser.prototype.assertAllConsumed = function(){ + if (this.tokens.length !== 0) { + throw "Did not understand '" + this.text.substring(this.tokens[0].index) + + "' while evaluating '" + this.text + "'."; + } +}; + +nglr.Parser.prototype.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; + }; + } + } +}; + +nglr.Parser.prototype.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; + } + } +}; + +nglr.Parser.prototype.filter = function(){ + return this._pipeFunction(angular.filter); +}; + +nglr.Parser.prototype.validator = function(){ + return this._pipeFunction(angular.validator); +}; + +nglr.Parser.prototype._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; + }; + } + } +}; + +nglr.Parser.prototype.expression = function(){ + return this.throwStmt(); +}; + +nglr.Parser.prototype.throwStmt = function(){ + if (this.expect('throw')) { + var throwExp = this.assignment(); + return function (self) { + throw throwExp(self); + }; + } else { + return this.assignment(); + } +}; + +nglr.Parser.prototype.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; + } +}; + +nglr.Parser.prototype.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; + } + } +}; + +nglr.Parser.prototype.logicalAND = function(){ + var left = this.negated(); + var token; + while(true) { + if ((token = this.expect('&&'))) { + left = this._binary(left, token.fn, this.negated); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.negated = function(){ + var token; + if (token = this.expect('!')) { + return this._unary(token.fn, this.equality); + } else { + return this.equality(); + } +}; + +nglr.Parser.prototype.equality = function(){ + var left = this.relational(); + var token; + while(true) { + if ((token = this.expect('==','!='))) { + left = this._binary(left, token.fn, this.relational); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.relational = function(){ + var left = this.additive(); + var token; + while(true) { + if ((token = this.expect('<', '>', '<=', '>='))) { + left = this._binary(left, token.fn, this.additive); + } else { + return left; + } + } +}; + +nglr.Parser.prototype.additive = function(){ + var left = this.multiplicative(); + var token; + while(token = this.expect('+','-')) { + left = this._binary(left, token.fn, this.multiplicative); + } + return left; +}; + +nglr.Parser.prototype.multiplicative = function(){ + var left = this.unary(); + var token; + while(token = this.expect('*','/','%')) { + left = this._binary(left, token.fn, this.unary); + } + return left; +}; + +nglr.Parser.prototype.unary = function(){ + var token; + if (this.expect('+')) { + return this.primary(); + } else if (token = this.expect('-')) { + return this._binary(nglr.Parser.ZERO, token.fn, this.multiplicative); + } else { + return this.primary(); + } +}; + +nglr.Parser.prototype.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; +}; + +nglr.Parser.prototype.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; +}; + +nglr.Parser.prototype.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 = new nglr.Scope(self.scope.state); + scope.set('$', $); + for ( var i = 0; i < args.length; i++) { + scope.set(args[i], arguments[i]); + } + return statements({scope:scope}); + }; + }; +}; + +nglr.Parser.prototype.fieldAccess = function(object) { + var field = this.expect().text; + var fn = function (self){ + return nglr.Scope.getter(object(self), field); + }; + fn.isAssignable = field; + return fn; +}; + +nglr.Parser.prototype.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; + }; + } +}; + +nglr.Parser.prototype.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 +nglr.Parser.prototype.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; + }; +}; + +nglr.Parser.prototype.object = function () { + var keyValues = []; + if (this.peekToken().text != '}') { + do { + var key = this.expect().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; + }; +}; + +nglr.Parser.prototype.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; + }; +}; + +nglr.Parser.prototype.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 datastore = self.scope.get('$datastore'); + var Entity = datastore.entity(entity, defaults); + self.scope.set(entity, Entity); + if (instance) { + var document = Entity(); + document.$$anchor = instance; + self.scope.set(instance, document); + return "$anchor." + instance + ":{" + + instance + "=" + entity + ".load($anchor." + instance + ");" + + instance + ".$$anchor=" + angular.String.quote(instance) + ";" + + "};"; + } else { + return ""; + } + }; +}; + +nglr.Parser.prototype.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); + } + }; +}; + +nglr.Parser.prototype.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/Scope.js b/src/Scope.js new file mode 100644 index 00000000..45dd15a4 --- /dev/null +++ b/src/Scope.js @@ -0,0 +1,198 @@ +// Copyright (C) 2009 BRAT Tech LLC + +nglr.Scope = function(initialState, name) { + this.widgets = []; + this.watchListeners = {}; + this.name = name; + initialState = initialState || {}; + var State = function(){}; + State.prototype = initialState; + this.state = new State(); + this.state.$parent = initialState; + if (name == "ROOT") { + this.state.$root = this.state; + } +}; + +nglr.Scope.expressionCache = {}; + +nglr.Scope.prototype.updateView = function() { + var self = this; + this.fireWatchers(); + _.each(this.widgets, function(widget){ + self.evalWidget(widget, "", {}, function(){ + this.updateView(self); + }); + }); +}; + +nglr.Scope.prototype.addWidget = function(controller) { + if (controller) this.widgets.push(controller); +}; + +nglr.Scope.prototype.isProperty = function(exp) { + for ( var i = 0; i < exp.length; i++) { + var ch = exp.charAt(i); + if (ch!='.' && !nglr.Lexer.prototype.isIdent(ch)) { + return false; + } + } + return true; +}; + +nglr.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 nglr.bind(lastInstance, instance); + } + return instance; +}; + +nglr.Scope.prototype.get = function(path) { + return nglr.Scope.getter(this.state, path); +}; + +nglr.Scope.prototype.set = function(path, value) { + var element = path.split('.'); + var instance = this.state; + 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; +}; + +nglr.Scope.prototype.setEval = function(expressionText, value) { + this.eval(expressionText + "=" + nglr.toJson(value)); +}; + +nglr.Scope.prototype.eval = function(expressionText, context) { + var expression = nglr.Scope.expressionCache[expressionText]; + if (!expression) { + var parser = new nglr.Parser(expressionText); + expression = parser.statements(); + parser.assertAllConsumed(); + nglr.Scope.expressionCache[expressionText] = expression; + } + context = context || {}; + context.scope = this; + return expression(context); +}; + +//TODO: Refactor. This function needs to be an execution closure for widgets +// move to widgets +// remove expression, just have inner closure. +nglr.Scope.prototype.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){ + console.error('Eval Widget Error:', e); + var jsonError = nglr.toJson(e, true); + widget.hasError = true; + jQuery(widget.view). + addClass('ng-exception'). + attr('ng-error', jsonError); + if (onFailure) { + onFailure.apply(widget, [e, jsonError]); + } + return false; + } +}; + +nglr.Scope.prototype.validate = function(expressionText, value) { + var expression = nglr.Scope.expressionCache[expressionText]; + if (!expression) { + expression = new nglr.Parser(expressionText).validator(); + nglr.Scope.expressionCache[expressionText] = expression; + } + var self = {scope:this}; + return expression(self)(self, value); +}; + +nglr.Scope.prototype.entity = function(entityDeclaration) { + var expression = new nglr.Parser(entityDeclaration).entityDeclaration(); + return expression({scope:this}); +}; + +nglr.Scope.prototype.markInvalid = function(widget) { + this.state.$invalidWidgets.push(widget); +}; + +nglr.Scope.prototype.watch = function(declaration) { + var self = this; + new nglr.Parser(declaration).watch()({ + scope:this, + addListener:function(watch, exp){ + self.addWatchListener(watch, function(n,o){ + try { + return exp({scope:self}, n, o); + } catch(e) { + nglr.alert(e); + } + }); + } + }); +}; + +nglr.Scope.prototype.addWatchListener = function(watchExpression, listener) { + var watcher = this.watchListeners[watchExpression]; + if (!watcher) { + watcher = {listeners:[], expression:watchExpression}; + this.watchListeners[watchExpression] = watcher; + } + watcher.listeners.push(listener); +}; + +nglr.Scope.prototype.fireWatchers = function() { + var self = this; + var fired = false; + jQuery.each(this.watchListeners, function(name, watcher) { + var value = self.eval(watcher.expression); + if (value !== watcher.lastValue) { + jQuery.each(watcher.listeners, function(i, listener){ + listener(value, watcher.lastValue); + fired = true; + }); + watcher.lastValue = value; + } + }); + return fired; +}; diff --git a/src/Server.js b/src/Server.js new file mode 100644 index 00000000..94b0cc10 --- /dev/null +++ b/src/Server.js @@ -0,0 +1,69 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC + +nglr.Server = function(url, getScript) { + this.url = url; + this.nextId = 0; + this.getScript = getScript; + this.uuid = "_" + ("" + Math.random()).substr(2) + "_"; + this.maxSize = 1800; +}; + +nglr.Server.prototype.base64url = function(txt) { + return Base64.encode(txt); +}; + +nglr.Server.prototype.request = function(method, url, request, callback) { + var requestId = this.uuid + (this.nextId++); + nglr[requestId] = function(response) { + delete nglr[requestId]; + callback(200, response); + }; + var payload = {u:url, m:method, p:request}; + payload = this.base64url(nglr.toJson(payload)); + var totalPockets = Math.ceil(payload.length / this.maxSize); + var baseUrl = this.url + "/$/" + requestId + "/" + totalPockets + "/"; + for ( var pocketNo = 0; pocketNo < totalPockets; pocketNo++) { + var pocket = payload.substr(pocketNo * this.maxSize, this.maxSize); + this.getScript(baseUrl + (pocketNo+1) + "?h=" + pocket, nglr.noop); + } +}; + +nglr.FrameServer = function(frame) { + this.frame = frame; +}; +nglr.FrameServer.PREFIX = "$DATASET:"; + +nglr.FrameServer.prototype = { + read:function(){ + this.data = nglr.fromJson(this.frame.name.substr(nglr.FrameServer.PREFIX.length)); + }, + write:function(){ + this.frame.name = nglr.FrameServer.PREFIX + nglr.toJson(this.data); + }, + request: function(method, url, request, callback) { + //alert(method + " " + url + " " + nglr.toJson(request) + " " + nglr.toJson(callback)); + } +}; + + +nglr.VisualServer = function(delegate, status, update) { + this.delegate = delegate; + this.update = update; + this.status = status; +}; + +nglr.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) { + nglr.alert(nglr.toJson(e)); + } + self.update(); + }); + } +}; diff --git a/src/Users.js b/src/Users.js new file mode 100644 index 00000000..c0c15848 --- /dev/null +++ b/src/Users.js @@ -0,0 +1,36 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC +nglr.Users = function(server, controlBar) { + this.server = server; + this.controlBar = controlBar; +}; + +nglr.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||nglr.noop)(); + }); + }, + + login: function(callback) { + var self = this; + this.controlBar.login(function(){ + self.fetchCurrentUser(function(){ + (callback||nglr.noop)(); + }); + }); + }, + + notAuthorized: function(){ + this.controlBar.notAuthorized(); + } +}; diff --git a/src/Validators.js b/src/Validators.js new file mode 100644 index 00000000..94cb1d52 --- /dev/null +++ b/src/Validators.js @@ -0,0 +1,80 @@ +// Copyright (C) 2009 BRAT Tech LLC + +angular.validator.regexp = function(value, regexp, msg) { + if (!value.match(regexp)) { + return msg || + "Value does not match expected format " + regexp + "."; + } else { + return null; + } +}; + +angular.validator.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 "Value is not a number."; + } +}; + +angular.validator.integer = function(value, min, max) { + var number = angular.validator.number(value, min, max); + if (number === null && value != Math.round(value)) { + return "Value is not a whole number."; + } + return number; +}; + +angular.validator.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)."; +}; + +angular.validator.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."; +}; + +angular.validator.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."; +}; + +angular.validator.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."; +}; + +angular.validator.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."; +}; + +angular.validator.json = function(value) { + try { + nglr.fromJson(value); + return null; + } catch (e) { + return e.toString(); + } +}; diff --git a/src/Widgets.js b/src/Widgets.js new file mode 100644 index 00000000..de74533a --- /dev/null +++ b/src/Widgets.js @@ -0,0 +1,774 @@ +// Copyright (C) 2009 BRAT Tech LLC + + +nglr.WidgetFactory = function(serverUrl, database) { + this.nextUploadId = 0; + this.serverUrl = serverUrl; + this.database = database; + this.createSWF = swfobject.createSWF; + this.onChangeListener = function(){}; +}; + +nglr.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; + if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') { + controller = new nglr.ButtonController(input[0], exp); + event = "click"; + bubbleEvent = false; + } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') { + controller = new nglr.TextController(input[0], exp); + event = "keyup change"; + } else if (type == 'checkbox') { + controller = new nglr.CheckboxController(input[0], exp); + event = "click"; + } else if (type == 'radio') { + controller = new nglr.RadioController(input[0], exp); + event="click"; + } else if (type == 'select-one') { + controller = new nglr.SelectController(input[0], exp); + } else if (type == 'select-multiple') { + controller = new nglr.MultiSelectController(input[0], exp); + } else if (type == 'file') { + controller = this.createFileController(input, exp); + } else { + throw 'Unknown type: ' + type; + } + input.data('controller', controller); + var binder = scope.get('$binder'); + var action = function() { + if (controller.updateModel(scope)) { + var action = jQuery(controller.view).attr('ng-action') || ""; + if (scope.evalWidget(controller, action)) { + binder.updateView(scope); + } + } + return bubbleEvent; + }; + jQuery(controller.view, ":input"). + bind(event, action); + return controller; +}; + +nglr.WidgetFactory.prototype.createFileController = function(fileInput) { + var uploadId = '__uploadWidget_' + (this.nextUploadId++); + var view = nglr.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 nglr.FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database); + jQuery(swfNode).data('controller', cntl); + return cntl; +}; + +nglr.WidgetFactory.prototype.createTextWidget = function(textInput) { + var controller = new nglr.TextController(textInput); + controller.onChange(this.onChangeListener); + return controller; +}; + +///////////////////// +// FileController +/////////////////////// + +nglr.FileController = function(view, scopeName, uploader, databaseUrl) { + this.view = view; + this.uploader = uploader; + this.scopeName = scopeName; + this.attachmentsPath = databaseUrl + '/_attachments'; + this.value = null; + this.lastValue = undefined; +}; + +nglr.FileController.dispatchEvent = function(id, event, args) { + var object = document.getElementById(id); + var controller = jQuery(object).data("controller"); + nglr.FileController.prototype['_on_' + event].apply(controller, args); +}; + +nglr.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>'); +}; + +nglr.FileController.prototype._on_cancel = function() { +}; + +nglr.FileController.prototype._on_complete = function() { +}; + +nglr.FileController.prototype._on_httpStatus = function(status) { + nglr.alert("httpStatus:" + this.scopeName + " status:" + status); +}; + +nglr.FileController.prototype._on_ioError = function() { + nglr.alert("ioError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_open = function() { + nglr.alert("open:" + this.scopeName); +}; + +nglr.FileController.prototype._on_progress = function(bytesLoaded, bytesTotal) { +}; + +nglr.FileController.prototype._on_securityError = function() { + nglr.alert("securityError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_uploadCompleteData = function(data) { + var value = nglr.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; + scope.get('$binder').updateView(); +}; + +nglr.FileController.prototype._on_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(); +}; + +nglr.FileController.prototype.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; + } +}; + +nglr.FileController.prototype.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); +}; + +nglr.FileController.prototype.upload = function() { + if (this.name) { + this.uploader.uploadFile(this.attachmentsPath); + } +}; + + +/////////////////////// +// NullController +/////////////////////// +nglr.NullController = function(view) {this.view = view;}; +nglr.NullController.prototype.updateModel = function() { return true; }; +nglr.NullController.prototype.updateView = function() { }; +nglr.NullController.instance = new nglr.NullController(); + + +/////////////////////// +// ButtonController +/////////////////////// +nglr.ButtonController = function(view) {this.view = view;}; +nglr.ButtonController.prototype.updateModel = function(scope) { return true; }; +nglr.ButtonController.prototype.updateView = function(scope) {}; + +/////////////////////// +// TextController +/////////////////////// +nglr.TextController = function(view, exp) { + this.view = view; + 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 = view.value; + var widget = view.getAttribute('ng-widget'); + if (widget === 'datepicker') { + jQuery(view).datepicker(); + } +}; + +nglr.TextController.prototype.updateModel = function(scope) { + var value = this.view.value; + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } +}; + +nglr.TextController.prototype.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 != value) { + view.value = value; + this.lastValue = value; + } + var isValidationError = false; + view.removeAttribute('ng-error'); + if (this.required) { + isValidationError = !(value && value.length > 0); + } + var errorText = isValidationError ? "Required Value" : null; + if (!isValidationError && this.validator && value) { + errorText = scope.validate(this.validator, value); + isValidationError = !!errorText; + } + if (this.lastErrorText !== errorText) { + this.lastErrorText = isValidationError; + if (errorText !== null) { + view.setAttribute('ng-error', errorText); + scope.markInvalid(this); + } + jQuery(view).toggleClass('ng-validation-error', isValidationError); + } +}; + +/////////////////////// +// CheckboxController +/////////////////////// +nglr.CheckboxController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = view.checked ? view.value : ""; +}; + +nglr.CheckboxController.prototype.updateModel = function(scope) { + var input = this.view; + var value = input.checked ? input.value : ''; + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } +}; + +nglr.CheckboxController.prototype.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 = input.value == (''+value); +}; + +/////////////////////// +// SelectController +/////////////////////// +nglr.SelectController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = view.value; +}; + +nglr.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; + } + } +}; + +nglr.SelectController.prototype.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 +/////////////////////// +nglr.MultiSelectController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = this.selected(); +}; + +nglr.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; +}; + +nglr.MultiSelectController.prototype.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; + } +}; + +nglr.MultiSelectController.prototype.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 +/////////////////////// +nglr.RadioController = function(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; +}; + +nglr.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; + } +}; + +nglr.RadioController.prototype.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 +/////////////////////// +nglr.BindUpdater = function(view, exp) { + this.view = view; + this.exp = nglr.Binder.parseBindings(exp); + this.hasError = false; + this.scopeSelf = {element:view}; +}; + +nglr.BindUpdater.toText = function(obj) { + var e = nglr.escapeHtml; + switch(typeof obj) { + case "string": + case "boolean": + case "number": + return e(obj); + case "function": + return nglr.BindUpdater.toText(obj()); + case "object": + if (nglr.isNode(obj)) { + return nglr.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 (nglr.isNode(obj.html)) + return nglr.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(nglr.toJson(obj, true)); + default: + return ""; + } +}; + +nglr.BindUpdater.prototype.updateModel = function(scope) {}; +nglr.BindUpdater.prototype.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 = nglr.Binder.binding(part); + if (binding) { + scope.evalWidget(this, binding, this.scopeSelf, function(value){ + html.push(nglr.BindUpdater.toText(value)); + }, function(e, text){ + nglr.setHtml(this.view, text); + }); + if (this.hasError) { + return; + } + } else { + html.push(nglr.escapeHtml(part)); + } + } + nglr.setHtml(this.view, html.join('')); +}; + +nglr.BindAttrUpdater = function(view, attrs) { + this.view = view; + this.attrs = attrs; +}; + +nglr.BindAttrUpdater.prototype.updateModel = function(scope) {}; +nglr.BindAttrUpdater.prototype.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 = nglr.Binder.parseBindings(attributeTemplates[attrName]); + var attrValues = []; + for ( var i = 0; i < attributeTemplate.length; i++) { + var binding = nglr.Binder.binding(attributeTemplate[i]); + if (binding) { + try { + var value = scope.eval(binding, {element:jNode[0], attrName:attrName}); + if (value && (value.constructor !== nglr.array || value.length !== 0)) + attrValues.push(value); + } catch (e) { + this.hasError = true; + console.error('BindAttrUpdater', e); + var jsonError = nglr.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.server') + '/images/blank.gif'; + jNode.attr(attrName, attrValue); + } +}; + +nglr.EvalUpdater = function(view, exp) { + this.view = view; + this.exp = exp; + this.hasError = false; +}; +nglr.EvalUpdater.prototype.updateModel = function(scope) {}; +nglr.EvalUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp); +}; + +nglr.HideUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.HideUpdater.prototype.updateModel = function(scope) {}; +nglr.HideUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(hideValue){ + var view = jQuery(this.view); + if (nglr.toBoolean(hideValue)) { + view.hide(); + } else { + view.show(); + } + }); +}; + +nglr.ShowUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ShowUpdater.prototype.updateModel = function(scope) {}; +nglr.ShowUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(hideValue){ + var view = jQuery(this.view); + if (nglr.toBoolean(hideValue)) { + view.show(); + } else { + view.hide(); + } + }); +}; + +nglr.ClassUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + if (classValue !== null && classValue !== undefined) { + this.view.className = classValue; + } + }); +}; + +nglr.ClassEvenUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassEvenUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassEvenUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + var index = scope.get('$index'); + jQuery(this.view).toggleClass(classValue, index % 2 === 1); + }); +}; + +nglr.ClassOddUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassOddUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassOddUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + var index = scope.get('$index'); + jQuery(this.view).toggleClass(classValue, index % 2 === 0); + }); +}; + +nglr.StyleUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.StyleUpdater.prototype.updateModel = function(scope) {}; +nglr.StyleUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(styleValue){ + jQuery(this.view).attr('style', "").css(styleValue); + }); +}; + +/////////////////////// +// RepeaterUpdater +/////////////////////// +nglr.RepeaterUpdater = function(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]; +}; + +nglr.RepeaterUpdater.prototype.updateModel = function(scope) {}; +nglr.RepeaterUpdater.prototype.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 iteratorLength = iterator.length; + var childrenLength = this.children.length; + var cursor = this.view; + var time = 0; + var child = null; + var keyExp = this.keyExp; + var valueExp = this.valueExp; + var i = 0; + jQuery.each(iterator, function(key, value){ + if (i < childrenLength) { + // reuse children + child = self.children[i]; + child.scope.set(valueExp, value); + } else { + // grow children + var name = self.prefix + + valueExp + " in " + self.iteratorExp + "[" + i + "]"; + var childScope = new nglr.Scope(scope.state, name); + childScope.set('$index', i); + if (keyExp) + childScope.set(keyExp, key); + childScope.set(valueExp, value); + child = { scope:childScope, element:self.template(childScope, self.prefix, i) }; + cursor.after(child.element); + self.children.push(child); + } + cursor = child.element; + var s = new Date().getTime(); + child.scope.updateView(); + time += new Date().getTime() - s; + i++; + }); + // shrink children + for ( var r = childrenLength; r > iteratorLength; --r) { + var unneeded = this.children.pop(); + unneeded.element.removeNode(); + } + // 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 +////////////////////////////////// + +nglr.PopUp = function(doc) { + this.doc = doc; +}; + +nglr.PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; + +nglr.PopUp.prototype.bind = function () { + var self = this; + this.doc.find('.ng-validation-error,.ng-exception'). + live("mouseover", nglr.PopUp.onOver); +}; + +nglr.PopUp.onOver = function(e) { + nglr.PopUp.onOut(); + var jNode = jQuery(this); + jNode.bind(nglr.PopUp.OUT_EVENT, nglr.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; +}; + +nglr.PopUp.onOut = function() { + jQuery('#ng-callout'). + unbind(nglr.PopUp.OUT_EVENT, nglr.PopUp.onOut). + remove(); + return true; +}; + +////////////////////////////////// +// Status +////////////////////////////////// + + +nglr.Status = function(body) { + this.loader = body.append(nglr.Status.DOM).find("#ng-loading"); + this.requestCount = 0; +}; + +nglr.Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>'; + +nglr.Status.prototype.beginRequest = function () { + if (this.requestCount === 0) { + this.loader.show(); + } + this.requestCount++; +}; + +nglr.Status.prototype.endRequest = function () { + this.requestCount--; + if (this.requestCount === 0) { + this.loader.hide("fold"); + } +}; diff --git a/src/Widgets.js.orig b/src/Widgets.js.orig new file mode 100644 index 00000000..df1d3e40 --- /dev/null +++ b/src/Widgets.js.orig @@ -0,0 +1,764 @@ + // Copyright (C) 2009 BRAT Tech LLC + + +nglr.WidgetFactory = function(serverUrl) { + this.nextUploadId = 0; + this.serverUrl = serverUrl; + this.createSWF = swfobject.createSWF; + this.onChangeListener = function(){}; +}; + +nglr.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; + if (type == 'button' || type == 'submit' || type == 'reset') { + controller = new nglr.ButtonController(input[0], exp); + event = "click"; + bubbleEvent = false; + } else if (type == 'text' || type == 'textarea') { + controller = new nglr.TextController(input[0], exp); + event = "keyup change"; + } else if (type == 'checkbox') { + controller = new nglr.CheckboxController(input[0], exp); + event = "click"; + } else if (type == 'radio') { + controller = new nglr.RadioController(input[0], exp); + event="click"; + } else if (type == 'select-one') { + controller = new nglr.SelectController(input[0], exp); + } else if (type == 'select-multiple') { + controller = new nglr.MultiSelectController(input[0], exp); + } else if (type == 'file') { + controller = this.createFileController(input, exp); + } else { + throw 'Unknown type: ' + type; + } + input.data('controller', controller); + var binder = scope.get('$binder'); + var action = function() { + if (controller.updateModel(scope)) { + var action = jQuery(controller.view).attr('ng-action') || ""; + if (scope.evalWidget(controller, action)) { + binder.updateView(scope); + } + } + return bubbleEvent; + }; + jQuery(controller.view, ":input"). + bind(event, action); + return controller; +}; + +nglr.WidgetFactory.prototype.createFileController = function(fileInput) { + var uploadId = '__uploadWidget_' + (this.nextUploadId++); + var view = nglr.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 nglr.FileController(view, fileInput[0].name, swfNode, this.serverUrl); + jQuery(swfNode).data('controller', cntl); + return cntl; +}; + +nglr.WidgetFactory.prototype.createTextWidget = function(textInput) { + var controller = new nglr.TextController(textInput); + controller.onChange(this.onChangeListener); + return controller; +}; + +///////////////////// +// FileController +/////////////////////// + +nglr.FileController = function(view, scopeName, uploader, serverUrl) { + this.view = view; + this.uploader = uploader; + this.scopeName = scopeName; + this.uploadUrl = serverUrl + '/upload'; + this.attachmentBase = serverUrl + '/attachments'; + this.value = null; + this.lastValue = undefined; +}; + +nglr.FileController.dispatchEvent = function(id, event, args) { + var object = document.getElementById(id); + var controller = jQuery(object).data("controller"); + nglr.FileController.prototype['_on_' + event].apply(controller, args); +}; + +nglr.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>'); +}; + +nglr.FileController.prototype._on_cancel = function() { +}; + +nglr.FileController.prototype._on_complete = function() { +}; + +nglr.FileController.prototype._on_httpStatus = function(status) { + nglr.alert("httpStatus:" + this.scopeName + " status:" + status); +}; + +nglr.FileController.prototype._on_ioError = function() { + nglr.alert("ioError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_open = function() { + nglr.alert("open:" + this.scopeName); +}; + +nglr.FileController.prototype._on_progress = function(bytesLoaded, bytesTotal) { +}; + +nglr.FileController.prototype._on_securityError = function() { + nglr.alert("securityError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_uploadCompleteData = function(data) { + this.value = nglr.fromJson(data); + this.value.url = this.attachmentBase + '/' + this.value.id + '/' + this.value.text; + this.view.find("input").attr('checked', true); + var scope = this.view.scope(); + this.updateModel(scope); + scope.get('$binder').updateView(); +}; + +nglr.FileController.prototype._on_select = function(name, size, type) { + this.name = name; + this.view.find("a").text(name).attr('href', name); + this.view.find("span").text(filters.bytes(size)); + this.upload(); +}; + +nglr.FileController.prototype.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; + } +}; + +nglr.FileController.prototype.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.name); + this.view.find("span").text(filters.bytes(this.value.size)); + } + this.view.find("input").attr('checked', !!modelValue); +}; + +nglr.FileController.prototype.upload = function() { + if (this.name) { + this.uploader.uploadFile(this.uploadUrl); + } +}; + + +/////////////////////// +// NullController +/////////////////////// +nglr.NullController = function(view) {this.view = view;}; +nglr.NullController.prototype.updateModel = function() { return true; }; +nglr.NullController.prototype.updateView = function() { }; +nglr.NullController.instance = new nglr.NullController(); + + +/////////////////////// +// ButtonController +/////////////////////// +nglr.ButtonController = function(view) {this.view = view;}; +nglr.ButtonController.prototype.updateModel = function(scope) { return true; }; +nglr.ButtonController.prototype.updateView = function(scope) {}; + +/////////////////////// +// TextController +/////////////////////// +nglr.TextController = function(view, exp) { + this.view = view; + 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 = view.value; + var widget = view.getAttribute('ng-widget'); + if (widget === 'datepicker') { + jQuery(view).datepicker(); + } +}; + +nglr.TextController.prototype.updateModel = function(scope) { + var value = this.view.value; + if (this.lastValue === value) { + return false; + } else { + scope.set(this.exp, value); + this.lastValue = value; + return true; + } +}; + +nglr.TextController.prototype.updateView = function(scope) { + var view = this.view; + var value = scope.get(this.exp); + if (typeof value === "undefined") { + value = this.initialValue; + scope.set(this.exp, value); + } + value = value ? value : ''; + if (this.lastValue != value) { + view.value = value; + this.lastValue = value; + } + var isValidationError = false; + view.removeAttribute('ng-error'); + if (this.required) { + isValidationError = !(value && value.length > 0); + } + var errorText = isValidationError ? "Required Value" : null; + if (!isValidationError && this.validator && value) { + errorText = scope.validate(this.validator, value); + isValidationError = !!errorText; + } + if (this.lastErrorText !== errorText) { + this.lastErrorText = isValidationError; + if (errorText !== null) { + view.setAttribute('ng-error', errorText); + scope.markInvalid(this); + } + jQuery(view).toggleClass('ng-validation-error', isValidationError); + } +}; + +/////////////////////// +// CheckboxController +/////////////////////// +nglr.CheckboxController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = view.checked ? view.value : ""; +}; + +nglr.CheckboxController.prototype.updateModel = function(scope) { + var input = this.view; + var value = input.checked ? input.value : ''; + if (this.lastValue === value) { + return false; + } else { + scope.setEval(this.exp, value); + this.lastValue = value; + return true; + } +}; + +nglr.CheckboxController.prototype.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 = input.value == (''+value); +}; + +/////////////////////// +// SelectController +/////////////////////// +nglr.SelectController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = view.value; +}; + +nglr.SelectController.prototype.updateModel = function(scope) { + var input = this.view; + if (input.selectedIndex < 0) { + scope.set(this.exp, null); + } else { + var value = this.view.value; + if (this.lastValue === value) { + return false; + } else { + scope.set(this.exp, value); + this.lastValue = value; + return true; + } + } +}; + +nglr.SelectController.prototype.updateView = function(scope) { + var input = this.view; + var value = scope.get(this.exp); + if (typeof value === 'undefined') { + value = this.initialValue; + scope.set(this.exp, value); + } + if (value !== this.lastValue) { + input.value = value ? value : ""; + this.lastValue = value; + } +}; + +/////////////////////// +// MultiSelectController +/////////////////////// +nglr.MultiSelectController = function(view, exp) { + this.view = view; + this.exp = exp; + this.lastValue = undefined; + this.initialValue = this.selected(); +}; + +nglr.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; +}; + +nglr.MultiSelectController.prototype.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.set(this.exp, value); + this.lastValue = value; + return true; + } +}; + +nglr.MultiSelectController.prototype.updateView = function(scope) { + var input = this.view; + var selected = scope.get(this.exp); + if (typeof selected === "undefined") { + selected = this.initialValue; + scope.set(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 = selected.contains(option.value); + } + this.lastValue = selected; + } +}; + +/////////////////////// +// RadioController +/////////////////////// +nglr.RadioController = function(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; +}; + +nglr.RadioController.prototype.updateModel = function(scope) { + var input = this.view; + if (this.lastChecked) { + return false; + } else { + input.checked = true; + this.lastValue = scope.set(this.exp, this.inputValue); + this.lastChecked = true; + return true; + } +}; + +nglr.RadioController.prototype.updateView = function(scope) { + var input = this.view; + var value = scope.get(this.exp); + if (this.initialValue && typeof value === "undefined") { + value = this.initialValue; + scope.set(this.exp, value); + } + if (this.lastValue != value) { + this.lastChecked = input.checked = this.inputValue == (''+value); + this.lastValue = value; + } +}; + +/////////////////////// +//ElementController +/////////////////////// +nglr.BindUpdater = function(view, exp) { + this.view = view; + this.exp = exp.parseBindings(); + this.hasError = false; + this.scopeSelf = {element:view}; +}; + +nglr.BindUpdater.toText = function(obj) { + var e = nglr.escapeHtml; + switch(typeof obj) { + case "string": + case "boolean": + case "number": + return e(obj); + case "function": + return nglr.BindUpdater.toText(obj()); + case "object": + if (nglr.isNode(obj)) { + return nglr.outerHTML(obj); + } else if (obj && obj.TAG === filters.Meta.TAG) { + switch(typeof obj.html) { + case "string": + case "number": + return obj.html; + case "function": + return 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(nglr.toJson(obj, true)); + default: + return ""; + } +}; + +nglr.BindUpdater.prototype.updateModel = function(scope) {}; +nglr.BindUpdater.prototype.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 = part.binding(); + if (binding) { + scope.evalWidget(this, binding, this.scopeSelf, function(value){ + html.push(nglr.BindUpdater.toText(value)); + }, function(e, text){ + nglr.setHtml(this.view, text); + }); + if (this.hasError) { + return; + } + } else { + html.push(nglr.escapeHtml(part)); + } + } + nglr.setHtml(this.view, html.join('')); +}; + +nglr.BindAttrUpdater = function(view, attrs) { + this.view = view; + this.attrs = attrs; +}; + +nglr.BindAttrUpdater.prototype.updateModel = function(scope) {}; +nglr.BindAttrUpdater.prototype.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 = attributeTemplates[attrName].parseBindings(); + var attrValues = []; + for ( var i = 0; i < attributeTemplate.length; i++) { + var binding = attributeTemplate[i].binding(); + if (binding) { + try { + var value = scope.eval(binding, {element:jNode[0], attrName:attrName}); + if (value && (value.constructor !== nglr.array || value.length !== 0)) + attrValues.push(value); + } catch (e) { + this.hasError = true; + console.error('BindAttrUpdater', e); + var jsonError = nglr.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.server') + '/images/blank.gif'; + jNode.attr(attrName, attrValue); + } +}; + +nglr.EvalUpdater = function(view, exp) { + this.view = view; + this.exp = exp; + this.hasError = false; +}; +nglr.EvalUpdater.prototype.updateModel = function(scope) {}; +nglr.EvalUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp); +}; + +nglr.HideUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.HideUpdater.prototype.updateModel = function(scope) {}; +nglr.HideUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(hideValue){ + var view = jQuery(this.view); + if (nglr.toBoolean(hideValue)) { + view.hide(); + } else { + view.show(); + } + }); +}; + +nglr.ShowUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ShowUpdater.prototype.updateModel = function(scope) {}; +nglr.ShowUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(hideValue){ + var view = jQuery(this.view); + if (nglr.toBoolean(hideValue)) { + view.show(); + } else { + view.hide(); + } + }); +}; + +nglr.ClassUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + if (classValue !== null && classValue !== undefined) { + this.view.className = classValue; + } + }); +}; + +nglr.ClassEvenUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassEvenUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassEvenUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + var index = scope.get('$index'); + jQuery(this.view).toggleClass(classValue, index % 2 === 1); + }); +}; + +nglr.ClassOddUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassOddUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassOddUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(classValue){ + var index = scope.get('$index'); + jQuery(this.view).toggleClass(classValue, index % 2 === 0); + }); +}; + +nglr.StyleUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.StyleUpdater.prototype.updateModel = function(scope) {}; +nglr.StyleUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.exp, {}, function(styleValue){ + jQuery(this.view).attr('style', "").css(styleValue); + }); +}; + +/////////////////////// +// RepeaterUpdater +/////////////////////// +nglr.RepeaterUpdater = function(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 + "'."; + } + this.itemExp = match[1]; + this.iteratorExp = match[2]; +}; + +nglr.RepeaterUpdater.prototype.updateModel = function(scope) {}; +nglr.RepeaterUpdater.prototype.updateView = function(scope) { + scope.evalWidget(this, this.iteratorExp, {}, function(iterator){ + if (!iterator) { + iterator = []; + if (scope.isProperty(this.iteratorExp)) { + scope.set(this.iteratorExp, iterator); + } + } + var iteratorLength = iterator.length; + var childrenLength = this.children.length; + var cursor = this.view; + var time = 0; + var child = null; + var itemExp = this.itemExp; + for ( var i = 0; i < iteratorLength; i++) { + if (i < iteratorLength) { + if (i < childrenLength) { // reuse children + child = this.children[i]; + child.scope.set(itemExp, iterator[i]); + } else { // grow children + var name = this.prefix + + itemExp + " in " + this.iteratorExp + "[" + i + "]"; + var childScope = new nglr.Scope(scope.state, name); + childScope.set('$index', i); + childScope.set(itemExp, iterator[i]); + child = { scope:childScope, element:this.template(childScope, this.prefix, i) }; + cursor.after(child.element); + this.children.push(child); + } + cursor = child.element; + var s = new Date().getTime(); + child.scope.updateView(); + time += new Date().getTime() - s; + } + } + // shrink children + for ( var r = childrenLength; r > iteratorLength; --r) { + var unneeded = this.children.pop(); + unneeded.element.removeNode(); + } + // 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 +////////////////////////////////// + +nglr.PopUp = function(doc) { + this.doc = doc; +}; + +nglr.PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; + +nglr.PopUp.prototype.bind = function () { + var self = this; + this.doc.find('.ng-validation-error,.ng-exception'). + live("mouseover", nglr.PopUp.onOver); +}; + +nglr.PopUp.onOver = function(e) { + nglr.PopUp.onOut(); + var jNode = jQuery(this); + jNode.bind(nglr.PopUp.OUT_EVENT, nglr.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; +}; + +nglr.PopUp.onOut = function() { + jQuery('#ng-callout'). + unbind(nglr.PopUp.OUT_EVENT, nglr.PopUp.onOut). + remove(); + return true; +}; + +////////////////////////////////// +// Status +////////////////////////////////// + +nglr.Status = function (body) { + this.body = body; + this.requestCount = 0; +}; +nglr.Status.ANGULAR = "<a class='ng-angular-logo' href='http://www.getangular.com'>&lt;angular/&gt;</a>™"; + +nglr.Status.prototype.beginRequest = function () { + if (this.requestCount === 0) { +<<<<<<< HEAD:public/javascripts/nglr/Widgets.js + this.dialogView = jQuery('<div class="ng-dialog" title="'+nglr.ControlBar.ANGULAR+' Server Communication:">Please Wait...<div/><div class="loader"></div></div>'); +======= + this.dialogView = jQuery('<div title="'+nglr.Status.ANGULAR+' Server Communication:">Please Wait...<div/></div>'); + this.progressWidget = this.dialogView.find("div"); + this.progressWidget.progressbar({value:0}); +>>>>>>> master:public/javascripts/nglr/Widgets.js + this.dialogView.dialog({bgiframe:true, minHeight:50, modal:true}); + this.maxRequestCount = 0; + } + this.requestCount++; + this.maxRequestCount++; +}; + +nglr.Status.prototype.endRequest = function () { + this.requestCount--; + if (this.requestCount === 0) { + this.dialogView.dialog("destroy"); + this.dialogView.remove(); + this.dialogView = null; + } +}; diff --git a/src/XSitePost.js b/src/XSitePost.js new file mode 100644 index 00000000..7d81e207 --- /dev/null +++ b/src/XSitePost.js @@ -0,0 +1,100 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC + +if (typeof nglr == 'undefined') nglr = {}; + +if (typeof console == 'undefined') console = {}; +if (typeof console.log == 'undefined') + console.log = function() {}; +if (typeof console.error == 'undefined') + console.error = function() {}; + +nglr.XSitePost = function(baseUrl, window, prefix) { + this.baseUrl = baseUrl; + this.post = jQuery.post; + this.window = window; + this.inQueue = {}; + this.outQueue = []; + this.maxMsgSize = 100000; + this.delay = 20; + this.prefix = prefix; + this.setTimeout=function(fn, delay){window.setTimeout(fn, delay);}; +}; + +nglr.XSitePost.prototype.init = function() { + this.window.name = ''; + this.response('ready', 'null'); +}; + +nglr.XSitePost.prototype.incomingFragment = function(fragment) { + var parts = fragment.split(";"); + this.incomingMsg(parts.shift(), 1*parts.shift(), 1*parts.shift(), parts.shift()); +}; + +nglr.XSitePost.prototype.incomingMsg = function(id, partNo, totalParts, msgPart) { + var msg = this.inQueue[id]; + if (!msg) { + msg = {id:id, parts:[], count:0}; + this.inQueue[id] = msg; + } + msg.parts[partNo] = msgPart; + msg.count++; + if (totalParts === msg.count) { + delete this.inQueue[id]; + var request = this.decodePost(msg.parts.join('')); + var self = this; + this.post(this.baseUrl + request.url, request.params, function(response, status){ + self.response(id, response, status); + }); + } +}; + +nglr.XSitePost.prototype.response = function(id, response, status) { + var start = 0; + var end; + var msg = Base64.encode(response); + var msgLen = msg.length; + var total = Math.ceil(msgLen / this.maxMsgSize); + var part = 0; + while (start < msgLen) { + end = Math.min(msgLen, start + this.maxMsgSize); + this.outQueue.push(id + ':'+part+':'+total+':' + msg.substring(start, end)); + start = end; + part++; + } +}; + +nglr.XSitePost.prototype.decodePost = function(post) { + var parts = post.split(':'); + var url = Base64.decode(parts.shift()); + var params = {}; + while(parts.length !== 0) { + var key = parts.shift(); + var value = Base64.decode(parts.shift()); + params[key] = value; + } + return {url:url, params:params}; +}; + +nglr.XSitePost.prototype.listen = function() { + console.log("listen()"); + var self = this; + var window = this.window; + var outQueue = this.outQueue; + var setTimeout = this.setTimeout; + var prefix = this.prefix; + var prefixLen = prefix.length; + var prefixRec = prefix + '>'; + var prefixRecLen = prefixRec.length; + window.name = prefix; + var pull = function(){ + var value = window.name; + if (value == prefix && outQueue.length > 0) { + window.name = prefix + '<' + outQueue.shift(); + } else if (value.substr(0, prefixRecLen) == prefixRec) { + self.incomingFragment(value.substr(prefixRecLen)); + window.name = prefix; + } + setTimeout(pull, self.delay); + }; + pull(); +}; diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js new file mode 100644 index 00000000..b7ae6a38 --- /dev/null +++ b/src/angular-bootstrap.js @@ -0,0 +1,100 @@ +// Copyright (C) 2008,2009 BRAT Tech LLC + +(function(previousOnLoad){ + var filename = /(.*)\/angular-(.*).js(#(.*))?/; + var scripts = document.getElementsByTagName("script"); + var scriptConfig = { + autoSubmit:true, + autoBind:true, + autoLoadDependencies:false + }; + for(var j = 0; j < scripts.length; j++) { + var src = scripts[j].src; + if (src && src.match(filename)) { + var parts = src.match(filename); + if (parts[2] == 'bootstrap') { + scriptConfig.autoLoadDependencies = true; + } + scriptConfig.server = parts[1] || ''; + if (!scriptConfig.server) { + scriptConfig.server = window.location.toString().split(window.location.pathname)[0]; + } + if (parts[4]) { + var directive = parts[4].split('&'); + for ( var i = 0; i < directive.length; i++) { + var keyValue = directive[i].split('='); + var key = keyValue[0]; + var value = keyValue.length == 1 ? true : keyValue[1]; + if (value == 'false') value = false; + if (value == 'true') value = true; + scriptConfig[key] = value; + } + } + } + } + + var addScript = function(path, server){ + server = server || scriptConfig.server; + document.write('<script type="text/javascript" src="' + server + path +'"></script>'); + }; + + if (scriptConfig.autoLoadDependencies) { + addScript("/javascripts/webtoolkit.base64.js"); + addScript("/javascripts/swfobject.js"); + addScript("/javascripts/jQuery/jquery-1.3.2.js"); + addScript("/javascripts/jQuery/jquery-ui-1.7.1.custom.min.js"); + addScript("/javascripts/underscore/underscore.js"); + addScript("/javascripts/nglr/Loader.js"); + addScript("/javascripts/nglr/API.js"); + addScript("/javascripts/nglr/Binder.js"); + addScript("/javascripts/nglr/ControlBar.js"); + addScript("/javascripts/nglr/DataStore.js"); + addScript("/javascripts/nglr/Filters.js"); + addScript("/javascripts/nglr/JSON.js"); + addScript("/javascripts/nglr/Model.js"); + addScript("/javascripts/nglr/Parser.js"); + addScript("/javascripts/nglr/Scope.js"); + addScript("/javascripts/nglr/Server.js"); + addScript("/javascripts/nglr/Users.js"); + addScript("/javascripts/nglr/Validators.js"); + addScript("/javascripts/nglr/Widgets.js"); + } else { + addScript("/ajax/libs/swfobject/2.2/swfobject.js", "http://ajax.googleapis.com"); + addScript("/ajax/libs/jquery/1.3.2/jquery.min.js", "http://ajax.googleapis.com"); + addScript("/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js", "http://ajax.googleapis.com"); + } + + window.onload = function() { + window.angular.init = function(root, config){ + var cnfgMerged = _.clone(scriptConfig||{}); + _.extend(cnfgMerged, config); + new nglr.Loader(root, jQuery("head"), cnfgMerged).load(); + }; + + var doc = window.document; + if (scriptConfig.bindRootId) { + doc = null; + var ids = scriptConfig.bindRootId.split('|'); + for ( var i = 0; i < ids.length && !doc; i++) { + var idCond = ids[i].split('?'); + var id = idCond[0]; + if (idCond.length > 1) { + if (!window.document.getElementById(idCond[1])) { + continue; + } + } + doc = window.document.getElementById(id); + } + } + if (scriptConfig.autoBind && doc) { + window.angular.init(doc); + } + if (typeof previousOnLoad === 'function') { + try { + previousOnLoad.apply(this, arguments); + } catch (e) {} + } + }; +})(window.onload); + + diff --git a/src/test/Runner.js b/src/test/Runner.js new file mode 100644 index 00000000..478ef73e --- /dev/null +++ b/src/test/Runner.js @@ -0,0 +1,160 @@ +nglr.test.ScenarioRunner = function(scenarios, body) { + this.scenarios = scenarios; + this.body = body; +}; + +nglr.test.ScenarioRunner.prototype = { + run:function(){ + this.setUpUI(); + this.runScenarios(); + }, + setUpUI:function(){ + this.body.html( + '<div id="runner">' + + '<div class="console"></div>' + + '</div>' + + '<div id="testView">' + + '<iframe></iframe>' + + '</div>'); + this.console = this.body.find(".console"); + this.testFrame = this.body.find("iframe"); + this.console.find(".run").live("click", function(){ + jQuery(this).parent().find('.log').toggle(); + }); + }, + runScenarios:function(){ + var runner = new nglr.test.Runner(this.console, this.testFrame); + _.stepper(this.scenarios, function(next, scenario, name){ + new nglr.test.Scenario(name, scenario).run(runner, next); + }, function(){ + } + ); + } +}; + +nglr.test.Runner = function(console, frame){ + this.console = console; + this.current = null; + this.tests = []; + this.frame = frame; +}; +nglr.test.Runner.prototype = { + start:function(name){ + var current = this.current = { + name:name, + start:new Date().getTime(), + scenario:jQuery('<div class="scenario"></div>') + }; + current.run = current.scenario.append( + '<div class="run">' + + '<span class="name">.</span>' + + '<span class="time">.</span>' + + '<span class="state">.</span>' + + '</run>').find(".run"); + current.log = current.scenario.append('<div class="log"></div>').find(".log"); + current.run.find(".name").text(name); + this.tests.push(current); + this.console.append(current.scenario); + }, + end:function(name){ + var current = this.current; + var run = current.run; + this.current = null; + current.end = new Date().getTime(); + current.time = current.end - current.start; + run.find(".time").text(current.time); + run.find(".state").text(current.error ? "FAIL" : "PASS"); + run.addClass(current.error ? "fail" : "pass"); + if (current.error) + run.find(".run").append('<span div="error"></span>').text(current.error); + current.scenario.find(".log").hide(); + }, + log:function(level) { + var buf = []; + for ( var i = 1; i < arguments.length; i++) { + var arg = arguments[i]; + buf.push(typeof arg == "string" ?arg:nglr.toJson(arg)); + } + var log = jQuery('<div class="' + level + '"></div>'); + log.text(buf.join(" ")); + this.current.log.append(log); + this.console.scrollTop(this.console[0].scrollHeight); + if (level == "error") + this.current.error = buf.join(" "); + } +}; + +nglr.test.Scenario = function(name, scenario){ + this.name = name; + this.scenario = scenario; +}; +nglr.test.Scenario.prototype = { + run:function(runner, callback) { + var self = this; + _.stepper(this.scenario, function(next, steps, name){ + if (name.charAt(0) == '$') { + next(); + } else { + runner.start(self.name + "::" + name); + var allSteps = (self.scenario.$before||[]).concat(steps); + _.stepper(allSteps, function(next, step){ + self.executeStep(runner, step, next); + }, function(){ + runner.end(); + next(); + }); + } + }, callback); + }, + verb:function(step){ + var fn = null; + if (!step) fn = function (){ throw "Step is null!"; } + else if (step.Given) fn = angular.test.GIVEN[step.Given]; + else if (step.When) fn = angular.test.WHEN[step.When]; + else if (step.Then) fn = angular.test.THEN[step.Then]; + return fn || function (){ + throw "ERROR: Need Given/When/Then got: " + nglr.toJson(step); + }; + }, + context: function(runner) { + var frame = runner.frame; + var window = frame[0].contentWindow; + var document; + if (window.jQuery) + document = window.jQuery(window.document); + var context = { + frame:frame, + window:window, + log:_.bind(runner.log, runner, "info"), + document:document, + assert:function(element, path){ + if (element.size() != 1) { + throw "Expected to find '1' found '"+ + element.size()+"' for '"+path+"'."; + } + return element; + }, + element:function(path){ + var exp = path.replace("{{","[ng-bind=").replace("}}", "]"); + var element = document.find(exp); + return context.assert(element, path); + } + }; + return context; + }, + executeStep:function(runner, step, callback) { + if (!step) { + callback(); + return; + } + runner.log("info", nglr.toJson(step)); + var fn = this.verb(step); + var context = this.context(runner); + _.extend(context, step); + try { + (fn.call(context)||function(c){c();})(callback); + } catch (e) { + runner.log("error", "ERROR: " + nglr.toJson(e)); + } + } +}; diff --git a/src/test/Steps.js b/src/test/Steps.js new file mode 100644 index 00000000..af4b84d6 --- /dev/null +++ b/src/test/Steps.js @@ -0,0 +1,57 @@ +angular.test.GIVEN = { + browser:function(){ + var self = this; + if (jQuery.browser.safari && this.frame.attr('src') == this.at) { + this.window.location.reload(); + } else { + this.frame.attr('src', this.at); + } + return function(done){ + self.frame.load(function(){ + self.frame.unbind(); + done(); + }); + }; + }, + dataset:function(){ + this.frame.name="$DATASET:" + nglr.toJson({dataset:this.dataset}); + } +}; +angular.test.WHEN = { + enter:function(){ + var element = this.element(this.at); + element.attr('value', this.text); + element.change(); + }, + click:function(){ + var element = this.element(this.at); + var input = element[0]; + // emulate the browser behavior which causes it + // to be overridden at the end. + var checked = input.checked = !input.checked; + element.click(); + input.checked = checked; + }, + select:function(){ + var element = this.element(this.at); + var path = "option[value=" + this.option + "]"; + var option = this.assert(element.find(path)); + option[0].selected = !option[0].selected; + element.change(); + } +}; +angular.test.THEN = { + text:function(){ + var element = this.element(this.at); + if (typeof this.should_be != undefined ) { + var should_be = this.should_be; + if (_.isArray(this.should_be)) + should_be = JSON.stringify(should_be); + if (element.text() != should_be) + throw "Expected " + should_be + + " but was " + element.text() + "."; + } + }, + drainRequestQueue:function(){ + } +}; diff --git a/src/test/_namespace.js b/src/test/_namespace.js new file mode 100644 index 00000000..78f430f1 --- /dev/null +++ b/src/test/_namespace.js @@ -0,0 +1,5 @@ +if (!angular) angular = {}; +if (!angular.test) angular.test = {}; +if (!angular.test.GIVEN) angular.test.GIVEN = {}; +if (!angular.test.WHEN) angular.test.WHEN = {}; +if (!angular.test.THEN) angular.test.THEN = {}; |
