aboutsummaryrefslogtreecommitdiffstats
path: root/src/delete
diff options
context:
space:
mode:
Diffstat (limited to 'src/delete')
-rw-r--r--src/delete/Binder.js356
-rw-r--r--src/delete/Scope.js407
-rw-r--r--src/delete/Widgets.js806
3 files changed, 1569 insertions, 0 deletions
diff --git a/src/delete/Binder.js b/src/delete/Binder.js
new file mode 100644
index 00000000..9fc32513
--- /dev/null
+++ b/src/delete/Binder.js
@@ -0,0 +1,356 @@
+function Binder(doc, widgetFactory, datastore, location, config) {
+ this.doc = doc;
+ this.location = location;
+ this.datastore = datastore;
+ this.anchor = {};
+ this.widgetFactory = widgetFactory;
+ this.config = config || {};
+ this.updateListeners = [];
+}
+
+Binder.parseBindings = function(string) {
+ var results = [];
+ var lastIndex = 0;
+ var index;
+ while((index = string.indexOf('{{', lastIndex)) > -1) {
+ if (lastIndex < index)
+ results.push(string.substr(lastIndex, index - lastIndex));
+ lastIndex = index;
+
+ index = string.indexOf('}}', index);
+ index = index < 0 ? string.length : index + 2;
+
+ results.push(string.substr(lastIndex, index - lastIndex));
+ lastIndex = index;
+ }
+ if (lastIndex != string.length)
+ results.push(string.substr(lastIndex, string.length - lastIndex));
+ return results.length === 0 ? [ string ] : results;
+};
+
+Binder.hasBinding = function(string) {
+ var bindings = Binder.parseBindings(string);
+ return bindings.length > 1 || Binder.binding(bindings[0]) !== null;
+};
+
+Binder.binding = function(string) {
+ var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/);
+ return binding ? binding[1] : null;
+};
+
+
+Binder.prototype = {
+ parseQueryString: function(query) {
+ var params = {};
+ query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,
+ function (match, left, right) {
+ if (left) params[decodeURIComponent(left)] = decodeURIComponent(right);
+ });
+ return params;
+ },
+
+ parseAnchor: function() {
+ var self = this, url = this.location['get']() || "";
+
+ var anchorIndex = url.indexOf('#');
+ if (anchorIndex < 0) return;
+ var anchor = url.substring(anchorIndex + 1);
+
+ var anchorQuery = this.parseQueryString(anchor);
+ foreach(self.anchor, function(newValue, key) {
+ delete self.anchor[key];
+ });
+ foreach(anchorQuery, function(newValue, key) {
+ self.anchor[key] = newValue;
+ });
+ },
+
+ onUrlChange: function() {
+ this.parseAnchor();
+ this.updateView();
+ },
+
+ updateAnchor: function() {
+ var url = this.location['get']() || "";
+ var anchorIndex = url.indexOf('#');
+ if (anchorIndex > -1)
+ url = url.substring(0, anchorIndex);
+ url += "#";
+ var sep = '';
+ for (var key in this.anchor) {
+ var value = this.anchor[key];
+ if (typeof value === 'undefined' || value === null) {
+ delete this.anchor[key];
+ } else {
+ url += sep + encodeURIComponent(key);
+ if (value !== true)
+ url += "=" + encodeURIComponent(value);
+ sep = '&';
+ }
+ }
+ this.location['set'](url);
+ return url;
+ },
+
+ updateView: function() {
+ var start = new Date().getTime();
+ var scope = jQuery(this.doc).scope();
+ scope.clearInvalid();
+ scope.updateView();
+ var end = new Date().getTime();
+ this.updateAnchor();
+ foreach(this.updateListeners, function(fn) {fn();});
+ },
+
+ docFindWithSelf: function(exp){
+ var doc = jQuery(this.doc);
+ var selection = doc.find(exp);
+ if (doc.is(exp)){
+ selection = selection.andSelf();
+ }
+ return selection;
+ },
+
+ executeInit: function() {
+ this.docFindWithSelf("[ng-init]").each(function() {
+ var jThis = jQuery(this);
+ var scope = jThis.scope();
+ try {
+ scope.eval(jThis.attr('ng-init'));
+ } catch (e) {
+ alert("EVAL ERROR:\n" + jThis.attr('ng-init') + '\n' + toJson(e, true));
+ }
+ });
+ },
+
+ entity: function (scope) {
+ var self = this;
+ this.docFindWithSelf("[ng-entity]").attr("ng-watch", function() {
+ try {
+ var jNode = jQuery(this);
+ var decl = scope.entity(jNode.attr("ng-entity"), self.datastore);
+ return decl + (jNode.attr('ng-watch') || "");
+ } catch (e) {
+ log(e);
+ alert(e);
+ }
+ });
+ },
+
+ compile: function() {
+ var jNode = jQuery(this.doc);
+ if (this.config['autoSubmit']) {
+ var submits = this.docFindWithSelf(":submit").not("[ng-action]");
+ submits.attr("ng-action", "$save()");
+ submits.not(":disabled").not("ng-bind-attr").attr("ng-bind-attr", '{disabled:"{{$invalidWidgets}}"}');
+ }
+ this.precompile(this.doc)(this.doc, jNode.scope(), "");
+ this.docFindWithSelf("a[ng-action]").live('click', function (event) {
+ var jNode = jQuery(this);
+ var scope = jNode.scope();
+ try {
+ scope.eval(jNode.attr('ng-action'));
+ jNode.removeAttr('ng-error');
+ jNode.removeClass("ng-exception");
+ } catch (e) {
+ jNode.addClass("ng-exception");
+ jNode.attr('ng-error', toJson(e, true));
+ }
+ scope.get('$updateView')();
+ return false;
+ });
+ },
+
+ translateBinding: function(node, parentPath, factories) {
+ var path = parentPath.concat();
+ var offset = path.pop();
+ var parts = Binder.parseBindings(node.nodeValue);
+ if (parts.length > 1 || Binder.binding(parts[0])) {
+ var parent = node.parentNode;
+ if (isLeafNode(parent)) {
+ parent.setAttribute('ng-bind-template', node.nodeValue);
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ return new BindUpdater(node, node.getAttribute('ng-bind-template'));
+ }});
+ } else {
+ for (var i = 0; i < parts.length; i++) {
+ var part = parts[i];
+ var binding = Binder.binding(part);
+ var newNode;
+ if (binding) {
+ newNode = document.createElement("span");
+ var jNewNode = jQuery(newNode);
+ jNewNode.attr("ng-bind", binding);
+ if (i === 0) {
+ factories.push({path:path.concat(offset + i), fn:this.ng_bind});
+ }
+ } else if (msie && part.charAt(0) == ' ') {
+ newNode = document.createElement("span");
+ newNode.innerHTML = '&nbsp;' + part.substring(1);
+ } else {
+ newNode = document.createTextNode(part);
+ }
+ parent.insertBefore(newNode, node);
+ }
+ }
+ parent.removeChild(node);
+ }
+ },
+
+ precompile: function(root) {
+ var factories = [];
+ this.precompileNode(root, [], factories);
+ return function (template, scope, prefix) {
+ var len = factories.length;
+ for (var i = 0; i < len; i++) {
+ var factory = factories[i];
+ var node = template;
+ var path = factory.path;
+ for (var j = 0; j < path.length; j++) {
+ node = node.childNodes[path[j]];
+ }
+ try {
+ scope.addWidget(factory.fn(node, scope, prefix));
+ } catch (e) {
+ alert(e);
+ }
+ }
+ };
+ },
+
+ precompileNode: function(node, path, factories) {
+ var nodeType = node.nodeType;
+ if (nodeType == Node.TEXT_NODE) {
+ this.translateBinding(node, path, factories);
+ return;
+ } else if (nodeType != Node.ELEMENT_NODE && nodeType != Node.DOCUMENT_NODE) {
+ return;
+ }
+
+ if (!node.getAttribute) return;
+ var nonBindable = node.getAttribute('ng-non-bindable');
+ if (nonBindable || nonBindable === "") return;
+
+ var attributes = node.attributes;
+ if (attributes) {
+ var bindings = node.getAttribute('ng-bind-attr');
+ node.removeAttribute('ng-bind-attr');
+ bindings = bindings ? fromJson(bindings) : {};
+ var attrLen = attributes.length;
+ for (var i = 0; i < attrLen; i++) {
+ var attr = attributes[i];
+ var attrName = attr.name;
+ // http://www.glennjones.net/Post/809/getAttributehrefbug.htm
+ var attrValue = msie && attrName == 'href' ?
+ decodeURI(node.getAttribute(attrName, 2)) : attr.value;
+ if (Binder.hasBinding(attrValue)) {
+ bindings[attrName] = attrValue;
+ }
+ }
+ var json = toJson(bindings);
+ if (json.length > 2) {
+ node.setAttribute("ng-bind-attr", json);
+ }
+ }
+
+ if (!node.getAttribute) log(node);
+ var repeaterExpression = node.getAttribute('ng-repeat');
+ if (repeaterExpression) {
+ node.removeAttribute('ng-repeat');
+ var precompiled = this.precompile(node);
+ var view = document.createComment("ng-repeat: " + repeaterExpression);
+ var parentNode = node.parentNode;
+ parentNode.insertBefore(view, node);
+ parentNode.removeChild(node);
+ function template(childScope, prefix, i) {
+ var clone = jQuery(node).clone();
+ clone.css('display', '');
+ clone.attr('ng-repeat-index', "" + i);
+ clone.data('scope', childScope);
+ precompiled(clone[0], childScope, prefix + i + ":");
+ return clone;
+ }
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ return new RepeaterUpdater(jQuery(node), repeaterExpression, template, prefix);
+ }});
+ return;
+ }
+
+ if (node.getAttribute('ng-eval')) factories.push({path:path, fn:this.ng_eval});
+ if (node.getAttribute('ng-bind')) factories.push({path:path, fn:this.ng_bind});
+ if (node.getAttribute('ng-bind-attr')) factories.push({path:path, fn:this.ng_bind_attr});
+ if (node.getAttribute('ng-hide')) factories.push({path:path, fn:this.ng_hide});
+ if (node.getAttribute('ng-show')) factories.push({path:path, fn:this.ng_show});
+ if (node.getAttribute('ng-class')) factories.push({path:path, fn:this.ng_class});
+ if (node.getAttribute('ng-class-odd')) factories.push({path:path, fn:this.ng_class_odd});
+ if (node.getAttribute('ng-class-even')) factories.push({path:path, fn:this.ng_class_even});
+ if (node.getAttribute('ng-style')) factories.push({path:path, fn:this.ng_style});
+ if (node.getAttribute('ng-watch')) factories.push({path:path, fn:this.ng_watch});
+ var nodeName = node.nodeName;
+ if ((nodeName == 'INPUT' ) ||
+ nodeName == 'TEXTAREA' ||
+ nodeName == 'SELECT' ||
+ nodeName == 'BUTTON') {
+ var self = this;
+ factories.push({path:path, fn:function(node, scope, prefix) {
+ node.name = prefix + node.name.split(":").pop();
+ return self.widgetFactory.createController(jQuery(node), scope);
+ }});
+ }
+ if (nodeName == 'OPTION') {
+ var html = jQuery('<select/>').append(jQuery(node).clone()).html();
+ if (!html.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\s*option\s*>/gi)) {
+ if (Binder.hasBinding(node.text)) {
+ jQuery(node).attr('ng-bind-attr', angular.toJson({'value':node.text}));
+ } else {
+ node.value = node.text;
+ }
+ }
+ }
+
+ var children = node.childNodes;
+ for (var k = 0; k < children.length; k++) {
+ this.precompileNode(children[k], path.concat(k), factories);
+ }
+ },
+
+ ng_eval: function(node) {
+ return new EvalUpdater(node, node.getAttribute('ng-eval'));
+ },
+
+ ng_bind: function(node) {
+ return new BindUpdater(node, "{{" + node.getAttribute('ng-bind') + "}}");
+ },
+
+ ng_bind_attr: function(node) {
+ return new BindAttrUpdater(node, fromJson(node.getAttribute('ng-bind-attr')));
+ },
+
+ ng_hide: function(node) {
+ return new HideUpdater(node, node.getAttribute('ng-hide'));
+ },
+
+ ng_show: function(node) {
+ return new ShowUpdater(node, node.getAttribute('ng-show'));
+ },
+
+ ng_class: function(node) {
+ return new ClassUpdater(node, node.getAttribute('ng-class'));
+ },
+
+ ng_class_even: function(node) {
+ return new ClassEvenUpdater(node, node.getAttribute('ng-class-even'));
+ },
+
+ ng_class_odd: function(node) {
+ return new ClassOddUpdater(node, node.getAttribute('ng-class-odd'));
+ },
+
+ ng_style: function(node) {
+ return new StyleUpdater(node, node.getAttribute('ng-style'));
+ },
+
+ ng_watch: function(node, scope) {
+ scope.watch(node.getAttribute('ng-watch'));
+ }
+};
diff --git a/src/delete/Scope.js b/src/delete/Scope.js
new file mode 100644
index 00000000..ae3f9f11
--- /dev/null
+++ b/src/delete/Scope.js
@@ -0,0 +1,407 @@
+function Scope(initialState, name) {
+ var self = this;
+ self.widgets = [];
+ self.evals = [];
+ self.watchListeners = {};
+ self.name = name;
+ initialState = initialState || {};
+ var State = function(){};
+ State.prototype = initialState;
+ self.state = new State();
+ extend(self.state, {
+ '$parent': initialState,
+ '$watch': bind(self, self.addWatchListener),
+ '$eval': bind(self, self.eval),
+ '$bind': bind(self, bind, self),
+ // change name to autoEval?
+ '$addEval': bind(self, self.addEval),
+ '$updateView': bind(self, self.updateView)
+ });
+ if (name == "ROOT") {
+ self.state['$root'] = self.state;
+ }
+};
+
+Scope.expressionCache = {};
+Scope.getter = function(instance, path) {
+ if (!path) return instance;
+ var element = path.split('.');
+ var key;
+ var lastInstance = instance;
+ var len = element.length;
+ for ( var i = 0; i < len; i++) {
+ key = element[i];
+ if (!key.match(/^[\$\w][\$\w\d]*$/))
+ throw "Expression '" + path + "' is not a valid expression for accesing variables.";
+ if (instance) {
+ lastInstance = instance;
+ instance = instance[key];
+ }
+ if (_.isUndefined(instance) && key.charAt(0) == '$') {
+ var type = angular['Global']['typeOf'](lastInstance);
+ type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
+ var fn = type ? type[[key.substring(1)]] : undefined;
+ if (fn) {
+ instance = _.bind(fn, lastInstance, lastInstance);
+ return instance;
+ }
+ }
+ }
+ if (typeof instance === 'function' && !instance['$$factory']) {
+ return bind(lastInstance, instance);
+ }
+ return instance;
+};
+
+Scope.setter = function(instance, path, value){
+ var element = path.split('.');
+ for ( var i = 0; element.length > 1; i++) {
+ var key = element.shift();
+ var newInstance = instance[key];
+ if (!newInstance) {
+ newInstance = {};
+ instance[key] = newInstance;
+ }
+ instance = newInstance;
+ }
+ instance[element.shift()] = value;
+ return value;
+};
+
+Scope.prototype = {
+ // TODO: rename to update? or eval?
+ updateView: function() {
+ var self = this;
+ this.fireWatchers();
+ foreach(this.widgets, function(widget){
+ self.evalWidget(widget, "", {}, function(){
+ this.updateView(self);
+ });
+ });
+ foreach(this.evals, bind(this, this.apply));
+ },
+
+ addWidget: function(controller) {
+ if (controller) this.widgets.push(controller);
+ },
+
+ addEval: function(fn, listener) {
+ // todo: this should take a function/string and a listener
+ // todo: this is a hack, which will need to be cleaned up.
+ var self = this,
+ listenFn = listener || noop,
+ expr = self.compile(fn);
+ this.evals.push(function(){
+ self.apply(listenFn, expr());
+ });
+ },
+
+ isProperty: function(exp) {
+ for ( var i = 0; i < exp.length; i++) {
+ var ch = exp.charAt(i);
+ if (ch!='.' && !Lexer.prototype.isIdent(ch)) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ get: function(path) {
+// log('SCOPE.get', path, Scope.getter(this.state, path));
+ return Scope.getter(this.state, path);
+ },
+
+ set: function(path, value) {
+// log('SCOPE.set', path, value);
+ var instance = this.state;
+ return Scope.setter(instance, path, value);
+ },
+
+ setEval: function(expressionText, value) {
+ this.eval(expressionText + "=" + toJson(value));
+ },
+
+ compile: function(exp) {
+ if (isFunction(exp)) return bind(this.state, exp);
+ var expFn = Scope.expressionCache[exp], self = this;
+ if (!expFn) {
+ var parser = new Parser(exp);
+ expFn = parser.statements();
+ parser.assertAllConsumed();
+ Scope.expressionCache[exp] = expFn;
+ }
+ return function(context){
+ context = context || {};
+ context.self = self.state;
+ context.scope = self;
+ return expFn.call(self, context);
+ };
+ },
+
+ eval: function(exp, context) {
+// log('Scope.eval', expressionText);
+ return this.compile(exp)(context);
+ },
+
+ //TODO: Refactor. This function needs to be an execution closure for widgets
+ // move to widgets
+ // remove expression, just have inner closure.
+ evalWidget: function(widget, expression, context, onSuccess, onFailure) {
+ try {
+ var value = this.eval(expression, context);
+ if (widget.hasError) {
+ widget.hasError = false;
+ jQuery(widget.view).
+ removeClass('ng-exception').
+ removeAttr('ng-error');
+ }
+ if (onSuccess) {
+ value = onSuccess.apply(widget, [value]);
+ }
+ return true;
+ } catch (e){
+ var jsonError = toJson(e, true);
+ error('Eval Widget Error:', jsonError);
+ widget.hasError = true;
+ jQuery(widget.view).
+ addClass('ng-exception').
+ attr('ng-error', jsonError);
+ if (onFailure) {
+ onFailure.apply(widget, [e, jsonError]);
+ }
+ return false;
+ }
+ },
+
+ validate: function(expressionText, value, element) {
+ var expression = Scope.expressionCache[expressionText];
+ if (!expression) {
+ expression = new Parser(expressionText).validator();
+ Scope.expressionCache[expressionText] = expression;
+ }
+ var self = {scope:this, self:this.state, '$element':element};
+ return expression(self)(self, value);
+ },
+
+ entity: function(entityDeclaration, datastore) {
+ var expression = new Parser(entityDeclaration).entityDeclaration();
+ return expression({scope:this, datastore:datastore});
+ },
+
+ clearInvalid: function() {
+ var invalid = this.state['$invalidWidgets'];
+ while(invalid.length > 0) {invalid.pop();}
+ },
+
+ markInvalid: function(widget) {
+ this.state['$invalidWidgets'].push(widget);
+ },
+
+ watch: function(declaration) {
+ var self = this;
+ new Parser(declaration).watch()({
+ scope:this,
+ addListener:function(watch, exp){
+ self.addWatchListener(watch, function(n,o){
+ try {
+ return exp({scope:self}, n, o);
+ } catch(e) {
+ alert(e);
+ }
+ });
+ }
+ });
+ },
+
+ addWatchListener: function(watchExpression, listener) {
+ // TODO: clean me up!
+ if (!isFunction(listener)) {
+ listener = this.compile(listener);
+ }
+ var watcher = this.watchListeners[watchExpression];
+ if (!watcher) {
+ watcher = {listeners:[], expression:watchExpression};
+ this.watchListeners[watchExpression] = watcher;
+ }
+ watcher.listeners.push(listener);
+ },
+
+ fireWatchers: function() {
+ var self = this, fired = false;
+ foreach(this.watchListeners, function(watcher) {
+ var value = self.eval(watcher.expression);
+ if (value !== watcher.lastValue) {
+ foreach(watcher.listeners, function(listener){
+ listener(value, watcher.lastValue);
+ fired = true;
+ });
+ watcher.lastValue = value;
+ }
+ });
+ return fired;
+ },
+
+ apply: function(fn) {
+ fn.apply(this.state, slice.call(arguments, 1, arguments.length));
+ }
+};
+
+//////////////////////////////
+
+function getter(instance, path) {
+ if (!path) return instance;
+ var element = path.split('.');
+ var key;
+ var lastInstance = instance;
+ var len = element.length;
+ for ( var i = 0; i < len; i++) {
+ key = element[i];
+ if (!key.match(/^[\$\w][\$\w\d]*$/))
+ throw "Expression '" + path + "' is not a valid expression for accesing variables.";
+ if (instance) {
+ lastInstance = instance;
+ instance = instance[key];
+ }
+ if (_.isUndefined(instance) && key.charAt(0) == '$') {
+ var type = angular['Global']['typeOf'](lastInstance);
+ type = angular[type.charAt(0).toUpperCase()+type.substring(1)];
+ var fn = type ? type[[key.substring(1)]] : undefined;
+ if (fn) {
+ instance = _.bind(fn, lastInstance, lastInstance);
+ return instance;
+ }
+ }
+ }
+ if (typeof instance === 'function' && !instance['$$factory']) {
+ return bind(lastInstance, instance);
+ }
+ return instance;
+};
+
+function setter(instance, path, value){
+ var element = path.split('.');
+ for ( var i = 0; element.length > 1; i++) {
+ var key = element.shift();
+ var newInstance = instance[key];
+ if (!newInstance) {
+ newInstance = {};
+ instance[key] = newInstance;
+ }
+ instance = newInstance;
+ }
+ instance[element.shift()] = value;
+ return value;
+};
+
+var compileCache = {};
+function expressionCompile(exp){
+ if (isFunction(exp)) return exp;
+ var expFn = compileCache[exp];
+ if (!expFn) {
+ var parser = new Parser(exp);
+ expFn = parser.statements();
+ parser.assertAllConsumed();
+ compileCache[exp] = expFn;
+ }
+ // return expFn
+ // TODO(remove this hack)
+ return function(){
+ return expFn({
+ scope: {
+ set: this.$set,
+ get: this.$get
+ }
+ });
+ };
+};
+
+var NON_RENDERABLE_ELEMENTS = {
+ '#text': 1, '#comment':1, 'TR':1, 'TH':1
+};
+
+function isRenderableElement(element){
+ return element && element[0] && !NON_RENDERABLE_ELEMENTS[element[0].nodeName];
+}
+
+function rethrow(e) { throw e; }
+function errorHandlerFor(element) {
+ while (!isRenderableElement(element)) {
+ element = element.parent() || jqLite(document.body);
+ }
+ return function(error) {
+ element.attr('ng-error', angular.toJson(error));
+ element.addClass('ng-exception');
+ };
+}
+
+function createScope(parent, Class) {
+ function Parent(){}
+ function API(){}
+ function Behavior(){}
+
+ var instance, behavior, api, watchList = [], evalList = [];
+
+ Class = Class || noop;
+ parent = Parent.prototype = parent || {};
+ api = API.prototype = new Parent();
+ behavior = Behavior.prototype = extend(new API(), Class.prototype);
+ instance = new Behavior();
+
+ extend(api, {
+ $parent: parent,
+ $bind: bind(instance, bind, instance),
+ $get: bind(instance, getter, instance),
+ $set: bind(instance, setter, instance),
+
+ $eval: function(exp) {
+ if (isDefined(exp)) {
+ return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length));
+ } else {
+ foreach(evalList, function(eval) {
+ instance.$tryEval(eval.fn, eval.handler);
+ });
+ foreach(watchList, function(watch) {
+ var value = instance.$tryEval(watch.watch, watch.handler);
+ if (watch.last !== value) {
+ instance.$tryEval(watch.listener, watch.handler, value, watch.last);
+ watch.last = value;
+ }
+ });
+ }
+ },
+
+ $tryEval: function (expression, exceptionHandler) {
+ try {
+ return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length));
+ } catch (e) {
+ error(e);
+ if (isFunction(exceptionHandler)) {
+ exceptionHandler(e);
+ } else if (exceptionHandler) {
+ errorHandlerFor(exceptionHandler)(e);
+ }
+ }
+ },
+
+ $watch: function(watchExp, listener, exceptionHandler) {
+ var watch = expressionCompile(watchExp);
+ watchList.push({
+ watch: watch,
+ last: watch.call(instance),
+ handler: exceptionHandler,
+ listener:expressionCompile(listener)
+ });
+ },
+
+ $onEval: function(expr, exceptionHandler){
+ evalList.push({
+ fn: expressionCompile(expr),
+ handler: exceptionHandler
+ });
+ }
+ });
+
+ Class.apply(instance, slice.call(arguments, 2, arguments.length));
+
+ return instance;
+}
diff --git a/src/delete/Widgets.js b/src/delete/Widgets.js
new file mode 100644
index 00000000..74f70f21
--- /dev/null
+++ b/src/delete/Widgets.js
@@ -0,0 +1,806 @@
+function WidgetFactory(serverUrl, database) {
+ this.nextUploadId = 0;
+ this.serverUrl = serverUrl;
+ this.database = database;
+ if (window['swfobject']) {
+ this.createSWF = window['swfobject']['createSWF'];
+ } else {
+ this.createSWF = function(){
+ alert("ERROR: swfobject not loaded!");
+ };
+ }
+};
+
+WidgetFactory.prototype = {
+ createController: function(input, scope) {
+ var controller;
+ var type = input.attr('type').toLowerCase();
+ var exp = input.attr('name');
+ if (exp) exp = exp.split(':').pop();
+ var event = "change";
+ var bubbleEvent = true;
+ var formatter = angularFormatter[input.attr('ng-format')] || angularFormatter['noop'];
+ if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') {
+ controller = new ButtonController(input[0], exp, formatter);
+ event = "click";
+ bubbleEvent = false;
+ } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') {
+ controller = new TextController(input[0], exp, formatter);
+ event = "keyup change";
+ } else if (type == 'checkbox') {
+ controller = new CheckboxController(input[0], exp, formatter);
+ event = "click";
+ } else if (type == 'radio') {
+ controller = new RadioController(input[0], exp, formatter);
+ event="click";
+ } else if (type == 'select-one') {
+ controller = new SelectController(input[0], exp, formatter);
+ } else if (type == 'select-multiple') {
+ controller = new MultiSelectController(input[0], exp, formatter);
+ } else if (type == 'file') {
+ controller = this.createFileController(input, exp, formatter);
+ } else {
+ throw 'Unknown type: ' + type;
+ }
+ input.data('controller', controller);
+ var updateView = scope.get('$updateView');
+ var action = function() {
+ if (controller.updateModel(scope)) {
+ var action = jQuery(controller.view).attr('ng-action') || "";
+ if (scope.evalWidget(controller, action)) {
+ updateView(scope);
+ }
+ }
+ return bubbleEvent;
+ };
+ jQuery(controller.view, ":input").
+ bind(event, action);
+ return controller;
+ },
+
+ createFileController: function(fileInput) {
+ var uploadId = '__uploadWidget_' + (this.nextUploadId++);
+ var view = FileController.template(uploadId);
+ fileInput.after(view);
+ var att = {
+ 'data':this.serverUrl + "/admin/ServerAPI.swf",
+ 'width':"95", 'height':"20", 'align':"top",
+ 'wmode':"transparent"};
+ var par = {
+ 'flashvars':"uploadWidgetId=" + uploadId,
+ 'allowScriptAccess':"always"};
+ var swfNode = this.createSWF(att, par, uploadId);
+ fileInput.remove();
+ var cntl = new FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database);
+ jQuery(swfNode).parent().data('controller', cntl);
+ return cntl;
+ }
+};
+/////////////////////
+// FileController
+///////////////////////
+
+function FileController(view, scopeName, uploader, databaseUrl) {
+ this.view = view;
+ this.uploader = uploader;
+ this.scopeName = scopeName;
+ this.attachmentsPath = databaseUrl + '/_attachments';
+ this.value = null;
+ this.lastValue = undefined;
+};
+
+angularCallbacks['flashEvent'] = function(id, event, args) {
+ var object = document.getElementById(id);
+ var jobject = jQuery(object);
+ var controller = jobject.parent().data("controller");
+ FileController.prototype[event].apply(controller, args);
+ _.defer(jobject.scope().get('$updateView'));
+};
+
+FileController.template = function(id) {
+ return jQuery('<span class="ng-upload-widget">' +
+ '<input type="checkbox" ng-non-bindable="true"/>' +
+ '<object id="' + id + '" />' +
+ '<a></a>' +
+ '<span/>' +
+ '</span>');
+};
+
+extend(FileController.prototype, {
+ 'cancel': noop,
+ 'complete': noop,
+ 'httpStatus': function(status) {
+ alert("httpStatus:" + this.scopeName + " status:" + status);
+ },
+ 'ioError': function() {
+ alert("ioError:" + this.scopeName);
+ },
+ 'open': function() {
+ alert("open:" + this.scopeName);
+ },
+ 'progress':noop,
+ 'securityError': function() {
+ alert("securityError:" + this.scopeName);
+ },
+ 'uploadCompleteData': function(data) {
+ var value = fromJson(data);
+ value.url = this.attachmentsPath + '/' + value.id + '/' + value.text;
+ this.view.find("input").attr('checked', true);
+ var scope = this.view.scope();
+ this.value = value;
+ this.updateModel(scope);
+ this.value = null;
+ },
+ 'select': function(name, size, type) {
+ this.name = name;
+ this.view.find("a").text(name).attr('href', name);
+ this.view.find("span").text(angular['filter']['bytes'](size));
+ this.upload();
+ },
+
+ updateModel: function(scope) {
+ var isChecked = this.view.find("input").attr('checked');
+ var value = isChecked ? this.value : null;
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.set(this.scopeName, value);
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var modelValue = scope.get(this.scopeName);
+ if (modelValue && this.value !== modelValue) {
+ this.value = modelValue;
+ this.view.find("a").
+ attr("href", this.value.url).
+ text(this.value.text);
+ this.view.find("span").text(angular['filter']['bytes'](this.value.size));
+ }
+ this.view.find("input").attr('checked', !!modelValue);
+ },
+
+ upload: function() {
+ if (this.name) {
+ this.uploader['uploadFile'](this.attachmentsPath);
+ }
+ }
+});
+
+///////////////////////
+// NullController
+///////////////////////
+function NullController(view) {this.view = view;};
+NullController.prototype = {
+ updateModel: function() { return true; },
+ updateView: noop
+};
+NullController.instance = new NullController();
+
+
+///////////////////////
+// ButtonController
+///////////////////////
+var ButtonController = NullController;
+
+///////////////////////
+// TextController
+///////////////////////
+function TextController(view, exp, formatter) {
+ this.view = view;
+ this.formatter = formatter;
+ this.exp = exp;
+ this.validator = view.getAttribute('ng-validate');
+ this.required = typeof view.attributes['ng-required'] != "undefined";
+ this.lastErrorText = null;
+ this.lastValue = undefined;
+ this.initialValue = this.formatter['parse'](view.value);
+ var widget = view.getAttribute('ng-widget');
+ if (widget === 'datepicker') {
+ jQuery(view).datepicker();
+ }
+};
+
+TextController.prototype = {
+ updateModel: function(scope) {
+ var value = this.formatter['parse'](this.view.value);
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var view = this.view;
+ var value = scope.get(this.exp);
+ if (typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ value = value ? value : '';
+ if (!_(this.lastValue).isEqual(value)) {
+ view.value = this.formatter['format'](value);
+ this.lastValue = value;
+ }
+
+ var isValidationError = false;
+ view.removeAttribute('ng-error');
+ if (this.required) {
+ isValidationError = !(value && $.trim("" + value).length > 0);
+ }
+ var errorText = isValidationError ? "Required Value" : null;
+ if (!isValidationError && this.validator && value) {
+ errorText = scope.validate(this.validator, value, view);
+ isValidationError = !!errorText;
+ }
+ if (this.lastErrorText !== errorText) {
+ this.lastErrorText = isValidationError;
+ if (errorText && isVisible(view)) {
+ view.setAttribute('ng-error', errorText);
+ scope.markInvalid(this);
+ }
+ jQuery(view).toggleClass('ng-validation-error', isValidationError);
+ }
+ }
+};
+
+///////////////////////
+// CheckboxController
+///////////////////////
+function CheckboxController(view, exp, formatter) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.formatter = formatter;
+ this.initialValue = this.formatter['parse'](view.checked ? view.value : "");
+};
+
+CheckboxController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ var value = input.checked ? input.value : '';
+ value = this.formatter['parse'](value);
+ value = this.formatter['format'](value);
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, this.formatter['parse'](value));
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.eval(this.exp);
+ if (typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ input.checked = this.formatter['parse'](input.value) == value;
+ }
+};
+
+///////////////////////
+// SelectController
+///////////////////////
+function SelectController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.initialValue = view.value;
+};
+
+SelectController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ if (input.selectedIndex < 0) {
+ scope.setEval(this.exp, null);
+ } else {
+ var value = this.view.value;
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.get(this.exp);
+ if (typeof value === 'undefined') {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ if (value !== this.lastValue) {
+ input.value = value ? value : "";
+ this.lastValue = value;
+ }
+ }
+};
+
+///////////////////////
+// MultiSelectController
+///////////////////////
+function MultiSelectController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastValue = undefined;
+ this.initialValue = this.selected();
+};
+
+MultiSelectController.prototype = {
+ selected: function () {
+ var value = [];
+ var options = this.view.options;
+ for ( var i = 0; i < options.length; i++) {
+ var option = options[i];
+ if (option.selected) {
+ value.push(option.value);
+ }
+ }
+ return value;
+ },
+
+ updateModel: function(scope) {
+ var value = this.selected();
+ // TODO: This is wrong! no caching going on here as we are always comparing arrays
+ if (this.lastValue === value) {
+ return false;
+ } else {
+ scope.setEval(this.exp, value);
+ this.lastValue = value;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var selected = scope.get(this.exp);
+ if (typeof selected === "undefined") {
+ selected = this.initialValue;
+ scope.setEval(this.exp, selected);
+ }
+ if (selected !== this.lastValue) {
+ var options = input.options;
+ for ( var i = 0; i < options.length; i++) {
+ var option = options[i];
+ option.selected = _.include(selected, option.value);
+ }
+ this.lastValue = selected;
+ }
+ }
+};
+
+///////////////////////
+// RadioController
+///////////////////////
+function RadioController(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.lastChecked = undefined;
+ this.lastValue = undefined;
+ this.inputValue = view.value;
+ this.initialValue = view.checked ? view.value : null;
+};
+
+RadioController.prototype = {
+ updateModel: function(scope) {
+ var input = this.view;
+ if (this.lastChecked) {
+ return false;
+ } else {
+ input.checked = true;
+ this.lastValue = scope.setEval(this.exp, this.inputValue);
+ this.lastChecked = true;
+ return true;
+ }
+ },
+
+ updateView: function(scope) {
+ var input = this.view;
+ var value = scope.get(this.exp);
+ if (this.initialValue && typeof value === "undefined") {
+ value = this.initialValue;
+ scope.setEval(this.exp, value);
+ }
+ if (this.lastValue != value) {
+ this.lastChecked = input.checked = this.inputValue == (''+value);
+ this.lastValue = value;
+ }
+ }
+};
+
+///////////////////////
+//ElementController
+///////////////////////
+function BindUpdater(view, exp) {
+ this.view = view;
+ this.exp = Binder.parseBindings(exp);
+ this.hasError = false;
+};
+
+BindUpdater.toText = function(obj) {
+ var e = escapeHtml;
+ switch(typeof obj) {
+ case "string":
+ case "boolean":
+ case "number":
+ return e(obj);
+ case "function":
+ return BindUpdater.toText(obj());
+ case "object":
+ if (isNode(obj)) {
+ return outerHTML(obj);
+ } else if (obj instanceof angular.filter.Meta) {
+ switch(typeof obj.html) {
+ case "string":
+ case "number":
+ return obj.html;
+ case "function":
+ return obj.html();
+ case "object":
+ if (isNode(obj.html))
+ return outerHTML(obj.html);
+ default:
+ break;
+ }
+ switch(typeof obj.text) {
+ case "string":
+ case "number":
+ return e(obj.text);
+ case "function":
+ return e(obj.text());
+ default:
+ break;
+ }
+ }
+ if (obj === null)
+ return "";
+ return e(toJson(obj, true));
+ default:
+ return "";
+ }
+};
+
+BindUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ var html = [];
+ var parts = this.exp;
+ var length = parts.length;
+ for(var i=0; i<length; i++) {
+ var part = parts[i];
+ var binding = Binder.binding(part);
+ if (binding) {
+ scope.evalWidget(this, binding, {$element:this.view}, function(value){
+ html.push(BindUpdater.toText(value));
+ }, function(e, text){
+ setHtml(this.view, text);
+ });
+ if (this.hasError) {
+ return;
+ }
+ } else {
+ html.push(escapeHtml(part));
+ }
+ }
+ setHtml(this.view, html.join(''));
+ }
+};
+
+function BindAttrUpdater(view, attrs) {
+ this.view = view;
+ this.attrs = attrs;
+};
+
+BindAttrUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ var jNode = jQuery(this.view);
+ var attributeTemplates = this.attrs;
+ if (this.hasError) {
+ this.hasError = false;
+ jNode.
+ removeClass('ng-exception').
+ removeAttr('ng-error');
+ }
+ var isImage = jNode.is('img');
+ for (var attrName in attributeTemplates) {
+ var attributeTemplate = Binder.parseBindings(attributeTemplates[attrName]);
+ var attrValues = [];
+ for ( var i = 0; i < attributeTemplate.length; i++) {
+ var binding = Binder.binding(attributeTemplate[i]);
+ if (binding) {
+ try {
+ var value = scope.eval(binding, {$element:jNode[0], attrName:attrName});
+ if (value && (value.constructor !== array || value.length !== 0))
+ attrValues.push(value);
+ } catch (e) {
+ this.hasError = true;
+ error('BindAttrUpdater', e);
+ var jsonError = toJson(e, true);
+ attrValues.push('[' + jsonError + ']');
+ jNode.
+ addClass('ng-exception').
+ attr('ng-error', jsonError);
+ }
+ } else {
+ attrValues.push(attributeTemplate[i]);
+ }
+ }
+ var attrValue = attrValues.length ? attrValues.join('') : null;
+ if(isImage && attrName == 'src' && !attrValue)
+ attrValue = scope.get('$config.blankImage');
+ jNode.attr(attrName, attrValue);
+ }
+ }
+};
+
+function EvalUpdater(view, exp) {
+ this.view = view;
+ this.exp = exp;
+ this.hasError = false;
+};
+EvalUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp);
+ }
+};
+
+function HideUpdater(view, exp) { this.view = view; this.exp = exp; };
+HideUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(hideValue){
+ var view = jQuery(this.view);
+ if (toBoolean(hideValue)) {
+ view.hide();
+ } else {
+ view.show();
+ }
+ });
+ }
+};
+
+function ShowUpdater(view, exp) { this.view = view; this.exp = exp; };
+ShowUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(hideValue){
+ var view = jQuery(this.view);
+ if (toBoolean(hideValue)) {
+ view.show();
+ } else {
+ view.hide();
+ }
+ });
+ }
+};
+
+function ClassUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ if (classValue !== null && classValue !== undefined) {
+ this.view.className = classValue;
+ }
+ });
+ }
+};
+
+function ClassEvenUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassEvenUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ var index = scope.get('$index');
+ jQuery(this.view).toggleClass(classValue, index % 2 === 1);
+ });
+ }
+};
+
+function ClassOddUpdater(view, exp) { this.view = view; this.exp = exp; };
+ClassOddUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(classValue){
+ var index = scope.get('$index');
+ jQuery(this.view).toggleClass(classValue, index % 2 === 0);
+ });
+ }
+};
+
+function StyleUpdater(view, exp) { this.view = view; this.exp = exp; };
+StyleUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.exp, {}, function(styleValue){
+ jQuery(this.view).attr('style', "").css(styleValue);
+ });
+ }
+};
+
+///////////////////////
+// RepeaterUpdater
+///////////////////////
+function RepeaterUpdater(view, repeaterExpression, template, prefix) {
+ this.view = view;
+ this.template = template;
+ this.prefix = prefix;
+ this.children = [];
+ var match = repeaterExpression.match(/^\s*(.+)\s+in\s+(.*)\s*$/);
+ if (! match) {
+ throw "Expected ng-repeat in form of 'item in collection' but got '" +
+ repeaterExpression + "'.";
+ }
+ var keyValue = match[1];
+ this.iteratorExp = match[2];
+ match = keyValue.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/);
+ if (!match) {
+ throw "'item' in 'item in collection' should be identifier or (key, value) but get '" +
+ keyValue + "'.";
+ }
+ this.valueExp = match[3] || match[1];
+ this.keyExp = match[2];
+};
+
+RepeaterUpdater.prototype = {
+ updateModel: noop,
+ updateView: function(scope) {
+ scope.evalWidget(this, this.iteratorExp, {}, function(iterator){
+ var self = this;
+ if (!iterator) {
+ iterator = [];
+ if (scope.isProperty(this.iteratorExp)) {
+ scope.set(this.iteratorExp, iterator);
+ }
+ }
+ var childrenLength = this.children.length;
+ var cursor = this.view;
+ var time = 0;
+ var child = null;
+ var keyExp = this.keyExp;
+ var valueExp = this.valueExp;
+ var iteratorCounter = 0;
+ foreach(iterator, function(value, key){
+ if (iteratorCounter < childrenLength) {
+ // reuse children
+ child = self.children[iteratorCounter];
+ child.scope.set(valueExp, value);
+ } else {
+ // grow children
+ var name = self.prefix +
+ valueExp + " in " + self.iteratorExp + "[" + iteratorCounter + "]";
+ var childScope = new Scope(scope.state, name);
+ childScope.set('$index', iteratorCounter);
+ if (keyExp)
+ childScope.set(keyExp, key);
+ childScope.set(valueExp, value);
+ child = { scope:childScope, element:self.template(childScope, self.prefix, iteratorCounter) };
+ cursor.after(child.element);
+ self.children.push(child);
+ }
+ cursor = child.element;
+ var s = new Date().getTime();
+ child.scope.updateView();
+ time += new Date().getTime() - s;
+ iteratorCounter++;
+ });
+ // shrink children
+ for ( var r = childrenLength; r > iteratorCounter; --r) {
+ this.children.pop().element.remove();
+ }
+ // Special case for option in select
+ if (child && child.element[0].nodeName === "OPTION") {
+ var select = jQuery(child.element[0].parentNode);
+ var cntl = select.data('controller');
+ if (cntl) {
+ cntl.lastValue = undefined;
+ cntl.updateView(scope);
+ }
+ }
+ });
+ }
+};
+
+//////////////////////////////////
+// PopUp
+//////////////////////////////////
+
+function PopUp(doc) {
+ this.doc = doc;
+};
+
+PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup";
+
+PopUp.onOver = function(e) {
+ PopUp.onOut();
+ var jNode = jQuery(this);
+ jNode.bind(PopUp.OUT_EVENT, PopUp.onOut);
+ var position = jNode.position();
+ var de = document.documentElement;
+ var w = self.innerWidth || (de&&de.clientWidth) || document.body.clientWidth;
+ var hasArea = w - position.left;
+ var width = 300;
+ var title = jNode.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error...";
+ var msg = jNode.attr("ng-error");
+
+ var x;
+ var arrowPos = hasArea>(width+75) ? "left" : "right";
+ var tip = jQuery(
+ "<div id='ng-callout' style='width:"+width+"px'>" +
+ "<div class='ng-arrow-"+arrowPos+"'/>" +
+ "<div class='ng-title'>"+title+"</div>" +
+ "<div class='ng-content'>"+msg+"</div>" +
+ "</div>");
+ jQuery("body").append(tip);
+ if(arrowPos === 'left'){
+ x = position.left + this.offsetWidth + 11;
+ }else{
+ x = position.left - (width + 15);
+ tip.find('.ng-arrow-right').css({left:width+1});
+ }
+
+ tip.css({left: x+"px", top: (position.top - 3)+"px"});
+ return true;
+};
+
+PopUp.onOut = function() {
+ jQuery('#ng-callout').
+ unbind(PopUp.OUT_EVENT, PopUp.onOut).
+ remove();
+ return true;
+};
+
+PopUp.prototype = {
+ bind: function () {
+ var self = this;
+ this.doc.find('.ng-validation-error,.ng-exception').
+ live("mouseover", PopUp.onOver);
+ }
+};
+
+//////////////////////////////////
+// Status
+//////////////////////////////////
+
+function NullStatus(body) {
+};
+
+NullStatus.prototype = {
+ beginRequest:function(){},
+ endRequest:function(){}
+};
+
+function Status(body) {
+ this.requestCount = 0;
+ this.body = body;
+};
+
+Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>';
+
+Status.prototype = {
+ beginRequest: function () {
+ if (this.requestCount === 0) {
+ (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show();
+ }
+ this.requestCount++;
+ },
+
+ endRequest: function () {
+ this.requestCount--;
+ if (this.requestCount === 0) {
+ this.loader.hide("fold");
+ }
+ }
+};