function Binder(doc, widgetFactory, datastore, location, config) { this.doc = doc; this.location = location; this.datastore = datastore; this.anchor = {}; this.widgetFactory = widgetFactory; this.config = config || {}; this.updateListeners = []; } Binder.parseBindings = function(string) { var results = []; var lastIndex = 0; var index; while((index = string.indexOf('{{', lastIndex)) > -1) { if (lastIndex < index) results.push(string.substr(lastIndex, index - lastIndex)); lastIndex = index; index = string.indexOf('}}', index); index = index < 0 ? string.length : index + 2; results.push(string.substr(lastIndex, index - lastIndex)); lastIndex = index; } if (lastIndex != string.length) results.push(string.substr(lastIndex, string.length - lastIndex)); return results.length === 0 ? [ string ] : results; }; Binder.hasBinding = function(string) { var bindings = Binder.parseBindings(string); return bindings.length > 1 || Binder.binding(bindings[0]) !== null; }; Binder.binding = function(string) { var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); return binding ? binding[1] : null; }; Binder.prototype = { parseQueryString: function(query) { var params = {}; query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, function (match, left, right) { if (left) params[decodeURIComponent(left)] = decodeURIComponent(right); }); return params; }, parseAnchor: function() { var self = this, url = this.location['get']() || ""; var anchorIndex = url.indexOf('#'); if (anchorIndex < 0) return; var anchor = url.substring(anchorIndex + 1); var anchorQuery = this.parseQueryString(anchor); foreach(self.anchor, function(newValue, key) { delete self.anchor[key]; }); foreach(anchorQuery, function(newValue, key) { self.anchor[key] = newValue; }); }, onUrlChange: function() { this.parseAnchor(); this.updateView(); }, updateAnchor: function() { var url = this.location['get']() || ""; var anchorIndex = url.indexOf('#'); if (anchorIndex > -1) url = url.substring(0, anchorIndex); url += "#"; var sep = ''; for (var key in this.anchor) { var value = this.anchor[key]; if (typeof value === 'undefined' || value === null) { delete this.anchor[key]; } else { url += sep + encodeURIComponent(key); if (value !== true) url += "=" + encodeURIComponent(value); sep = '&'; } } this.location['set'](url); return url; }, updateView: function() { var start = new Date().getTime(); var scope = jQuery(this.doc).scope(); scope.clearInvalid(); scope.updateView(); var end = new Date().getTime(); this.updateAnchor(); foreach(this.updateListeners, function(fn) {fn();}); }, docFindWithSelf: function(exp){ var doc = jQuery(this.doc); var selection = doc.find(exp); if (doc.is(exp)){ selection = selection.andSelf(); } return selection; }, executeInit: function() { this.docFindWithSelf("[ng-init]").each(function() { var jThis = jQuery(this); var scope = jThis.scope(); try { scope.eval(jThis.attr('ng-init')); } catch (e) { alert("EVAL ERROR:\n" + jThis.attr('ng-init') + '\n' + toJson(e, true)); } }); }, entity: function (scope) { var self = this; this.docFindWithSelf("[ng-entity]").attr("ng-watch", function() { try { var jNode = jQuery(this); var decl = scope.entity(jNode.attr("ng-entity"), self.datastore); return decl + (jNode.attr('ng-watch') || ""); } catch (e) { log(e); alert(e); } }); }, compile: function() { var jNode = jQuery(this.doc); if (this.config['autoSubmit']) { var submits = this.docFindWithSelf(":submit").not("[ng-action]"); submits.attr("ng-action", "$save()"); submits.not(":disabled").not("ng-bind-attr").attr("ng-bind-attr", '{disabled:"{{$invalidWidgets}}"}'); } this.precompile(this.doc)(this.doc, jNode.scope(), ""); this.docFindWithSelf("a[ng-action]").live('click', function (event) { var jNode = jQuery(this); var scope = jNode.scope(); try { scope.eval(jNode.attr('ng-action')); jNode.removeAttr('ng-error'); jNode.removeClass("ng-exception"); } catch (e) { jNode.addClass("ng-exception"); jNode.attr('ng-error', toJson(e, true)); } scope.get('$updateView')(); return false; }); }, translateBinding: function(node, parentPath, factories) { var path = parentPath.concat(); var offset = path.pop(); var parts = Binder.parseBindings(node.nodeValue); if (parts.length > 1 || Binder.binding(parts[0])) { var parent = node.parentNode; if (isLeafNode(parent)) { parent.setAttribute('ng-bind-template', node.nodeValue); factories.push({path:path, fn:function(node, scope, prefix) { return new BindUpdater(node, node.getAttribute('ng-bind-template')); }}); } else { for (var i = 0; i < parts.length; i++) { var part = parts[i]; var binding = Binder.binding(part); var newNode; if (binding) { newNode = document.createElement("span"); var jNewNode = jQuery(newNode); jNewNode.attr("ng-bind", binding); if (i === 0) { factories.push({path:path.concat(offset + i), fn:this.ng_bind}); } } else if (msie && part.charAt(0) == ' ') { newNode = document.createElement("span"); newNode.innerHTML = ' ' + part.substring(1); } else { newNode = document.createTextNode(part); } parent.insertBefore(newNode, node); } } parent.removeChild(node); } }, precompile: function(root) { var factories = []; this.precompileNode(root, [], factories); return function (template, scope, prefix) { var len = factories.length; for (var i = 0; i < len; i++) { var factory = factories[i]; var node = template; var path = factory.path; for (var j = 0; j < path.length; j++) { node = node.childNodes[path[j]]; } try { scope.addWidget(factory.fn(node, scope, prefix)); } catch (e) { alert(e); } } }; }, precompileNode: function(node, path, factories) { var nodeType = node.nodeType; if (nodeType == Node.TEXT_NODE) { this.translateBinding(node, path, factories); return; } else if (nodeType != Node.ELEMENT_NODE && nodeType != Node.DOCUMENT_NODE) { return; } if (!node.getAttribute) return; var nonBindable = node.getAttribute('ng-non-bindable'); if (nonBindable || nonBindable === "") return; var attributes = node.attributes; if (attributes) { var bindings = node.getAttribute('ng-bind-attr'); node.removeAttribute('ng-bind-attr'); bindings = bindings ? fromJson(bindings) : {}; var attrLen = attributes.length; for (var i = 0; i < attrLen; i++) { var attr = attributes[i]; var attrName = attr.name; // http://www.glennjones.net/Post/809/getAttributehrefbug.htm var attrValue = msie && attrName == 'href' ? decodeURI(node.getAttribute(attrName, 2)) : attr.value; if (Binder.hasBinding(attrValue)) { bindings[attrName] = attrValue; } } var json = toJson(bindings); if (json.length > 2) { node.setAttribute("ng-bind-attr", json); } } if (!node.getAttribute) log(node); var repeaterExpression = node.getAttribute('ng-repeat'); if (repeaterExpression) { node.removeAttribute('ng-repeat'); var precompiled = this.precompile(node); var view = document.createComment("ng-repeat: " + repeaterExpression); var parentNode = node.parentNode; parentNode.insertBefore(view, node); parentNode.removeChild(node); function template(childScope, prefix, i) { var clone = jQuery(node).clone(); clone.css('display', ''); clone.attr('ng-repeat-index', "" + i); clone.data('scope', childScope); precompiled(clone[0], childScope, prefix + i + ":"); return clone; } factories.push({path:path, fn:function(node, scope, prefix) { return new RepeaterUpdater(jQuery(node), repeaterExpression, template, prefix); }}); return; } if (node.getAttribute('ng-eval')) factories.push({path:path, fn:this.ng_eval}); if (node.getAttribute('ng-bind')) factories.push({path:path, fn:this.ng_bind}); if (node.getAttribute('ng-bind-attr')) factories.push({path:path, fn:this.ng_bind_attr}); if (node.getAttribute('ng-hide')) factories.push({path:path, fn:this.ng_hide}); if (node.getAttribute('ng-show')) factories.push({path:path, fn:this.ng_show}); if (node.getAttribute('ng-class')) factories.push({path:path, fn:this.ng_class}); if (node.getAttribute('ng-class-odd')) factories.push({path:path, fn:this.ng_class_odd}); if (node.getAttribute('ng-class-even')) factories.push({path:path, fn:this.ng_class_even}); if (node.getAttribute('ng-style')) factories.push({path:path, fn:this.ng_style}); if (node.getAttribute('ng-watch')) factories.push({path:path, fn:this.ng_watch}); var nodeName = node.nodeName; if ((nodeName == 'INPUT' ) || nodeName == 'TEXTAREA' || nodeName == 'SELECT' || nodeName == 'BUTTON') { var self = this; factories.push({path:path, fn:function(node, scope, prefix) { node.name = prefix + node.name.split(":").pop(); return self.widgetFactory.createController(jQuery(node), scope); }}); } if (nodeName == 'OPTION') { var html = jQuery('