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('').append(jQuery(node).clone()).html();
if (!html.match(/