diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 119 | ||||
| -rw-r--r-- | src/Compiler.js | 208 | ||||
| -rw-r--r-- | src/Filters.js | 52 | ||||
| -rw-r--r-- | src/Scope.js | 65 | ||||
| -rw-r--r-- | src/directives.js | 234 | ||||
| -rw-r--r-- | src/directivesAngularCom.js | 29 | ||||
| -rw-r--r-- | src/jqLite.js | 185 | ||||
| -rw-r--r-- | src/markup.js | 64 | ||||
| -rw-r--r-- | src/scenario/bootstrap.js | 6 | ||||
| -rw-r--r-- | src/widgets2.js | 101 |
10 files changed, 787 insertions, 276 deletions
diff --git a/src/Angular.js b/src/Angular.js index 39a6e91d..95f7325a 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1,6 +1,7 @@ if (typeof document.getAttribute == 'undefined') document.getAttribute = function() {}; if (typeof Node == 'undefined') { + //TODO: can we get rid of this? Node = { ELEMENT_NODE : 1, ATTRIBUTE_NODE : 2, @@ -18,24 +19,43 @@ if (typeof Node == 'undefined') { } function noop() {} +function identity($) {return $;} if (!window['console']) window['console']={'log':noop, 'error':noop}; +function extensionMap(angular, name) { + var extPoint; + return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + if (isDefined(fn)) { + extPoint[name] = extend(fn, prop || {}); + } + return extPoint[name]; + }); +} + +function extensionList(angular, name) { + var extPoint, length = 0; + return angular[name] || (extPoint = angular[name] = function (fn, prop){ + if (isDefined(fn)) { + extPoint[length] = extend(fn, prop || {}); + length++; + } + return extPoint; + }); +} + var consoleNode, msie, - jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy - foreach = _.each, - extend = _.extend, - slice = Array.prototype.slice, - identity = _.identity, - angular = window['angular'] || (window['angular'] = {}), - angularValidator = angular['validator'] || (angular['validator'] = {}), - angularDirective = angular['directive'] || (angular['directive'] = function(name, fn){ - if (fn) {angularDirective[name] = fn;}; - return angularDirective[name]; - }), - angularFilter = angular['filter'] || (angular['filter'] = {}), - angularFormatter = angular['formatter'] || (angular['formatter'] = {}), - angularCallbacks = angular['callbacks'] || (angular['callbacks'] = {}), - angularAlert = angular['alert'] || (angular['alert'] = function(){ + jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy + slice = Array.prototype.slice, + angular = window['angular'] || (window['angular'] = {}), + angularTextMarkup = extensionList(angular, 'textMarkup'), + angularAttrMarkup = extensionList(angular, 'attrMarkup'), + angularDirective = extensionMap(angular, 'directive'), + angularWidget = extensionMap(angular, 'widget'), + angularValidator = extensionMap(angular, 'validator'), + angularFilter = extensionMap(angular, 'filter'), + angularFormatter = extensionMap(angular, 'formatter'), + angularCallbacks = extensionMap(angular, 'callbacks'), + angularAlert = angular['alert'] || (angular['alert'] = function(){ log(arguments); window.alert.apply(window, arguments); }); angular['copy'] = copy; @@ -44,6 +64,35 @@ var isVisible = isVisible || function (element) { return jQuery(element).is(":visible"); }; +function foreach(obj, iterator, context) { + var key; + if (obj) { + if (obj.forEach) { + obj.forEach(iterator, context); + } else if (obj instanceof Array) { + for (key = 0; key < obj.length; key++) + iterator.call(context, obj[key], key); + } else { + for (key in obj) + iterator.call(context, obj[key], key); + } + } + return obj; +} + +function extend(dst, obj) { + foreach(obj, function(value, key){ + dst[key] = value; + }); + return dst; +} + +function isDefined(value){ return typeof value != 'undefined'; } +function isObject(value){ return typeof value == 'object';} +function isString(value){ return typeof value == 'string';} +function isArray(value) { return value instanceof Array; } +function isFunction(value){ return typeof value == 'function';} + function log(a, b, c){ var console = window['console']; switch(arguments.length) { @@ -97,14 +146,15 @@ function isNode(inp) { } function isLeafNode (node) { - switch (node.nodeName) { - case "OPTION": - case "PRE": - case "TITLE": - return true; - default: - return false; + if (node) { + switch (node.nodeName) { + case "OPTION": + case "PRE": + case "TITLE": + return true; + } } + return false; } function copy(source, destination){ @@ -160,15 +210,32 @@ function escapeAttr(html) { function bind(_this, _function) { var curryArgs = slice.call(arguments, 2, arguments.length); - if (!_this) - throw "Missing this"; - if (!_.isFunction(_function)) - throw "Missing function"; return function() { return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); }; } +function bindTry(_this, _function) { + var args = arguments, + last = args.length - 1, + curryArgs = slice.call(args, 2, last), + exceptionHandler = args[last]; + return function() { + try { + return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); + } catch (e) { + if (e = exceptionHandler(e)) throw e; + } + }; +} + +function errorHandlerFor(element) { + return function(error){ + element.attr('ng-error', angular.toJson(error)); + element.addClass('ng-exception'); + }; +} + function outerHTML(node) { var temp = document.createElement('div'); temp.appendChild(node); diff --git a/src/Compiler.js b/src/Compiler.js index f4d901fb..ba598a43 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -12,12 +12,13 @@ function Template() { Template.prototype = { init: function(element, scope) { + element = jqLite(element); foreach(this.inits, function(fn) { - scope.apply(fn, nodeLite(element)); + scope.apply(fn, element); }); var i, - childNodes = element.childNodes, + childNodes = element[0].childNodes, children = this.children, paths = this.paths, length = paths.length; @@ -26,106 +27,74 @@ Template.prototype = { } }, + addInit:function(init) { if (init) { this.inits.push(init); } }, - setExclusiveInit: function(init) { - this.inits = [init]; - this.addInit = noop; - }, - addChild: function(index, template) { - this.paths.push(index); - this.children.push(template); + if (template) { + this.paths.push(index); + this.children.push(template); + } + }, + + empty: function() { + return this.inits.length == 0 && this.paths.length == 0; } }; /////////////////////////////////// -//NodeLite +//Compiler ////////////////////////////////// - -function NodeLite(element) { - this.element = element; -} - -function nodeLite(element) { - return element instanceof NodeLite ? element : new NodeLite(element); +function isTextNode(node) { + return node.nodeType == Node.TEXT_NODE; } -NodeLite.prototype = { - eachTextNode: function(fn){ - var i, chldNodes = this.element.childNodes || [], size = chldNodes.length, chld; - for (i = 0; i < size; i++) { - if((chld = new NodeLite(chldNodes[i])).isText()) { - fn(chld, i); - } +function eachTextNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], size = chldNodes.length, chld; + for (i = 0; i < size; i++) { + if(isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); } - }, - - eachNode: function(fn){ - var i, chldNodes = this.element.childNodes || [], size = chldNodes.length, chld; - for (i = 0; i < size; i++) { - if(!(chld = new NodeLite(chldNodes[i])).isText()) { - fn(chld, i); - } - } - }, + } +} - eachAttribute: function(fn){ - var i, attrs = this.element.attributes || [], size = attrs.length, chld, attr; - for (i = 0; i < size; i++) { - var attr = attrs[i]; - fn(attr.name, attr.value); +function eachNode(element, fn){ + var i, chldNodes = element[0].childNodes || [], size = chldNodes.length, chld; + for (i = 0; i < size; i++) { + if(!isTextNode(chld = chldNodes[i])) { + fn(jqLite(chld), i); } - }, - - replaceWith: function(replaceNode) { - this.element.parentNode.replaceChild(nodeLite(replaceNode).element, this.element); - }, - - removeAttribute: function(name) { - this.element.removeAttribute(name); - }, - - after: function(element) { - this.element.parentNode.insertBefore(element, this.element.nextSibling); - }, - - attr: function(name, value){ - if (typeof value == 'undefined') { - return this.element.getAttribute(name); - } else { - this.element.setAttribute(name); - } - }, - - isText: function() { return this.element.nodeType == Node.TEXT_NODE; }, - text: function() { return this.element.nodeValue; }, - clone: function() { return nodeLite(this.element.cloneNode(true)); } -}; + } +} -/////////////////////////////////// -//Compiler -////////////////////////////////// +function eachAttribute(element, fn){ + var i, attrs = element[0].attributes || [], size = attrs.length, chld, attr; + for (i = 0; i < size; i++) { + var attr = attrs[i]; + fn(attr.name, attr.value); + } +} -function Compiler(markup, directives, widgets){ - this.markup = markup; +function Compiler(textMarkup, attrMarkup, directives, widgets){ + this.textMarkup = textMarkup; + this.attrMarkup = attrMarkup; this.directives = directives; this.widgets = widgets; } -DIRECTIVE = /^ng-(.*)$/; - Compiler.prototype = { - compile: function(element) { - var template = this.templetize(nodeLite(element)) || new Template(); - return function(element){ - var scope = new Scope(); + compile: function(rawElement) { + rawElement = jqLite(rawElement); + var template = this.templatize(rawElement) || new Template(); + return function(element, parentScope){ + var scope = new Scope(parentScope); scope.element = element; + // todo return should be a scope with everything already set on it as element return { scope: scope, element:element, @@ -134,55 +103,64 @@ Compiler.prototype = { }; }, - templetize: function(element){ + templatize: function(element){ var self = this, - markup = self.markup, - markupSize = markup.length, - directives = self.directives, + elementName = element[0].nodeName, widgets = self.widgets, - recurse = true, + widget = widgets[elementName], + directives = self.directives, + descend = true, exclusive = false, - template; - - // process markup for text nodes only - element.eachTextNode(function(textNode){ - for (var i = 0, text = textNode.text(); i < markupSize; i++) { - markup[i].call(self, text, textNode, element); - } - }); + directiveQueue = [], + template = new Template(), + selfApi = { + compile: bind(self, self.compile), + comment:function(text) {return jqLite(document.createComment(text));}, + element:function(type) {return jqLite(document.createElement(type));}, + text:function(text) {return jqLite(document.createTextNode(text));}, + descend: function(value){ if(isDefined(value)) descend = value; return descend;} + }; + + if (widget) { + template.addInit(widget.call(selfApi, element)); + } else { + // process markup for text nodes only + eachTextNode(element, function(textNode){ + var text = textNode.text(); + foreach(self.textMarkup, function(markup){ + markup.call(selfApi, text, textNode, element); + }); + }); - // Process attributes/directives - element.eachAttribute(function(name, value){ - var match = name.match(DIRECTIVE), - directive, init; - if (!exclusive && match) { - directive = directives[match[1]]; - if (directive) { - init = directive.call(self, value, element); - template = template || new Template(); + // Process attributes/directives + eachAttribute(element, function(name, value){ + foreach(self.attrMarkup, function(markup){ + markup.call(selfApi, value, name, element); + }); + }); + eachAttribute(element, function(name, value){ + var directive = directives[name]; + if (!exclusive && directive) { if (directive.exclusive) { - template.setExclusiveInit(init); exclusive = true; - } else { - template.addInit(init); + directiveQueue = []; } - recurse = recurse && init; - } else { - error("Directive '" + match[0] + "' is not recognized."); + directiveQueue.push(bindTry(selfApi, directive, value, element, errorHandlerFor(element))); } - } - }); + }); - // Process non text child nodes - if (recurse) { - element.eachNode(function(child, i){ - var childTemplate = self.templetize(child); - if(childTemplate) { - template = template || new Template(); - template.addChild(i, childTemplate); - } + // Execute directives + foreach(directiveQueue, function(directive){ + template.addInit(directive()); }); + + // Process non text child nodes + if (descend) { + eachNode(element, function(child, i){ + template.addChild(i, self.templatize(child)); + }); + } } - return template; + return template.empty() ? null : template; } }; diff --git a/src/Filters.js b/src/Filters.js index 60d53fb9..dac8d31d 100644 --- a/src/Filters.js +++ b/src/Filters.js @@ -27,7 +27,7 @@ foreach({ jQuery(this.$element).toggleClass('ng-format-negative', amount < 0); return '$' + angularFilter['number'].apply(this, [amount, 2]); }, - + 'number': function(amount, fractionSize){ if (isNaN(amount) || !isFinite(amount)) { return ''; @@ -55,15 +55,15 @@ foreach({ } return text; }, - + 'date': function(amount) { }, - + 'json': function(object) { jQuery(this.$element).addClass("ng-monospace"); return toJson(object, true); }, - + 'trackPackage': (function(){ var MATCHERS = [ { name: "UPS", @@ -89,7 +89,7 @@ foreach({ var returnValue; foreach(MATCHERS, function(carrier){ foreach(carrier.regexp, function(regexp){ - if (regexp.test(tNo)) { + if (!returnValue && regexp.test(tNo)) { var text = carrier.name + ": " + trackingNo; var url = carrier.url + trackingNo; returnValue = new angularFilter.Meta({ @@ -97,19 +97,17 @@ foreach({ url:url, html: '<a href="' + escapeAttr(url) + '">' + text + '</a>', trackingNo:trackingNo}); - _.breakLoop(); } }); - if (returnValue) _.breakLoop(); }); - if (returnValue) + if (returnValue) return returnValue; else if (trackingNo) return noMatch || new angularFilter.Meta({text:trackingNo + " is not recognized"}); else return null; };})(), - + 'link': function(obj, title) { var text = title || angularFilter.Meta.get(obj); var url = angularFilter.Meta.get(obj, "url") || angularFilter.Meta.get(obj); @@ -122,13 +120,13 @@ foreach({ } return obj; }, - - + + 'bytes': (function(){ var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; return function(size) { if(size === null) return ""; - + var suffix = 0; while (size > 1000) { size = size / 1024; @@ -142,7 +140,7 @@ foreach({ return txt + " " + SUFFIX[suffix]; }; })(), - + 'image': function(obj, width, height) { if (obj && obj.url) { var style = ""; @@ -155,36 +153,36 @@ foreach({ } return null; }, - + 'lowercase': function (obj) { var text = angularFilter.Meta.get(obj); return text ? ("" + text).toLowerCase() : text; }, - + 'uppercase': function (obj) { var text = angularFilter.Meta.get(obj); return text ? ("" + text).toUpperCase() : text; }, - + 'linecount': function (obj) { var text = angularFilter.Meta.get(obj); if (text==='' || !text) return 1; return text.split(/\n|\f/).length; }, - + 'if': function (result, expression) { return expression ? result : undefined; }, - + 'unless': function (result, expression) { return expression ? undefined : result; }, - + 'googleChartApi': extend( function(type, data, width, height) { data = data || {}; var chart = { - 'cht':type, + 'cht':type, 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), 'chtt':angularFilterGoogleChartApi['title'](data), 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), @@ -210,7 +208,7 @@ foreach({ var values = seriesValues.join('|'); return values === "" ? null : "t:" + values; }, - + 'title': function(data){ var titles = []; var title = data['title'] || []; @@ -219,7 +217,7 @@ foreach({ }); return titles.join('|'); }, - + 'collect': function(data, key){ var outterValues = []; var count = 0; @@ -234,7 +232,7 @@ foreach({ }); return count?outterValues.join(','):null; }, - + 'encode': function(params, width, height) { width = width || 200; height = height || width; @@ -253,8 +251,8 @@ foreach({ } } ), - - + + 'qrcode': function(value, width, height) { return angularFilterGoogleChartApi['encode']({ 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); @@ -291,11 +289,11 @@ foreach({ return angularFilterGoogleChartApi('s', data, width, height); } }, - + 'html': function(html){ return new angularFilter.Meta({html:html}); }, - + 'linky': function(text){ if (!text) return text; function regExpEscape(text) { diff --git a/src/Scope.js b/src/Scope.js index 3633f960..daafabb0 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -1,18 +1,23 @@ function Scope(initialState, name) { - this.widgets = []; - this.evals = []; - this.watchListeners = {}; - this.name = name; + var self = this; + self.widgets = []; + self.evals = []; + self.watchListeners = {}; + self.name = name; initialState = initialState || {}; var State = function(){}; State.prototype = initialState; - this.state = new State(); - this.state['$parent'] = initialState; + self.state = new State(); + extend(self.state, { + '$parent': initialState, + '$watch': bind(self, self.addWatchListener), + '$eval': bind(self, self.eval), + // change name to onEval? + '$addEval': bind(self, self.addEval) + }); if (name == "ROOT") { - this.state['$root'] = this.state; + self.state['$root'] = self.state; } - this.set('$watch', bind(this, this.addWatchListener)); - this.set('$eval', bind(this, this.addEval)); }; Scope.expressionCache = {}; @@ -47,6 +52,7 @@ Scope.getter = function(instance, path) { }; Scope.prototype = { + // TODO: rename to update? or eval? updateView: function() { var self = this; this.fireWatchers(); @@ -64,7 +70,13 @@ Scope.prototype = { addEval: function(fn, listener) { // todo: this should take a function/string and a listener - this.evals.push(fn); + // todo: this is a hack, which will need to be cleaned up. + var self = this, + listenFn = listener || noop, + expr = self.compile(fn); + this.evals.push(function(){ + self.apply(listenFn, expr()); + }); }, isProperty: function(exp) { @@ -103,19 +115,26 @@ Scope.prototype = { this.eval(expressionText + "=" + toJson(value)); }, - eval: function(expressionText, context) { -// log('Scope.eval', expressionText); - var expression = Scope.expressionCache[expressionText]; - if (!expression) { - var parser = new Parser(expressionText); - expression = parser.statements(); + compile: function(exp) { + if (isFunction(exp)) return bind(this.state, exp); + var expFn = Scope.expressionCache[exp], self = this; + if (!expFn) { + var parser = new Parser(exp); + expFn = parser.statements(); parser.assertAllConsumed(); - Scope.expressionCache[expressionText] = expression; + Scope.expressionCache[exp] = expFn; } - context = context || {}; - context.scope = this; - context.self = this.state; - return expression(context); + return function(context){ + context = context || {}; + context.self = self.state; + context.scope = self; + return expFn.call(self, context); + }; + }, + + eval: function(expressionText, context) { +// log('Scope.eval', expressionText); + return this.compile(expressionText)(context); }, //TODO: Refactor. This function needs to be an execution closure for widgets @@ -189,6 +208,10 @@ Scope.prototype = { }, addWatchListener: function(watchExpression, listener) { + // TODO: clean me up! + if (!isFunction(listener)) { + listener = this.compile(listener); + } var watcher = this.watchListeners[watchExpression]; if (!watcher) { watcher = {listeners:[], expression:watchExpression}; diff --git a/src/directives.js b/src/directives.js index 26cbfe2c..747da3f5 100644 --- a/src/directives.js +++ b/src/directives.js @@ -1,121 +1,187 @@ - -angular.directive("auth", function(expression, element){ +angularDirective("ng-init", function(expression){ return function(){ - if(expression == "eager") { - this.$users.fetchCurrent(); - } + this.$eval(expression); }; }); - -//expression = "book=Book:{year=2000}" -angular.directive("entity", function(expression, element){ - //parse expression, ignore element - var entityName; // "Book"; - var instanceName; // "book"; - var defaults; // {year: 2000}; - - parse(expression); - +angularDirective("ng-eval", function(expression){ return function(){ - this[entityName] = this.$datastore.entity(entityName, defaults); - this[instanceName] = this[entityName](); - this.$watch("$anchor."+instanceName, function(newAnchor){ - this[instanceName] = this[entityName].get(this.$anchor[instanceName]); - }); + this.$addEval(expression); }; }); - -angular.directive("init", function(expression, element){ - return function(){ - this.$eval(expresssion); +angularDirective("ng-bind", function(expression){ + return function(element) { + this.$watch(expression, function(value){ + element.text(value); + }); }; }); - -//translation of {{ }} to ng-bind is external to this -angular.directive("bind", function(expression, element){ - return function() { - this.$watch(expression, function(value){ - element.innerText = value; +var bindTemplateCache = {}; +function compileBindTemplate(template){ + var fn = bindTemplateCache[template]; + if (!fn) { + var bindings = []; + foreach(parseBindings(template), function(text){ + var exp = binding(text); + bindings.push(exp ? function(){ + return this.$eval(exp); + } : function(){ + return text; + }); + }); + bindTemplateCache[template] = fn = function(){ + var parts = [], self = this; + foreach(bindings, function(fn){ + parts.push(fn.call(self)); + }); + return parts.join(''); + }; + } + return fn; +}; +angularDirective("ng-bind-template", function(expression){ + var templateFn = compileBindTemplate(expression); + return function(element) { + var lastValue; + this.$addEval(function() { + var value = templateFn.call(this); + if (value != lastValue) { + element.text(value); + lastValue = value; + } }); }; }); - -// translation of {{ }} to ng-bind-attr is external to this -// <a href="http://example.com?id={{book.$id}}" alt="{{book.$name}}">link</a> -// becomes -// <a href="" ng-bind-attr="{href:'http://example.com?id={{book.$id}}', alt:'{{book.$name}}'}">link</a> -angular.directive("bind-attr", function(expression, element){ - return function(expression, element){ - var jElement = jQuery(element); - this.$watch(expression, _(jElement.attr).bind(jElement)); +angularDirective("ng-bind-attr", function(expression){ + return function(element){ + this.$addEval(function(){ + foreach(this.$eval(expression), function(value, key){ + element.attr(key, compileBindTemplate(value).call(this)); + }, this); + }); }; }); -angular.directive("repeat", function(expression, element){ - var anchor = document.createComment(expression); - jQuery(element).replace(anchor); - var template = this.compile(element); - var lhs = "item"; - var rhs = "items"; +angularDirective("ng-non-bindable", function(){ + this.descend(false); +}); + +angularDirective("ng-repeat", function(expression, element){ + var reference = this.comment("ng-repeat: " + expression), + r = element.removeAttr('ng-repeat'), + template = this.compile(element), + match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), + lhs, rhs, valueIdent, keyIdent; + if (! match) { + throw "Expected ng-repeat in form of 'item in collection' but got '" + + expression + "'."; + } + lhs = match[1]; + rhs = match[2]; + match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); + if (!match) { + throw "'item' in 'item in collection' should be identifier or (key, value) but got '" + + keyValue + "'."; + } + valueIdent = match[3] || match[1]; + keyIdent = match[2]; + + var parent = element.parent(); + element.replaceWith(reference); return function(){ - var children = []; - this.$eval(rhs, function(items){ - foreach(children, function(child){ - child.element.remove(); - }); - foreach(items, function(item){ - var child = template(item); // create scope - element.addChild(child.element, anchor); - children.push(child); + var children = [], + currentScope = this; + this.$addEval(rhs, function(items){ + var index = 0, childCount = children.length, childScope, lastElement = reference; + foreach(items || [], function(value, key){ + if (index < childCount) { + // reuse existing child + childScope = children[index]; + } else { + // grow children + childScope = template(element.clone(), currentScope); + childScope.init(); + childScope.scope.set('$index', index); + childScope.element.attr('ng-index', index); + lastElement.after(childScope.element); + children.push(childScope); + } + childScope.scope.set(valueIdent, value); + if (keyIdent) childScope.scope.set(keyIdent, key); + childScope.scope.updateView(); + lastElement = childScope.element; + index ++; }); + // shrink children + while(children.length > index) { + children.pop().element.remove(); + } }); }; -}); +}, {exclusive: true}); +angularDirective("ng-action", function(expression, element){ + return function(){ + var self = this; + element.click(function(){ + self.$eval(expression); + }); + }; +}); -//ng-non-bindable -angular.directive("non-bindable", function(expression, element){ - return false; +angularDirective("ng-watch", function(expression, element){ + var match = expression.match(/^([^.]*):(.*)$/); + if (!match) { + throw "Expecting watch expression 'ident_to_watch: watch_statement' got '" + + expression + "'"; + } + return function(){ + this.$watch(match[1], match[2]); + }; }); -//Styling -// -//ng-class -//ng-class-odd, ng-class-even -//ng-style -//ng-show, ng-hide +function ngClass(selector) { + return function(expression, element){ + var existing = element[0].className + ' '; + return function(element){ + this.$addEval(expression, function(value){ + if (selector(this.$index)) { + if (isArray(value)) value = value.join(' '); + element[0].className = (existing + value).replace(/\s\s+/g, ' '); + } + }); + }; + }; +} +angularDirective("ng-class", ngClass(function(){return true;})); +angularDirective("ng-class-odd", ngClass(function(i){return i % 2 == 1;})); +angularDirective("ng-class-even", ngClass(function(i){return i % 2 == 0;})); -angular.directive("action", function(expression, element){ - return function(){ - var self = this; - jQuery(element).click(function(){ - self.$eval(expression); +angularDirective("ng-show", function(expression, element){ + return function(element){ + this.$addEval(expression, function(value){ + element.css('display', toBoolean(value) ? '' : 'none'); }); }; }); -//ng-eval -angular.directive("eval", function(expression, element){ - return function(){ - this.$eval(expression); +angularDirective("ng-hide", function(expression, element){ + return function(element){ + this.$addEval(expression, function(value){ + element.css('display', toBoolean(value) ? 'none' : ''); + }); }; }); -//ng-watch -// <div ng-watch="$anchor.book: book=Book.get();"/> -angular.directive("watch", function(expression, element){ - var watches = { - 'lhs':'rhs' - }; // parse - return function(){ - this.$watch(watches); + +angularDirective("ng-style", function(expression, element){ + return function(element){ + this.$addEval(expression, function(value){ + element.css(value); + }); }; }); -//widget related -//ng-validate, ng-required, ng-formatter -//ng-error diff --git a/src/directivesAngularCom.js b/src/directivesAngularCom.js new file mode 100644 index 00000000..84032bdd --- /dev/null +++ b/src/directivesAngularCom.js @@ -0,0 +1,29 @@ + +angular.directive("auth", function(expression, element){ + return function(){ + if(expression == "eager") { + this.$users.fetchCurrent(); + } + }; +}); + + +//expression = "book=Book:{year=2000}" +angular.directive("entity", function(expression, element){ + //parse expression, ignore element + var entityName; // "Book"; + var instanceName; // "book"; + var defaults; // {year: 2000}; + + parse(expression); + + return function(){ + this[entityName] = this.$datastore.entity(entityName, defaults); + this[instanceName] = this[entityName](); + this.$watch("$anchor."+instanceName, function(newAnchor){ + this[instanceName] = this[entityName].get(this.$anchor[instanceName]); + }); + }; +}); + + diff --git a/src/jqLite.js b/src/jqLite.js new file mode 100644 index 00000000..035a7a1b --- /dev/null +++ b/src/jqLite.js @@ -0,0 +1,185 @@ + +/////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}; +var jqName = 'ng-' + new Date().getTime(); +var jqId = 1; +function jqNextId() { return jqId++; } + +var addEventListener = window.document.attachEvent ? + function(element, type, fn) { + element.attachEvent('on' + type, fn); + } : function(element, type, fn) { + element.addEventListener(type, fn, false); + }; + +var removeEventListener = window.document.detachEvent ? + function(element, type, fn) { + element.detachEvent('on' + type, fn); + } : function(element, type, fn) { + element.removeEventListener(type, fn, false); + }; + +function jqClearData(element) { + var cacheId = element[jqName], + cache = jqCache[cacheId]; + if (cache) { + foreach(cache.bind || {}, function(fn, type){ + removeEventListener(element, type, fn); + }); + delete jqCache[cacheId]; + delete element[jqName]; + } +}; + +function JQLite(element) { + this[0] = element; +} + +function jqLite(element) { + if (typeof element == 'string') { + var div = document.createElement('div'); + div.innerHTML = element; + element = div.childNodes[0]; + } + return element instanceof JQLite ? element : new JQLite(element); +} + +JQLite.prototype = { + data: function(key, value) { + var element = this[0], + cacheId = element[jqName], + cache = jqCache[cacheId || -1]; + if (isDefined(value)) { + if (!cache) { + element[jqName] = cacheId = jqNextId(); + cache = jqCache[cacheId] = {}; + } + cache[key] = value; + } else { + return cache ? cache[key] : null; + } + }, + + removeData: function(){ + jqClearData(this[0]); + }, + + dealoc: function(){ + (function dealoc(element){ + jqClearData(element); + for ( var i = 0, children = element.childNodes; i < children.length; i++) { + dealoc(children[0]); + } + })(this[0]); + }, + + bind: function(type, fn){ + var element = this[0], + bind = this.data('bind'), + eventHandler; + if (!bind) this.data('bind', bind = {}); + eventHandler = bind[type]; + if (!eventHandler) { + bind[type] = eventHandler = function() { + var self = this; + foreach(eventHandler.fns, function(fn){ + fn.apply(self, arguments); + }); + }; + eventHandler.fns = []; + addEventListener(element, type, eventHandler); + } + eventHandler.fns.push(fn); + }, + + trigger: function(type) { + var cache = this.data('bind'); + if (cache) { + (cache[type] || noop)(); + } + }, + + click: function(fn) { + if (fn) + this.bind('click', fn); + else + this.trigger('click'); + }, + + replaceWith: function(replaceNode) { + this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); + }, + + remove: function() { + this.dealoc(); + this[0].parentNode.removeChild(this[0]); + }, + + removeAttr: function(name) { + this[0].removeAttribute(name); + }, + + after: function(element) { + this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); + }, + + hasClass: function(selector) { + var className = " " + selector + " "; + if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { + return true; + } + return false; + }, + + addClass: function( selector ) { + if (!this.hasClass(selector)) { + this[0].className += ' ' + selector; + } + }, + + css: function(name, value) { + var style = this[0].style; + if (isString(name)) { + if (isDefined(value)) { + style[name] = value; + } else { + return style[name]; + } + } else { + extend(style, name); + } + }, + + attr: function(name, value){ + var e = this[0]; + if (isObject(name)) { + foreach(name, function(value, name){ + e.setAttribute(name, value); + }); + } else if (isDefined(value)) { + e.setAttribute(name, value); + } else { + return e.getAttribute(name); + } + }, + + text: function(value) { + if (isDefined(value)) { + this[0].textContent = value; + } + return this[0].textContent; + }, + + html: function(value) { + if (isDefined(value)) { + this[0].innerHTML = value; + } + return this[0].innerHTML; + }, + + parent: function() { return jqLite(this[0].parentNode);}, + clone: function() { return jqLite(this[0].cloneNode(true)); } +}; diff --git a/src/markup.js b/src/markup.js new file mode 100644 index 00000000..add7ce03 --- /dev/null +++ b/src/markup.js @@ -0,0 +1,64 @@ +function parseBindings(string) { + var results = []; + var lastIndex = 0; + var index; + while((index = string.indexOf('{{', lastIndex)) > -1) { + if (lastIndex < index) + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + + index = string.indexOf('}}', index); + index = index < 0 ? string.length : index + 2; + + results.push(string.substr(lastIndex, index - lastIndex)); + lastIndex = index; + } + if (lastIndex != string.length) + results.push(string.substr(lastIndex, string.length - lastIndex)); + return results.length === 0 ? [ string ] : results; +}; + +function binding(string) { + var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); + return binding ? binding[1] : null; +}; + +function hasBindings(bindings) { + return bindings.length > 1 || Binder.binding(bindings[0]) !== null; +}; + +angularTextMarkup(function(text, textNode, parentElement) { + var bindings = parseBindings(text), + self = this; + if (isLeafNode(parentElement[0])) { + parentElement.attr('ng-bind-template', text); + } else { + var cursor = textNode, newElement; + foreach(parseBindings(text), function(text){ + var exp = binding(text); + if (exp) { + newElement = self.element('span'); + newElement.attr('ng-bind', exp); + } else { + newElement = self.text(text); + } + cursor.after(newElement); + cursor = newElement; + }); + } + textNode.remove(); +}); + +var NG_BIND_ATTR = 'ng-bind-attr'; +angularAttrMarkup(function(value, name, element){ + if (name.substr(0, 3) != 'ng-') { + var bindings = parseBindings(value), + bindAttr; + if (hasBindings(bindings)) { + element.removeAttr(name); + bindAttr = fromJson(element.attr(NG_BIND_ATTR) || "{}"); + bindAttr[name] = value; + element.attr(NG_BIND_ATTR, toJson(bindAttr)); + } + } +}); diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js index 1d40b9d0..b49530df 100644 --- a/src/scenario/bootstrap.js +++ b/src/scenario/bootstrap.js @@ -8,7 +8,7 @@ var parts = src.match(filename); return parts[1]; } - } + } })(); function addScript(path) { document.write('<script type="text/javascript" src="' + prefix + path + '"></script>'); @@ -17,7 +17,7 @@ document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>'); }; window.onload = function(){ - if (!_.stepper) { + if (!_.stepper) { _.stepper = function(collection, iterator, done){ var keys = _.keys(collection); function next() { @@ -38,7 +38,7 @@ }; addCSS("../../css/angular-scenario.css"); addScript("../../lib/underscore/underscore.js"); - addScript("../../lib/jquery/jquery-1.3.2.js"); + addScript("../../lib/jquery/jquery-1.4.2.js"); addScript("../angular-bootstrap.js"); addScript("_namespace.js"); addScript("Steps.js"); diff --git a/src/widgets2.js b/src/widgets2.js new file mode 100644 index 00000000..b0f467d4 --- /dev/null +++ b/src/widgets2.js @@ -0,0 +1,101 @@ +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// +///////////////////////////////////////// + + + + + +//widget related +//ng-validate, ng-required, ng-formatter +//ng-error + +//ng-scope ng-controller???? + +// <input type="text" name="bla" ng-action=""> -> <ng:textinput name="" ng-action=""/> +angular.widget("inputtext", function(element) { + var expression = element.attr('name'); + var formatter = this.formatter(element.attr('formatter')); + var validator = this.validator(element.attr('validator')); + + function validate(value) { + var error = validator(element); + if (error) { + element.addClass("ng-error"); + scope.markInvalid(this); //move out of scope + } else { + scope.clearInvalid(this); + } + } + + + element.keyup(this.withScope(function(){ + this.$evalSet(expression, formatter.parse(element.val())); + validate(element.val()); + })); + + return {watch: expression, apply: function(newValue){ + element.val(formatter.format(newValue)); + validate(element.val()); + }}; + +}); + +angular.widget("inputfile", function(element) { + +}); + +angular.widget("inputradio", function(element) { + +}); + + +// <ng:colorpicker name="chosenColor" > +angular.widget("colorpicker", function(element) { + var name = element.attr('datasource'); + var formatter = this.formatter(element.attr('ng-formatter')); + + element.colorPicker(this.withScope(function(selectedColor){ + this.$evalSet(name, formatter.parse(selectedColor)); + })); + + return function(){ + this.$watch(expression, function(cmyk){ + element.setColor(formatter.format(cmyk)); + }); + }; +}); + +angular.widget("template", function(element) { + var srcExpression = element.attr('src'); + var self = this; + return {watch:srcExpression, apply:function(src){ + $.load(src, function(html){ + self.destroy(element); + element.html(html); + self.compile(element); + }); + }}; +}); + + +/** + * + * { + * withScope: //safely executes, with a try/catch. applies scope + * compile: + * widget: + * directive: + * validator: + * formatter: + * + * + * config: + * loadCSS: + * loadScript: + * loadTemplate: + * } + * + **/ |
