From 258ca5f16581f0e8befa493644225a02ae2fc002 Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 26 Mar 2010 16:27:18 -0700 Subject: moved all uneeded files out, widgets.html works, tests horribly broken --- src/API.js | 325 ------------- src/Angular.js | 234 +--------- src/Binder.js | 356 -------------- src/Compiler.js | 11 +- src/ControlBar.js | 72 --- src/DataStore.js | 330 ------------- src/Formatters.js | 11 +- src/JSON.js | 4 +- src/Model.js | 65 --- src/Scope.js | 278 +---------- src/Server.js | 68 --- src/UrlWatcher.js | 62 +++ src/Users.js | 35 -- src/Widgets.js | 927 ++++++------------------------------- src/angular-bootstrap.js | 61 ++- src/apis.js | 328 +++++++++++++ src/delete/Binder.js | 356 ++++++++++++++ src/delete/Scope.js | 407 ++++++++++++++++ src/delete/Widgets.js | 806 ++++++++++++++++++++++++++++++++ src/directives.js | 15 +- src/jqLite.js | 16 +- src/markup.js | 76 --- src/markups.js | 76 +++ src/moveToAngularCom/ControlBar.js | 72 +++ src/moveToAngularCom/DataStore.js | 330 +++++++++++++ src/moveToAngularCom/Model.js | 65 +++ src/moveToAngularCom/Server.js | 68 +++ src/moveToAngularCom/Users.js | 35 ++ src/widgets2.js | 129 ------ 29 files changed, 2853 insertions(+), 2765 deletions(-) delete mode 100644 src/API.js delete mode 100644 src/Binder.js delete mode 100644 src/ControlBar.js delete mode 100644 src/DataStore.js delete mode 100644 src/Model.js delete mode 100644 src/Server.js create mode 100644 src/UrlWatcher.js delete mode 100644 src/Users.js create mode 100644 src/apis.js create mode 100644 src/delete/Binder.js create mode 100644 src/delete/Scope.js create mode 100644 src/delete/Widgets.js delete mode 100644 src/markup.js create mode 100644 src/markups.js create mode 100644 src/moveToAngularCom/ControlBar.js create mode 100644 src/moveToAngularCom/DataStore.js create mode 100644 src/moveToAngularCom/Model.js create mode 100644 src/moveToAngularCom/Server.js create mode 100644 src/moveToAngularCom/Users.js delete mode 100644 src/widgets2.js (limited to 'src') diff --git a/src/API.js b/src/API.js deleted file mode 100644 index ce690ad1..00000000 --- a/src/API.js +++ /dev/null @@ -1,325 +0,0 @@ -var angularGlobal = { - 'typeOf':function(obj){ - if (obj === null) return "null"; - var type = typeof obj; - if (type == "object") { - if (obj instanceof Array) return "array"; - if (obj instanceof Date) return "date"; - if (obj.nodeType == 1) return "element"; - } - return type; - } -}; - -var angularCollection = {}; -var angularObject = {}; -var angularArray = { - 'includeIf':function(array, value, condition) { - var index = _.indexOf(array, value); - if (condition) { - if (index == -1) - array.push(value); - } else { - array.splice(index, 1); - } - return array; - }, - 'sum':function(array, expression) { - var fn = angular['Function']['compile'](expression); - var sum = 0; - for (var i = 0; i < array.length; i++) { - var value = 1 * fn(array[i]); - if (!isNaN(value)){ - sum += value; - } - } - return sum; - }, - 'remove':function(array, value) { - var index = _.indexOf(array, value); - if (index >=0) - array.splice(index, 1); - return value; - }, - 'find':function(array, condition, defaultValue) { - if (!condition) return undefined; - var fn = angular['Function']['compile'](condition); - _.detect(array, function($){ - if (fn($)){ - defaultValue = $; - return true; - } - }); - return defaultValue; - }, - 'findById':function(array, id) { - return angular.Array.find(array, function($){return $.$id == id;}, null); - }, - 'filter':function(array, expression) { - var predicates = []; - predicates.check = function(value) { - for (var j = 0; j < predicates.length; j++) { - if(!predicates[j](value)) { - return false; - } - } - return true; - }; - var getter = Scope.getter; - var search = function(obj, text){ - if (text.charAt(0) === '!') { - return !search(obj, text.substr(1)); - } - switch (typeof obj) { - case "boolean": - case "number": - case "string": - return ('' + obj).toLowerCase().indexOf(text) > -1; - case "object": - for ( var objKey in obj) { - if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { - return true; - } - } - return false; - case "array": - for ( var i = 0; i < obj.length; i++) { - if (search(obj[i], text)) { - return true; - } - } - return false; - default: - return false; - } - }; - switch (typeof expression) { - case "boolean": - case "number": - case "string": - expression = {$:expression}; - case "object": - for (var key in expression) { - if (key == '$') { - (function(){ - var text = (''+expression[key]).toLowerCase(); - if (!text) return; - predicates.push(function(value) { - return search(value, text); - }); - })(); - } else { - (function(){ - var path = key; - var text = (''+expression[key]).toLowerCase(); - if (!text) return; - predicates.push(function(value) { - return search(getter(value, path), text); - }); - })(); - } - } - break; - case "function": - predicates.push(expression); - break; - default: - return array; - } - var filtered = []; - for ( var j = 0; j < array.length; j++) { - var value = array[j]; - if (predicates.check(value)) { - filtered.push(value); - } - } - return filtered; - }, - 'add':function(array, value) { - array.push(_.isUndefined(value)? {} : value); - return array; - }, - 'count':function(array, condition) { - if (!condition) return array.length; - var fn = angular['Function']['compile'](condition); - return _.reduce(array, 0, function(count, $){return count + (fn($)?1:0);}); - }, - 'orderBy':function(array, expression, descend) { - function reverse(comp, descending) { - return toBoolean(descending) ? - function(a,b){return comp(b,a);} : comp; - } - function compare(v1, v2){ - var t1 = typeof v1; - var t2 = typeof v2; - if (t1 == t2) { - if (t1 == "string") v1 = v1.toLowerCase(); - if (t1 == "string") v2 = v2.toLowerCase(); - if (v1 === v2) return 0; - return v1 < v2 ? -1 : 1; - } else { - return t1 < t2 ? -1 : 1; - } - } - expression = _.isArray(expression) ? expression: [expression]; - expression = _.map(expression, function($){ - var descending = false; - if (typeof $ == "string" && ($.charAt(0) == '+' || $.charAt(0) == '-')) { - descending = $.charAt(0) == '-'; - $ = $.substring(1); - } - var get = $ ? angular['Function']['compile']($) : _.identity; - return reverse(function(a,b){ - return compare(get(a),get(b)); - }, descending); - }); - var comparator = function(o1, o2){ - for ( var i = 0; i < expression.length; i++) { - var comp = expression[i](o1, o2); - if (comp !== 0) return comp; - } - return 0; - }; - return _.clone(array).sort(reverse(comparator, descend)); - }, - 'orderByToggle':function(predicate, attribute) { - var STRIP = /^([+|-])?(.*)/; - var ascending = false; - var index = -1; - _.detect(predicate, function($, i){ - if ($ == attribute) { - ascending = true; - index = i; - return true; - } - if (($.charAt(0)=='+'||$.charAt(0)=='-') && $.substring(1) == attribute) { - ascending = $.charAt(0) == '+'; - index = i; - return true; - } - }); - if (index >= 0) { - predicate.splice(index, 1); - } - predicate.unshift((ascending ? "-" : "+") + attribute); - return predicate; - }, - 'orderByDirection':function(predicate, attribute, ascend, descend) { - ascend = ascend || 'ng-ascend'; - descend = descend || 'ng-descend'; - var att = predicate[0] || ''; - var direction = true; - if (att.charAt(0) == '-') { - att = att.substring(1); - direction = false; - } else if(att.charAt(0) == '+') { - att = att.substring(1); - } - return att == attribute ? (direction ? ascend : descend) : ""; - }, - 'merge':function(array, index, mergeValue) { - var value = array[index]; - if (!value) { - value = {}; - array[index] = value; - } - merge(mergeValue, value); - return array; - } -}; - -var angularString = { - 'quote':function(string) { - return '"' + string.replace(/\\/g, '\\\\'). - replace(/"/g, '\\"'). - replace(/\n/g, '\\n'). - replace(/\f/g, '\\f'). - replace(/\r/g, '\\r'). - replace(/\t/g, '\\t'). - replace(/\v/g, '\\v') + - '"'; - }, - 'quoteUnicode':function(string) { - var str = angular['String']['quote'](string); - var chars = []; - for ( var i = 0; i < str.length; i++) { - var ch = str.charCodeAt(i); - if (ch < 128) { - chars.push(str.charAt(i)); - } else { - var encode = "000" + ch.toString(16); - chars.push("\\u" + encode.substring(encode.length - 4)); - } - } - return chars.join(''); - }, - 'toDate':function(string){ - var match; - if (typeof string == 'string' && - (match = string.match(/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/))){ - var date = new Date(0); - date.setUTCFullYear(match[1], match[2] - 1, match[3]); - date.setUTCHours(match[4], match[5], match[6], 0); - return date; - } - return string; - } -}; - -var angularDate = { - 'toString':function(date){ - function pad(n) { return n < 10 ? "0" + n : n; } - return (date.getUTCFullYear()) + '-' + - pad(date.getUTCMonth() + 1) + '-' + - pad(date.getUTCDate()) + 'T' + - pad(date.getUTCHours()) + ':' + - pad(date.getUTCMinutes()) + ':' + - pad(date.getUTCSeconds()) + 'Z'; - } - }; - -var angularFunction = { - 'compile':function(expression) { - if (_.isFunction(expression)){ - return expression; - } else if (expression){ - var scope = new Scope(); - return function($) { - scope.state = $; - return scope.eval(expression); - }; - } else { - return function($){return $;}; - } - } -}; - -function defineApi(dst, chain, underscoreNames){ - var lastChain = _.last(chain); - foreach(underscoreNames, function(name){ - lastChain[name] = _[name]; - }); - angular[dst] = angular[dst] || {}; - foreach(chain, function(parent){ - extend(angular[dst], parent); - }); -} -defineApi('Global', [angularGlobal], - ['extend', 'clone','isEqual', - 'isElement', 'isArray', 'isFunction', 'isUndefined']); -defineApi('Collection', [angularGlobal, angularCollection], - ['each', 'map', 'reduce', 'reduceRight', 'detect', - 'select', 'reject', 'all', 'any', 'include', - 'invoke', 'pluck', 'max', 'min', 'sortBy', - 'sortedIndex', 'toArray', 'size']); -defineApi('Array', [angularGlobal, angularCollection, angularArray], - ['first', 'last', 'compact', 'flatten', 'without', - 'uniq', 'intersect', 'zip', 'indexOf', 'lastIndexOf']); -defineApi('Object', [angularGlobal, angularCollection, angularObject], - ['keys', 'values']); -defineApi('String', [angularGlobal, angularString], []); -defineApi('Date', [angularGlobal, angularDate], []); -//IE bug -angular['Date']['toString'] = angularDate['toString']; -defineApi('Function', [angularGlobal, angularCollection, angularFunction], - ['bind', 'bindAll', 'delay', 'defer', 'wrap', 'compose']); diff --git a/src/Angular.js b/src/Angular.js index 0cb89bbe..c3562e84 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -1,22 +1,5 @@ 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, - TEXT_NODE : 3, - CDATA_SECTION_NODE : 4, - ENTITY_REFERENCE_NODE : 5, - ENTITY_NODE : 6, - PROCESSING_INSTRUCTION_NODE : 7, - COMMENT_NODE : 8, - DOCUMENT_NODE : 9, - DOCUMENT_TYPE_NODE : 10, - DOCUMENT_FRAGMENT_NODE : 11, - NOTATION_NODE : 12 - }; -} function noop() {} function identity($) {return $;} @@ -32,9 +15,11 @@ function extensionMap(angular, name) { }); } -var consoleNode, msie, +var consoleNode, NOOP = 'noop', jQuery = window['jQuery'] || window['$'], // weirdness to make IE happy + _ = window['_'], + jqLite = jQuery, slice = Array.prototype.slice, angular = window['angular'] || (window['angular'] = {}), angularTextMarkup = extensionMap(angular, 'textMarkup'), @@ -77,6 +62,7 @@ function extend(dst, obj) { return dst; } +function isUndefined(value){ return typeof value == 'undefined'; } function isDefined(value){ return typeof value != 'undefined'; } function isObject(value){ return typeof value == 'object';} function isString(value){ return typeof value == 'string';} @@ -85,6 +71,12 @@ function isFunction(value){ return typeof value == 'function';} function lowercase(value){ return isString(value) ? value.toLowerCase() : value; } function uppercase(value){ return isString(value) ? value.toUpperCase() : value; } function trim(value) { return isString(value) ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; }; +function includes(array, obj) { + for ( var i = 0; i < array.length; i++) { + if (obj === array[i]) return true; + } + return false; +} function log(a, b, c){ var console = window['console']; @@ -154,18 +146,18 @@ function copy(source, destination){ if (!destination) { if (!source) { return source; - } else if (_.isArray(source)) { + } else if (isArray(source)) { return copy(source, []); } else { return copy(source, {}); } } else { - if (_.isArray(source)) { + if (isArray(source)) { while(destination.length) { destination.pop(); } } else { - _(destination).each(function(value, key){ + foreach(function(value, key){ delete destination[key]; }); } @@ -236,201 +228,19 @@ function merge(src, dst) { } } -// //////////////////////////// -// UrlWatcher -// //////////////////////////// - -function UrlWatcher(location) { - this.location = location; - this.delay = 25; - this.setTimeout = function(fn, delay) { - window.setTimeout(fn, delay); - }; - this.listener = function(url) { - return url; - }; - this.expectedUrl = location.href; -} - -UrlWatcher.prototype = { - listen: function(fn){ - this.listener = fn; - }, - watch: function() { - var self = this; - var pull = function() { - if (self.expectedUrl !== self.location.href) { - var notify = self.location.hash.match(/^#\$iframe_notify=(.*)$/); - if (notify) { - if (!self.expectedUrl.match(/#/)) { - self.expectedUrl += "#"; - } - self.location.href = self.expectedUrl; - var id = '_iframe_notify_' + notify[1]; - var notifyFn = angularCallbacks[id]; - delete angularCallbacks[id]; - try { - (notifyFn||noop)(); - } catch (e) { - alert(e); - } - } else { - self.listener(self.location.href); - self.expectedUrl = self.location.href; - } - } - self.setTimeout(pull, self.delay); - }; - pull(); - }, - - set: function(url) { - var existingURL = this.location.href; - if (!existingURL.match(/#/)) - existingURL += '#'; - if (existingURL != url) - this.location.href = url; - this.existingURL = url; - }, - - get: function() { - return window.location.href; - } -}; - ///////////////////////////////////////////////// -function configureJQueryPlugins() { - var fn = jQuery['fn']; - fn['scope'] = function() { - var element = this; - while (element && element.get(0)) { - var scope = element.data("scope"); - if (scope) - return scope; - element = element.parent(); - } - return null; - }; - fn['controller'] = function() { - return this.data('controller') || NullController.instance; - }; -} - -function configureLogging(config) { - if (config.debug == 'console' && !consoleNode) { - consoleNode = document.createElement("div"); - consoleNode.id = 'ng-console'; - document.getElementsByTagName('body')[0].appendChild(consoleNode); - log = function() { - consoleLog('ng-console-info', arguments); - }; - console.error = function() { - consoleLog('ng-console-error', arguments); - }; - } -} - -function exposeMethods(obj, methods){ - var bound = {}; - foreach(methods, function(fn, name){ - bound[name] = _(fn).bind(obj); - }); - return bound; -} - -function wireAngular(element, config) { - var widgetFactory = new WidgetFactory(config['server'], config['database']); - var binder = new Binder(element[0], widgetFactory, datastore, config['location'], config); - binder.updateListeners.push(config.onUpdateView); - var controlBar = new ControlBar(element.find('body'), config['server'], config['database']); - var onUpdate = function(){binder.updateView();}; - var server = config['database'] =="$MEMORY" ? - new FrameServer(window) : - new Server(config['server'], jQuery['getScript']); - server = new VisualServer(server, new NullStatus(element.find('body')), onUpdate); - var users = new Users(server, controlBar); - var databasePath = '/data/' + config['database']; - var post = function(request, callback){ - server.request("POST", databasePath, request, callback); - }; - var datastore = new DataStore(post, users, binder.anchor); - binder.datastore = datastore; - binder.updateListeners.push(function(){datastore.flush();}); - var scope = new Scope({ - '$anchor' : binder.anchor, - '$updateView': _(binder.updateView).bind(binder), - '$config' : config, - '$invalidWidgets': [], - '$console' : window.console, - '$datastore' : exposeMethods(datastore, { - 'load': datastore.load, - 'loadMany': datastore.loadMany, - 'loadOrCreate': datastore.loadOrCreate, - 'loadAll': datastore.loadAll, - 'save': datastore.save, - 'remove': datastore.remove, - 'flush': datastore.flush, - 'query': datastore.query, - 'entity': datastore.entity, - 'entities': datastore.entities, - 'documentCountsByUser': datastore.documentCountsByUser, - 'userDocumentIdsByEntity': datastore.userDocumentIdsByEntity, - 'join': datastore.join - }), - '$save' : function(callback) { - datastore.saveScope(scope.state, callback, binder.anchor); - }, - '$window' : window, - '$uid' : function() { - return "" + new Date().getTime(); - }, - '$users' : users - }, "ROOT"); - - element.data('scope', scope); - binder.entity(scope); - binder.compile(); - controlBar.bind(); - - //TODO: remove this code - new PopUp(element).bind(); - - var self = _(exposeMethods(scope, { - 'set': scope.set, - 'get': scope.get, - 'eval': scope.eval - })).extend({ - 'init':function(){ - config['location']['listen'](_(binder.onUrlChange).bind(binder)); - binder.parseAnchor(); - binder.executeInit(); - binder.updateView(); - return self; - }, - 'element':element[0], - 'updateView': _(binder.updateView).bind(binder), - 'config':config - }); - return self; -} - -angular['startUrlWatcher'] = function(){ - var watcher = new UrlWatcher(window['location']); - watcher.watch(); - return exposeMethods(watcher, {'listen':watcher.listen, 'set':watcher.set, 'get':watcher.get}); -}; angular['compile'] = function(element, config) { - jQuery = window['jQuery']; - msie = jQuery['browser']['msie']; - config = _({ + config = extend({ 'onUpdateView': noop, 'server': "", 'location': {'get':noop, 'set':noop, 'listen':noop} - }).extend(config||{}); - - configureLogging(config); - configureJQueryPlugins(); - - return wireAngular(jQuery(element), config); + }, config||{}); + + var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget); + $element = jqLite(element), + rootScope = { + '$window': window + }; + return rootScope['$root'] = compiler.compile($element)($element, rootScope); }; diff --git a/src/Binder.js b/src/Binder.js deleted file mode 100644 index 9fc32513..00000000 --- a/src/Binder.js +++ /dev/null @@ -1,356 +0,0 @@ -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(/.*<\/\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/Compiler.js b/src/Compiler.js index 3b492ebe..4f30521b 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -51,7 +51,7 @@ Template.prototype = { //Compiler ////////////////////////////////// function isTextNode(node) { - return node.nodeType == Node.TEXT_NODE; + return node.nodeName == '#text'; } function eachTextNode(element, fn){ @@ -92,10 +92,13 @@ Compiler.prototype = { rawElement = jqLite(rawElement); var template = this.templatize(rawElement) || new Template(); return function(element, parentScope){ - var model = scope(parentScope); - return extend(model, { + var scope = createScope(parentScope); + return extend(scope, { $element:element, - $init: bind(template, template.init, element, model) + $init: function() { + template.init(element, scope); + scope.$eval(); + } }); }; }, diff --git a/src/ControlBar.js b/src/ControlBar.js deleted file mode 100644 index 685beeb2..00000000 --- a/src/ControlBar.js +++ /dev/null @@ -1,72 +0,0 @@ -function ControlBar(document, serverUrl, database) { - this._document = document; - this.serverUrl = serverUrl; - this.database = database; - this._window = window; - this.callbacks = []; -}; - -ControlBar.HTML = - '' + - '' + - '' + - '' + - '' + - ''; - - -ControlBar.FORBIDEN = - '' + - 'Sorry, you do not have permission for this!'+ - ''; - -ControlBar.prototype = { - bind: function () { - }, - - login: function (loginSubmitFn) { - this.callbacks.push(loginSubmitFn); - if (this.callbacks.length == 1) { - this.doTemplate("/user_session/new.mini?database="+encodeURIComponent(this.database)+"&return_url=" + encodeURIComponent(this.urlWithoutAnchor())); - } - }, - - logout: function (loginSubmitFn) { - this.callbacks.push(loginSubmitFn); - if (this.callbacks.length == 1) { - this.doTemplate("/user_session/do_destroy.mini"); - } - }, - - urlWithoutAnchor: function (path) { - return this._window['location']['href'].split("#")[0]; - }, - - doTemplate: function (path) { - var self = this; - var id = new Date().getTime(); - var url = this.urlWithoutAnchor() + "#$iframe_notify=" + id; - var iframeHeight = 330; - var loginView = jQuery(''); - this._document.append(loginView); - loginView['dialog']({ - 'height':iframeHeight + 33, 'width':500, - 'resizable': false, 'modal':true, - 'title': 'Authentication: <angular/>' - }); - angularCallbacks["_iframe_notify_" + id] = function() { - loginView['dialog']("destroy"); - loginView['remove'](); - foreach(self.callbacks, function(callback){ - callback(); - }); - self.callbacks = []; - }; - }, - - notAuthorized: function () { - if (this.forbidenView) return; - this.forbidenView = jQuery(ControlBar.FORBIDEN); - this.forbidenView.dialog({bgiframe:true, height:70, modal:true}); - } -}; \ No newline at end of file diff --git a/src/DataStore.js b/src/DataStore.js deleted file mode 100644 index 70bcc623..00000000 --- a/src/DataStore.js +++ /dev/null @@ -1,330 +0,0 @@ -function DataStore(post, users, anchor) { - this.post = post; - this.users = users; - this._cache_collections = []; - this._cache = {'$collections':this._cache_collections}; - this.anchor = anchor; - this.bulkRequest = []; -}; - -DataStore.NullEntity = extend(function(){}, { - 'all': function(){return [];}, - 'query': function(){return [];}, - 'load': function(){return {};}, - 'title': undefined -}); - -DataStore.prototype = { - cache: function(document) { - if (! document.datastore === this) { - throw "Parameter must be an instance of Entity! " + toJson(document); - } - var key = document['$entity'] + '/' + document['$id']; - var cachedDocument = this._cache[key]; - if (cachedDocument) { - Model.copyDirectFields(document, cachedDocument); - } else { - this._cache[key] = document; - cachedDocument = document; - } - return cachedDocument; - }, - - load: function(instance, id, callback, failure) { - if (id && id !== '*') { - var self = this; - this._jsonRequest(["GET", instance['$entity'] + "/" + id], function(response) { - instance['$loadFrom'](response); - instance['$migrate'](); - var clone = instance['$$entity'](instance); - self.cache(clone); - (callback||noop)(instance); - }, failure); - } - return instance; - }, - - loadMany: function(entity, ids, callback) { - var self=this; - var list = []; - var callbackCount = 0; - foreach(ids, function(id){ - list.push(self.load(entity(), id, function(){ - callbackCount++; - if (callbackCount == ids.length) { - (callback||noop)(list); - } - })); - }); - return list; - }, - - loadOrCreate: function(instance, id, callback) { - var self=this; - return this.load(instance, id, callback, function(response){ - if (response['$status_code'] == 404) { - instance['$id'] = id; - (callback||noop)(instance); - } else { - throw response; - } - }); - }, - - loadAll: function(entity, callback) { - var self = this; - var list = []; - list['$$accept'] = function(doc){ - return doc['$entity'] == entity['title']; - }; - this._cache_collections.push(list); - this._jsonRequest(["GET", entity['title']], function(response) { - var rows = response; - for ( var i = 0; i < rows.length; i++) { - var document = entity(); - document['$loadFrom'](rows[i]); - list.push(self.cache(document)); - } - (callback||noop)(list); - }); - return list; - }, - - save: function(document, callback) { - var self = this; - var data = {}; - document['$saveTo'](data); - this._jsonRequest(["POST", "", data], function(response) { - document['$loadFrom'](response); - var cachedDoc = self.cache(document); - _.each(self._cache_collections, function(collection){ - if (collection['$$accept'](document)) { - angularArray['includeIf'](collection, cachedDoc, true); - } - }); - if (document['$$anchor']) { - self.anchor[document['$$anchor']] = document['$id']; - } - if (callback) - callback(document); - }); - }, - - remove: function(document, callback) { - var self = this; - var data = {}; - document['$saveTo'](data); - this._jsonRequest(["DELETE", "", data], function(response) { - delete self._cache[document['$entity'] + '/' + document['$id']]; - _.each(self._cache_collections, function(collection){ - for ( var i = 0; i < collection.length; i++) { - var item = collection[i]; - if (item['$id'] == document['$id']) { - collection.splice(i, 1); - } - } - }); - (callback||noop)(response); - }); - }, - - _jsonRequest: function(request, callback, failure) { - request['$$callback'] = callback; - request['$$failure'] = failure||function(response){ - throw response; - }; - this.bulkRequest.push(request); - }, - - flush: function() { - if (this.bulkRequest.length === 0) return; - var self = this; - var bulkRequest = this.bulkRequest; - this.bulkRequest = []; - log('REQUEST:', bulkRequest); - function callback(code, bulkResponse){ - log('RESPONSE[' + code + ']: ', bulkResponse); - if(bulkResponse['$status_code'] == 401) { - self.users['login'](function(){ - self.post(bulkRequest, callback); - }); - } else if(bulkResponse['$status_code']) { - alert(toJson(bulkResponse)); - } else { - for ( var i = 0; i < bulkResponse.length; i++) { - var response = bulkResponse[i]; - var request = bulkRequest[i]; - var responseCode = response['$status_code']; - if(responseCode) { - if(responseCode == 403) { - self.users['notAuthorized'](); - } else { - request['$$failure'](response); - } - } else { - request['$$callback'](response); - } - } - } - } - this.post(bulkRequest, callback); - }, - - saveScope: function(scope, callback) { - var saveCounter = 1; - function onSaveDone() { - saveCounter--; - if (saveCounter === 0 && callback) - callback(); - } - for(var key in scope) { - var item = scope[key]; - if (item && item['$save'] == Model.prototype['$save']) { - saveCounter++; - item['$save'](onSaveDone); - } - } - onSaveDone(); - }, - - query: function(type, query, arg, callback){ - var self = this; - var queryList = []; - queryList['$$accept'] = function(doc){ - return false; - }; - this._cache_collections.push(queryList); - var request = type['title'] + '/' + query + '=' + arg; - this._jsonRequest(["GET", request], function(response){ - var list = response; - foreach(list, function(item){ - var document = type()['$loadFrom'](item); - queryList.push(self.cache(document)); - }); - (callback||noop)(queryList); - }); - return queryList; - }, - - entities: function(callback) { - var entities = []; - var self = this; - this._jsonRequest(["GET", "$entities"], function(response) { - foreach(response, function(value, entityName){ - entities.push(self.entity(entityName)); - }); - entities.sort(function(a,b){return a.title > b.title ? 1 : -1;}); - (callback||noop)(entities); - }); - return entities; - }, - - documentCountsByUser: function(){ - var counts = {}; - var self = this; - self.post([["GET", "$users"]], function(code, response){ - extend(counts, response[0]); - }); - return counts; - }, - - userDocumentIdsByEntity: function(user){ - var ids = {}; - var self = this; - self.post([["GET", "$users/" + user]], function(code, response){ - extend(ids, response[0]); - }); - return ids; - }, - - entity: function(name, defaults){ - if (!name) { - return DataStore.NullEntity; - } - var self = this; - var entity = extend(function(initialState){ - return new Model(entity, initialState); - }, { - // entity.name does not work as name seems to be reserved for functions - 'title': name, - '$$factory': true, - datastore: this, //private, obfuscate - 'defaults': defaults || {}, - 'load': function(id, callback){ - return self.load(entity(), id, callback); - }, - 'loadMany': function(ids, callback){ - return self.loadMany(entity, ids, callback); - }, - 'loadOrCreate': function(id, callback){ - return self.loadOrCreate(entity(), id, callback); - }, - 'all': function(callback){ - return self.loadAll(entity, callback); - }, - 'query': function(query, queryArgs, callback){ - return self.query(entity, query, queryArgs, callback); - }, - 'properties': function(callback) { - self._jsonRequest(["GET", name + "/$properties"], callback); - } - }); - return entity; - }, - - join: function(join){ - function fn(){ - throw "Joined entities can not be instantiated into a document."; - }; - function base(name){return name ? name.substring(0, name.indexOf('.')) : undefined;} - function next(name){return name.substring(name.indexOf('.') + 1);} - var joinOrder = _(join).chain(). - map(function($, name){ - return name;}). - sortBy(function(name){ - var path = []; - do { - if (_(path).include(name)) throw "Infinite loop in join: " + path.join(" -> "); - path.push(name); - if (!join[name]) throw _("Named entity '<%=name%>' is undefined.").template({name:name}); - name = base(join[name].on); - } while(name); - return path.length; - }). - value(); - if (_(joinOrder).select(function($){return join[$].on;}).length != joinOrder.length - 1) - throw "Exactly one entity needs to be primary."; - fn['query'] = function(exp, value) { - var joinedResult = []; - var baseName = base(exp); - if (baseName != joinOrder[0]) throw _("Named entity '<%=name%>' is not a primary entity.").template({name:baseName}); - var Entity = join[baseName].join; - var joinIndex = 1; - Entity['query'](next(exp), value, function(result){ - var nextJoinName = joinOrder[joinIndex++]; - var nextJoin = join[nextJoinName]; - var nextJoinOn = nextJoin.on; - var joinIds = {}; - _(result).each(function(doc){ - var row = {}; - joinedResult.push(row); - row[baseName] = doc; - var id = Scope.getter(row, nextJoinOn); - joinIds[id] = id; - }); - nextJoin.join.loadMany(_.toArray(joinIds), function(result){ - var byId = {}; - _(result).each(function(doc){ - byId[doc.$id] = doc; - }); - _(joinedResult).each(function(row){ - var id = Scope.getter(row, nextJoinOn); - row[nextJoinName] = byId[id]; - }); - }); - }); - return joinedResult; - }; - return fn; - } -}; diff --git a/src/Formatters.js b/src/Formatters.js index 6aa832af..f2d5d33e 100644 --- a/src/Formatters.js +++ b/src/Formatters.js @@ -7,12 +7,17 @@ extend(angularFormatter, { 'list':formater( function(obj) { return obj ? obj.join(", ") : obj; }, - function(value) { - return value ? _(_(value.split(',')).map(jQuery.trim)).select(_.identity) : []; + function(value) { + var list = []; + foreach(value.split(','), function(item){ + item = trim(item); + if (item) list.push(item); + }); + return list; } ), 'trim':formater( function(obj) { return obj ? $.trim("" + obj) : ""; } - ) + ) }); diff --git a/src/JSON.js b/src/JSON.js index 98dfddd2..baf3a2fa 100644 --- a/src/JSON.js +++ b/src/JSON.js @@ -2,7 +2,7 @@ array = [].constructor; function toJson(obj, pretty){ var buf = []; - toJsonArray(buf, obj, pretty ? "\n " : null, _([])); + toJsonArray(buf, obj, pretty ? "\n " : null, []); return buf.join(''); }; @@ -27,7 +27,7 @@ angular['fromJson'] = fromJson; function toJsonArray(buf, obj, pretty, stack){ if (typeof obj == "object") { - if (stack.include(obj)) { + if (includes(stack, obj)) { buf.push("RECURSION"); return; } diff --git a/src/Model.js b/src/Model.js deleted file mode 100644 index b09efd0e..00000000 --- a/src/Model.js +++ /dev/null @@ -1,65 +0,0 @@ -// Single $ is special and does not get searched -// Double $$ is special an is client only (does not get sent to server) - -function Model(entity, initial) { - this['$$entity'] = entity; - this['$loadFrom'](initial||{}); - this['$entity'] = entity['title']; - this['$migrate'](); -}; - -Model.copyDirectFields = function(src, dst) { - if (src === dst || !src || !dst) return; - var isDataField = function(src, dst, field) { - return (field.substring(0,2) !== '$$') && - (typeof src[field] !== 'function') && - (typeof dst[field] !== 'function'); - }; - for (var field in dst) { - if (isDataField(src, dst, field)) - delete dst[field]; - } - for (field in src) { - if (isDataField(src, dst, field)) - dst[field] = src[field]; - } -}; - -extend(Model.prototype, { - '$migrate': function() { - merge(this['$$entity']['defaults'], this); - return this; - }, - - '$merge': function(other) { - merge(other, this); - return this; - }, - - '$save': function(callback) { - this['$$entity'].datastore.save(this, callback === true ? undefined : callback); - if (callback === true) this['$$entity'].datastore.flush(); - return this; - }, - - '$delete': function(callback) { - this['$$entity'].datastore.remove(this, callback === true ? undefined : callback); - if (callback === true) this['$$entity'].datastore.flush(); - return this; - }, - - '$loadById': function(id, callback) { - this['$$entity'].datastore.load(this, id, callback); - return this; - }, - - '$loadFrom': function(other) { - Model.copyDirectFields(other, this); - return this; - }, - - '$saveTo': function(other) { - Model.copyDirectFields(this, other); - return this; - } -}); \ No newline at end of file diff --git a/src/Scope.js b/src/Scope.js index ccf55077..6ba6aa8e 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -1,253 +1,3 @@ -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('.'); @@ -262,12 +12,12 @@ function getter(instance, path) { lastInstance = instance; instance = instance[key]; } - if (_.isUndefined(instance) && key.charAt(0) == '$') { + 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); + instance = bind(fn, lastInstance, lastInstance); return instance; } } @@ -303,24 +53,26 @@ function expressionCompile(exp){ parser.assertAllConsumed(); compileCache[exp] = expFn; } - // return expFn - // TODO(remove this hack) + return parserNewScopeAdapter(expFn); +}; + +// return expFn +// TODO(remove this hack) +function parserNewScopeAdapter(fn) { return function(){ - return expFn({ + return fn({ 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 isRenderableElement(element) { + var name = element && element[0] && element[0].nodeName; + return name && name.charAt(0) != '#' && + !includes(['TR', 'COL', 'COLGROUP', 'TBODY', 'THEAD', 'TFOOT'], name); } function rethrow(e) { throw e; } @@ -334,7 +86,7 @@ function errorHandlerFor(element) { }; } -function scope(parent, Class) { +function createScope(parent, Class) { function Parent(){} function API(){} function Behavior(){} diff --git a/src/Server.js b/src/Server.js deleted file mode 100644 index 5c4ec3c6..00000000 --- a/src/Server.js +++ /dev/null @@ -1,68 +0,0 @@ -function Server(url, getScript) { - this.url = url; - this.nextId = 0; - this.getScript = getScript; - this.uuid = "_" + ("" + Math.random()).substr(2) + "_"; - this.maxSize = 1800; -}; - -Server.prototype = { - base64url: function(txt) { - return Base64.encode(txt); - }, - - request: function(method, url, request, callback) { - var requestId = this.uuid + (this.nextId++); - var payload = this.base64url(toJson({'u':url, 'm':method, 'p':request})); - var totalPockets = Math.ceil(payload.length / this.maxSize); - var baseUrl = this.url + "/$/" + requestId + "/" + totalPockets + "/"; - angularCallbacks[requestId] = function(response) { - delete angularCallbacks[requestId]; - callback(200, response); - }; - for ( var pocketNo = 0; pocketNo < totalPockets; pocketNo++) { - var pocket = payload.substr(pocketNo * this.maxSize, this.maxSize); - this.getScript(baseUrl + (pocketNo+1) + "?h=" + pocket, noop); - } - } -}; - -function FrameServer(frame) { - this.frame = frame; -}; -FrameServer.PREFIX = "$DATASET:"; - -FrameServer.prototype = { - read:function(){ - this.data = fromJson(this.frame.name.substr(FrameServer.PREFIX.length)); - }, - write:function(){ - this.frame.name = FrameServer.PREFIX + toJson(this.data); - }, - request: function(method, url, request, callback) { - //alert(method + " " + url + " " + toJson(request) + " " + toJson(callback)); - } -}; - - -function VisualServer(delegate, status, update) { - this.delegate = delegate; - this.update = update; - this.status = status; -}; - -VisualServer.prototype = { - request:function(method, url, request, callback) { - var self = this; - this.status.beginRequest(request); - this.delegate.request(method, url, request, function() { - self.status.endRequest(); - try { - callback.apply(this, arguments); - } catch (e) { - alert(toJson(e)); - } - self.update(); - }); - } -}; diff --git a/src/UrlWatcher.js b/src/UrlWatcher.js new file mode 100644 index 00000000..0892eb1a --- /dev/null +++ b/src/UrlWatcher.js @@ -0,0 +1,62 @@ + +// //////////////////////////// +// UrlWatcher +// //////////////////////////// + +function UrlWatcher(location) { + this.location = location; + this.delay = 25; + this.setTimeout = function(fn, delay) { + window.setTimeout(fn, delay); + }; + this.listener = function(url) { + return url; + }; + this.expectedUrl = location.href; +} + +UrlWatcher.prototype = { + listen: function(fn){ + this.listener = fn; + }, + watch: function() { + var self = this; + var pull = function() { + if (self.expectedUrl !== self.location.href) { + var notify = self.location.hash.match(/^#\$iframe_notify=(.*)$/); + if (notify) { + if (!self.expectedUrl.match(/#/)) { + self.expectedUrl += "#"; + } + self.location.href = self.expectedUrl; + var id = '_iframe_notify_' + notify[1]; + var notifyFn = angularCallbacks[id]; + delete angularCallbacks[id]; + try { + (notifyFn||noop)(); + } catch (e) { + alert(e); + } + } else { + self.listener(self.location.href); + self.expectedUrl = self.location.href; + } + } + self.setTimeout(pull, self.delay); + }; + pull(); + }, + + set: function(url) { + var existingURL = this.location.href; + if (!existingURL.match(/#/)) + existingURL += '#'; + if (existingURL != url) + this.location.href = url; + this.existingURL = url; + }, + + get: function() { + return window.location.href; + } +}; diff --git a/src/Users.js b/src/Users.js deleted file mode 100644 index fb5845d3..00000000 --- a/src/Users.js +++ /dev/null @@ -1,35 +0,0 @@ -function Users(server, controlBar) { - this.server = server; - this.controlBar = controlBar; -}; - -extend(Users.prototype, { - 'fetchCurrentUser':function(callback) { - var self = this; - this.server.request("GET", "/account.json", {}, function(code, response){ - self['current'] = response['user']; - callback(response['user']); - }); - }, - - 'logout': function(callback) { - var self = this; - this.controlBar.logout(function(){ - delete self['current']; - (callback||noop)(); - }); - }, - - 'login': function(callback) { - var self = this; - this.controlBar.login(function(){ - self['fetchCurrentUser'](function(){ - (callback||noop)(); - }); - }); - }, - - 'notAuthorized': function(){ - this.controlBar.notAuthorized(); - } -}); diff --git a/src/Widgets.js b/src/Widgets.js index 74f70f21..42b9e916 100644 --- a/src/Widgets.js +++ b/src/Widgets.js @@ -1,806 +1,137 @@ -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('' + - '' + - '' + - '' + - '' + - ''); -}; - -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; +function modelAccessor(scope, element) { + var expr = element.attr('name'), + farmatterName = element.attr('ng-format') || NOOP, + formatter = angularFormatter(farmatterName); + if (!expr) throw "Required field 'name' not found."; + if (!formatter) throw "Formatter named '" + farmatterName + "' not found."; + return { + get: function() { + return formatter['format'](scope.$eval(expr)); + }, + set: function(value) { + scope.$eval(expr + '=' + toJson(formatter['parse'](value))); + } + }; +} + +function compileValidator(expr) { + return new Parser(expr).validator()(); +} + +function valueAccessor(element) { + var validatorName = element.attr('ng-validate') || NOOP, + validator = compileValidator(validatorName), + required = element.attr('ng-required'), + lastError; + required = required || required == ''; + if (!validator) throw "Validator named '" + validatorName + "' not found."; + function validate(value) { + var error = required && !trim(value) ? "Required" : validator.call(this, value); + if (error !== lastError) { + if (error) { + element.addClass(NG_VALIDATION_ERROR); + element.attr(NG_ERROR, error); } 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); + element.removeClass(NG_VALIDATION_ERROR); + element.removeAttr(NG_ERROR); } + lastError = error; } 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 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( - "" + - "" + - ""+title+"" + - ""+msg+"" + - ""); - 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 ='loading....'; - -Status.prototype = { - beginRequest: function () { - if (this.requestCount === 0) { - (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show(); } - this.requestCount++; - }, + }; +} + +function noopAccessor() { return { get: noop, set: noop }; } + +var NG_ERROR = 'ng-error', + NG_VALIDATION_ERROR = 'ng-validation-error', + textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, ''), + buttonWidget = inputWidget('click', noopAccessor, noopAccessor, undefined), + INPUT_TYPE = { + 'text': textWidget, + 'textarea': textWidget, + 'hidden': textWidget, + 'password': textWidget, + 'button': buttonWidget, + 'submit': buttonWidget, + 'reset': buttonWidget, + 'image': buttonWidget, + 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, false), + 'radio': inputWidget('click', modelAccessor, radioAccessor, undefined), + 'select-one': inputWidget('click', modelAccessor, valueAccessor, null), + 'select-multiple': inputWidget('click', modelAccessor, optionsAccessor, []) +// 'file': fileWidget??? + }; - endRequest: function () { - this.requestCount--; - if (this.requestCount === 0) { - this.loader.hide("fold"); - } - } -}; +function inputWidget(events, modelAccessor, viewAccessor, initValue) { + return function(element) { + var scope = this, + model = modelAccessor(scope, element), + view = viewAccessor(element), + action = element.attr('ng-action') || '', + value = view.get() || copy(initValue); + if (isDefined(value)) model.set(value); + this.$eval(element.attr('ng-init')||''); + element.bind(events, function(){ + model.set(view.get()); + scope.$tryEval(action, element); + scope.$root.$eval(); + // if we have no initValue than we are just a button, + // therefore we want to prevent default action + return isDefined(initValue); + }); + scope.$watch(model.get, view.set); + }; +} + +function inputWidgetSelector(element){ + return INPUT_TYPE[lowercase(element[0].type)] || noop; +} + +angularWidget('INPUT', inputWidgetSelector); +angularWidget('TEXTAREA', inputWidgetSelector); +angularWidget('BUTTON', inputWidgetSelector); +angularWidget('SELECT', function(element){ + this.descend(true); + return inputWidgetSelector.call(this, element); +}); diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index d2b2ff9c..7798afa5 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -1,18 +1,18 @@ /** * The MIT License - * + * * Copyright (c) 2010 Adam Abrons and Misko Hevery http://getangular.com - * + * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: - * + * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. - * + * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,35 +22,58 @@ * THE SOFTWARE. */ (function(previousOnLoad){ - var filename = /(.*)\/angular-(.*).js/; - var scripts = document.getElementsByTagName("script"); + var filename = /(.*)\/angular-(.*).js(#(.*))?/; + var scripts = document.getElementsByTagName("SCRIPT"); var serverPath; + var config = {}; for(var j = 0; j < scripts.length; j++) { var match = (scripts[j].src || "").match(filename); if (match) { serverPath = match[1]; + parseConfig(match[4]); + } + } + + function parseConfig(args) { + var keyValues = args.split('&'), keyValue, i = 0; + for (; i < keyValues.length; i++) { + keyValue = keyValues[i].split('='); + config[keyValue[0]] = keyValue[1] || true; } } function addScript(file){ document.write(''); - }; + } addScript("/Angular.js"); - addScript("/API.js"); - addScript("/Binder.js"); - addScript("/ControlBar.js"); - addScript("/DataStore.js"); - addScript("/Filters.js"); - addScript("/Formatters.js"); addScript("/JSON.js"); - addScript("/Model.js"); + addScript("/Compiler.js"); + addScript("/Scope.js"); + addScript("/jqlite.js"); addScript("/Parser.js"); addScript("/Resource.js"); - addScript("/Scope.js"); - addScript("/Server.js"); - addScript("/Users.js"); - addScript("/Validators.js"); - addScript("/Widgets.js"); + addScript("/URLWatcher.js"); + + // Extension points + addScript("/apis.js"); + addScript("/filters.js"); + addScript("/formatters.js"); + addScript("/validators.js"); + addScript("/directives.js"); + addScript("/markups.js"); + addScript("/widgets.js"); + + if (config.autobind) { + window.onload = function(){ + try { + if (previousOnLoad) previousOnLoad(); + } catch(e) {} + var scope = angular.compile(window.document, config); + if (config.rootScope) window[config.rootScope] = scope; + scope.$init(); + }; + } + })(window.onload); diff --git a/src/apis.js b/src/apis.js new file mode 100644 index 00000000..e375e8fc --- /dev/null +++ b/src/apis.js @@ -0,0 +1,328 @@ +var angularGlobal = { + 'typeOf':function(obj){ + if (obj === null) return "null"; + var type = typeof obj; + if (type == "object") { + if (obj instanceof Array) return "array"; + if (obj instanceof Date) return "date"; + if (obj.nodeType == 1) return "element"; + } + return type; + } +}; + +var angularCollection = {}; +var angularObject = {}; +var angularArray = { + 'includeIf':function(array, value, condition) { + var index = _.indexOf(array, value); + if (condition) { + if (index == -1) + array.push(value); + } else { + array.splice(index, 1); + } + return array; + }, + 'sum':function(array, expression) { + var fn = angular['Function']['compile'](expression); + var sum = 0; + for (var i = 0; i < array.length; i++) { + var value = 1 * fn(array[i]); + if (!isNaN(value)){ + sum += value; + } + } + return sum; + }, + 'remove':function(array, value) { + var index = _.indexOf(array, value); + if (index >=0) + array.splice(index, 1); + return value; + }, + 'find':function(array, condition, defaultValue) { + if (!condition) return undefined; + var fn = angular['Function']['compile'](condition); + _.detect(array, function($){ + if (fn($)){ + defaultValue = $; + return true; + } + }); + return defaultValue; + }, + 'findById':function(array, id) { + return angular.Array.find(array, function($){return $.$id == id;}, null); + }, + 'filter':function(array, expression) { + var predicates = []; + predicates.check = function(value) { + for (var j = 0; j < predicates.length; j++) { + if(!predicates[j](value)) { + return false; + } + } + return true; + }; + var getter = Scope.getter; + var search = function(obj, text){ + if (text.charAt(0) === '!') { + return !search(obj, text.substr(1)); + } + switch (typeof obj) { + case "boolean": + case "number": + case "string": + return ('' + obj).toLowerCase().indexOf(text) > -1; + case "object": + for ( var objKey in obj) { + if (objKey.charAt(0) !== '$' && search(obj[objKey], text)) { + return true; + } + } + return false; + case "array": + for ( var i = 0; i < obj.length; i++) { + if (search(obj[i], text)) { + return true; + } + } + return false; + default: + return false; + } + }; + switch (typeof expression) { + case "boolean": + case "number": + case "string": + expression = {$:expression}; + case "object": + for (var key in expression) { + if (key == '$') { + (function(){ + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(value, text); + }); + })(); + } else { + (function(){ + var path = key; + var text = (''+expression[key]).toLowerCase(); + if (!text) return; + predicates.push(function(value) { + return search(getter(value, path), text); + }); + })(); + } + } + break; + case "function": + predicates.push(expression); + break; + default: + return array; + } + var filtered = []; + for ( var j = 0; j < array.length; j++) { + var value = array[j]; + if (predicates.check(value)) { + filtered.push(value); + } + } + return filtered; + }, + 'add':function(array, value) { + array.push(_.isUndefined(value)? {} : value); + return array; + }, + 'count':function(array, condition) { + if (!condition) return array.length; + var fn = angular['Function']['compile'](condition); + return _.reduce(array, 0, function(count, $){return count + (fn($)?1:0);}); + }, + 'orderBy':function(array, expression, descend) { + function reverse(comp, descending) { + return toBoolean(descending) ? + function(a,b){return comp(b,a);} : comp; + } + function compare(v1, v2){ + var t1 = typeof v1; + var t2 = typeof v2; + if (t1 == t2) { + if (t1 == "string") v1 = v1.toLowerCase(); + if (t1 == "string") v2 = v2.toLowerCase(); + if (v1 === v2) return 0; + return v1 < v2 ? -1 : 1; + } else { + return t1 < t2 ? -1 : 1; + } + } + expression = _.isArray(expression) ? expression: [expression]; + expression = _.map(expression, function($){ + var descending = false; + if (typeof $ == "string" && ($.charAt(0) == '+' || $.charAt(0) == '-')) { + descending = $.charAt(0) == '-'; + $ = $.substring(1); + } + var get = $ ? angular['Function']['compile']($) : _.identity; + return reverse(function(a,b){ + return compare(get(a),get(b)); + }, descending); + }); + var comparator = function(o1, o2){ + for ( var i = 0; i < expression.length; i++) { + var comp = expression[i](o1, o2); + if (comp !== 0) return comp; + } + return 0; + }; + return _.clone(array).sort(reverse(comparator, descend)); + }, + 'orderByToggle':function(predicate, attribute) { + var STRIP = /^([+|-])?(.*)/; + var ascending = false; + var index = -1; + _.detect(predicate, function($, i){ + if ($ == attribute) { + ascending = true; + index = i; + return true; + } + if (($.charAt(0)=='+'||$.charAt(0)=='-') && $.substring(1) == attribute) { + ascending = $.charAt(0) == '+'; + index = i; + return true; + } + }); + if (index >= 0) { + predicate.splice(index, 1); + } + predicate.unshift((ascending ? "-" : "+") + attribute); + return predicate; + }, + 'orderByDirection':function(predicate, attribute, ascend, descend) { + ascend = ascend || 'ng-ascend'; + descend = descend || 'ng-descend'; + var att = predicate[0] || ''; + var direction = true; + if (att.charAt(0) == '-') { + att = att.substring(1); + direction = false; + } else if(att.charAt(0) == '+') { + att = att.substring(1); + } + return att == attribute ? (direction ? ascend : descend) : ""; + }, + 'merge':function(array, index, mergeValue) { + var value = array[index]; + if (!value) { + value = {}; + array[index] = value; + } + merge(mergeValue, value); + return array; + } +}; + +var angularString = { + 'quote':function(string) { + return '"' + string.replace(/\\/g, '\\\\'). + replace(/"/g, '\\"'). + replace(/\n/g, '\\n'). + replace(/\f/g, '\\f'). + replace(/\r/g, '\\r'). + replace(/\t/g, '\\t'). + replace(/\v/g, '\\v') + + '"'; + }, + 'quoteUnicode':function(string) { + var str = angular['String']['quote'](string); + var chars = []; + for ( var i = 0; i < str.length; i++) { + var ch = str.charCodeAt(i); + if (ch < 128) { + chars.push(str.charAt(i)); + } else { + var encode = "000" + ch.toString(16); + chars.push("\\u" + encode.substring(encode.length - 4)); + } + } + return chars.join(''); + }, + 'toDate':function(string){ + var match; + if (typeof string == 'string' && + (match = string.match(/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/))){ + var date = new Date(0); + date.setUTCFullYear(match[1], match[2] - 1, match[3]); + date.setUTCHours(match[4], match[5], match[6], 0); + return date; + } + return string; + } +}; + +var angularDate = { + 'toString':function(date){ + function pad(n) { return n < 10 ? "0" + n : n; } + return !date ? date : + date.getUTCFullYear() + '-' + + pad(date.getUTCMonth() + 1) + '-' + + pad(date.getUTCDate()) + 'T' + + pad(date.getUTCHours()) + ':' + + pad(date.getUTCMinutes()) + ':' + + pad(date.getUTCSeconds()) + 'Z' ; + } + }; + +var angularFunction = { + 'compile':function(expression) { + if (_.isFunction(expression)){ + return expression; + } else if (expression){ + var scope = new Scope(); + return function($) { + scope.state = $; + return scope.eval(expression); + }; + } else { + return function($){return $;}; + } + } +}; + +function defineApi(dst, chain, underscoreNames){ + if (_) { + var lastChain = _.last(chain); + foreach(underscoreNames, function(name){ + lastChain[name] = _[name]; + }); + } + angular[dst] = angular[dst] || {}; + foreach(chain, function(parent){ + extend(angular[dst], parent); + }); +} +defineApi('Global', [angularGlobal], + ['extend', 'clone','isEqual', + 'isElement', 'isArray', 'isFunction', 'isUndefined']); +defineApi('Collection', [angularGlobal, angularCollection], + ['each', 'map', 'reduce', 'reduceRight', 'detect', + 'select', 'reject', 'all', 'any', 'include', + 'invoke', 'pluck', 'max', 'min', 'sortBy', + 'sortedIndex', 'toArray', 'size']); +defineApi('Array', [angularGlobal, angularCollection, angularArray], + ['first', 'last', 'compact', 'flatten', 'without', + 'uniq', 'intersect', 'zip', 'indexOf', 'lastIndexOf']); +defineApi('Object', [angularGlobal, angularCollection, angularObject], + ['keys', 'values']); +defineApi('String', [angularGlobal, angularString], []); +defineApi('Date', [angularGlobal, angularDate], []); +//IE bug +angular['Date']['toString'] = angularDate['toString']; +defineApi('Function', [angularGlobal, angularCollection, angularFunction], + ['bind', 'bindAll', 'delay', 'defer', 'wrap', 'compose']); 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 = ' ' + 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(/.*<\/\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('' + + '' + + '' + + '' + + '' + + ''); +}; + +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 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( + "" + + "" + + ""+title+"" + + ""+msg+"" + + ""); + 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 ='loading....'; + +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"); + } + } +}; diff --git a/src/directives.js b/src/directives.js index 10476c77..c54c89e9 100644 --- a/src/directives.js +++ b/src/directives.js @@ -11,9 +11,15 @@ angularDirective("ng-eval", function(expression){ }); angularDirective("ng-bind", function(expression){ + var templateFn = compileBindTemplate("{{" + expression + "}}"); return function(element) { - this.$watch(expression, function(value){ - element.text(value); + var lastValue; + this.$onEval(function() { + var value = templateFn.call(this); + if (value != lastValue) { + element.text(value); + lastValue = value; + } }, element); }; }); @@ -34,7 +40,9 @@ function compileBindTemplate(template){ bindTemplateCache[template] = fn = function(){ var parts = [], self = this; foreach(bindings, function(fn){ - parts.push(fn.call(self)); + var value = fn.call(self); + if (isObject(value)) value = toJson(value, true); + parts.push(value); }); return parts.join(''); }; @@ -125,6 +133,7 @@ angularDirective("ng-action", function(expression, element){ var self = this; element.click(function(){ self.$tryEval(expression, element); + self.$eval(); }); }; }); diff --git a/src/jqLite.js b/src/jqLite.js index 7646bf98..a5014354 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -38,7 +38,8 @@ function JQLite(element) { this[0] = element; } -function jqLite(element) { + +function jqLiteWrap(element) { if (typeof element == 'string') { var div = document.createElement('div'); div.innerHTML = element; @@ -47,6 +48,8 @@ function jqLite(element) { return element instanceof JQLite ? element : new JQLite(element); } +jqLite = jqLite || jqLiteWrap; + JQLite.prototype = { data: function(key, value) { var element = this[0], @@ -85,12 +88,15 @@ JQLite.prototype = { foreach(type.split(' '), function(type){ eventHandler = bind[type]; if (!eventHandler) { - bind[type] = eventHandler = function() { - var value = false; + bind[type] = eventHandler = function(event) { + var bubbleEvent = false; foreach(eventHandler.fns, function(fn){ - value = value || fn.apply(self, arguments); + bubbleEvent = bubbleEvent || fn.apply(self, arguments); }); - return value; + if (!bubbleEvent) { + event.preventDefault(); + event.stopPropagation(); + } }; eventHandler.fns = []; addEventListener(element, type, eventHandler); diff --git a/src/markup.js b/src/markup.js deleted file mode 100644 index 5fb10779..00000000 --- a/src/markup.js +++ /dev/null @@ -1,76 +0,0 @@ -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 (hasBindings(bindings)) { - 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(); - } -}); - -angularTextMarkup('OPTION', function(text, textNode, parentElement){ - if (parentElement[0].nodeName == "OPTION") { - var select = document.createElement('select'); - select.insertBefore(parentElement[0].cloneNode(true), null); - if (!select.innerHTML.match(/.*<\/\s*option\s*>/gi)) { - parentElement.attr('value', text); - } - } -}); - -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/markups.js b/src/markups.js new file mode 100644 index 00000000..6bc27c85 --- /dev/null +++ b/src/markups.js @@ -0,0 +1,76 @@ +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 || binding(bindings[0]) !== null; +}; + +angularTextMarkup('{{}}', function(text, textNode, parentElement) { + var bindings = parseBindings(text), + self = this; + if (hasBindings(bindings)) { + 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(); + } +}); + +angularTextMarkup('OPTION', function(text, textNode, parentElement){ + if (parentElement[0].nodeName == "OPTION") { + var select = document.createElement('select'); + select.insertBefore(parentElement[0].cloneNode(true), null); + if (!select.innerHTML.match(/.*<\/\s*option\s*>/gi)) { + parentElement.attr('value', text); + } + } +}); + +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/moveToAngularCom/ControlBar.js b/src/moveToAngularCom/ControlBar.js new file mode 100644 index 00000000..685beeb2 --- /dev/null +++ b/src/moveToAngularCom/ControlBar.js @@ -0,0 +1,72 @@ +function ControlBar(document, serverUrl, database) { + this._document = document; + this.serverUrl = serverUrl; + this.database = database; + this._window = window; + this.callbacks = []; +}; + +ControlBar.HTML = + '' + + '' + + '' + + '' + + '' + + ''; + + +ControlBar.FORBIDEN = + '' + + 'Sorry, you do not have permission for this!'+ + ''; + +ControlBar.prototype = { + bind: function () { + }, + + login: function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/new.mini?database="+encodeURIComponent(this.database)+"&return_url=" + encodeURIComponent(this.urlWithoutAnchor())); + } + }, + + logout: function (loginSubmitFn) { + this.callbacks.push(loginSubmitFn); + if (this.callbacks.length == 1) { + this.doTemplate("/user_session/do_destroy.mini"); + } + }, + + urlWithoutAnchor: function (path) { + return this._window['location']['href'].split("#")[0]; + }, + + doTemplate: function (path) { + var self = this; + var id = new Date().getTime(); + var url = this.urlWithoutAnchor() + "#$iframe_notify=" + id; + var iframeHeight = 330; + var loginView = jQuery(''); + this._document.append(loginView); + loginView['dialog']({ + 'height':iframeHeight + 33, 'width':500, + 'resizable': false, 'modal':true, + 'title': 'Authentication: <angular/>' + }); + angularCallbacks["_iframe_notify_" + id] = function() { + loginView['dialog']("destroy"); + loginView['remove'](); + foreach(self.callbacks, function(callback){ + callback(); + }); + self.callbacks = []; + }; + }, + + notAuthorized: function () { + if (this.forbidenView) return; + this.forbidenView = jQuery(ControlBar.FORBIDEN); + this.forbidenView.dialog({bgiframe:true, height:70, modal:true}); + } +}; \ No newline at end of file diff --git a/src/moveToAngularCom/DataStore.js b/src/moveToAngularCom/DataStore.js new file mode 100644 index 00000000..70bcc623 --- /dev/null +++ b/src/moveToAngularCom/DataStore.js @@ -0,0 +1,330 @@ +function DataStore(post, users, anchor) { + this.post = post; + this.users = users; + this._cache_collections = []; + this._cache = {'$collections':this._cache_collections}; + this.anchor = anchor; + this.bulkRequest = []; +}; + +DataStore.NullEntity = extend(function(){}, { + 'all': function(){return [];}, + 'query': function(){return [];}, + 'load': function(){return {};}, + 'title': undefined +}); + +DataStore.prototype = { + cache: function(document) { + if (! document.datastore === this) { + throw "Parameter must be an instance of Entity! " + toJson(document); + } + var key = document['$entity'] + '/' + document['$id']; + var cachedDocument = this._cache[key]; + if (cachedDocument) { + Model.copyDirectFields(document, cachedDocument); + } else { + this._cache[key] = document; + cachedDocument = document; + } + return cachedDocument; + }, + + load: function(instance, id, callback, failure) { + if (id && id !== '*') { + var self = this; + this._jsonRequest(["GET", instance['$entity'] + "/" + id], function(response) { + instance['$loadFrom'](response); + instance['$migrate'](); + var clone = instance['$$entity'](instance); + self.cache(clone); + (callback||noop)(instance); + }, failure); + } + return instance; + }, + + loadMany: function(entity, ids, callback) { + var self=this; + var list = []; + var callbackCount = 0; + foreach(ids, function(id){ + list.push(self.load(entity(), id, function(){ + callbackCount++; + if (callbackCount == ids.length) { + (callback||noop)(list); + } + })); + }); + return list; + }, + + loadOrCreate: function(instance, id, callback) { + var self=this; + return this.load(instance, id, callback, function(response){ + if (response['$status_code'] == 404) { + instance['$id'] = id; + (callback||noop)(instance); + } else { + throw response; + } + }); + }, + + loadAll: function(entity, callback) { + var self = this; + var list = []; + list['$$accept'] = function(doc){ + return doc['$entity'] == entity['title']; + }; + this._cache_collections.push(list); + this._jsonRequest(["GET", entity['title']], function(response) { + var rows = response; + for ( var i = 0; i < rows.length; i++) { + var document = entity(); + document['$loadFrom'](rows[i]); + list.push(self.cache(document)); + } + (callback||noop)(list); + }); + return list; + }, + + save: function(document, callback) { + var self = this; + var data = {}; + document['$saveTo'](data); + this._jsonRequest(["POST", "", data], function(response) { + document['$loadFrom'](response); + var cachedDoc = self.cache(document); + _.each(self._cache_collections, function(collection){ + if (collection['$$accept'](document)) { + angularArray['includeIf'](collection, cachedDoc, true); + } + }); + if (document['$$anchor']) { + self.anchor[document['$$anchor']] = document['$id']; + } + if (callback) + callback(document); + }); + }, + + remove: function(document, callback) { + var self = this; + var data = {}; + document['$saveTo'](data); + this._jsonRequest(["DELETE", "", data], function(response) { + delete self._cache[document['$entity'] + '/' + document['$id']]; + _.each(self._cache_collections, function(collection){ + for ( var i = 0; i < collection.length; i++) { + var item = collection[i]; + if (item['$id'] == document['$id']) { + collection.splice(i, 1); + } + } + }); + (callback||noop)(response); + }); + }, + + _jsonRequest: function(request, callback, failure) { + request['$$callback'] = callback; + request['$$failure'] = failure||function(response){ + throw response; + }; + this.bulkRequest.push(request); + }, + + flush: function() { + if (this.bulkRequest.length === 0) return; + var self = this; + var bulkRequest = this.bulkRequest; + this.bulkRequest = []; + log('REQUEST:', bulkRequest); + function callback(code, bulkResponse){ + log('RESPONSE[' + code + ']: ', bulkResponse); + if(bulkResponse['$status_code'] == 401) { + self.users['login'](function(){ + self.post(bulkRequest, callback); + }); + } else if(bulkResponse['$status_code']) { + alert(toJson(bulkResponse)); + } else { + for ( var i = 0; i < bulkResponse.length; i++) { + var response = bulkResponse[i]; + var request = bulkRequest[i]; + var responseCode = response['$status_code']; + if(responseCode) { + if(responseCode == 403) { + self.users['notAuthorized'](); + } else { + request['$$failure'](response); + } + } else { + request['$$callback'](response); + } + } + } + } + this.post(bulkRequest, callback); + }, + + saveScope: function(scope, callback) { + var saveCounter = 1; + function onSaveDone() { + saveCounter--; + if (saveCounter === 0 && callback) + callback(); + } + for(var key in scope) { + var item = scope[key]; + if (item && item['$save'] == Model.prototype['$save']) { + saveCounter++; + item['$save'](onSaveDone); + } + } + onSaveDone(); + }, + + query: function(type, query, arg, callback){ + var self = this; + var queryList = []; + queryList['$$accept'] = function(doc){ + return false; + }; + this._cache_collections.push(queryList); + var request = type['title'] + '/' + query + '=' + arg; + this._jsonRequest(["GET", request], function(response){ + var list = response; + foreach(list, function(item){ + var document = type()['$loadFrom'](item); + queryList.push(self.cache(document)); + }); + (callback||noop)(queryList); + }); + return queryList; + }, + + entities: function(callback) { + var entities = []; + var self = this; + this._jsonRequest(["GET", "$entities"], function(response) { + foreach(response, function(value, entityName){ + entities.push(self.entity(entityName)); + }); + entities.sort(function(a,b){return a.title > b.title ? 1 : -1;}); + (callback||noop)(entities); + }); + return entities; + }, + + documentCountsByUser: function(){ + var counts = {}; + var self = this; + self.post([["GET", "$users"]], function(code, response){ + extend(counts, response[0]); + }); + return counts; + }, + + userDocumentIdsByEntity: function(user){ + var ids = {}; + var self = this; + self.post([["GET", "$users/" + user]], function(code, response){ + extend(ids, response[0]); + }); + return ids; + }, + + entity: function(name, defaults){ + if (!name) { + return DataStore.NullEntity; + } + var self = this; + var entity = extend(function(initialState){ + return new Model(entity, initialState); + }, { + // entity.name does not work as name seems to be reserved for functions + 'title': name, + '$$factory': true, + datastore: this, //private, obfuscate + 'defaults': defaults || {}, + 'load': function(id, callback){ + return self.load(entity(), id, callback); + }, + 'loadMany': function(ids, callback){ + return self.loadMany(entity, ids, callback); + }, + 'loadOrCreate': function(id, callback){ + return self.loadOrCreate(entity(), id, callback); + }, + 'all': function(callback){ + return self.loadAll(entity, callback); + }, + 'query': function(query, queryArgs, callback){ + return self.query(entity, query, queryArgs, callback); + }, + 'properties': function(callback) { + self._jsonRequest(["GET", name + "/$properties"], callback); + } + }); + return entity; + }, + + join: function(join){ + function fn(){ + throw "Joined entities can not be instantiated into a document."; + }; + function base(name){return name ? name.substring(0, name.indexOf('.')) : undefined;} + function next(name){return name.substring(name.indexOf('.') + 1);} + var joinOrder = _(join).chain(). + map(function($, name){ + return name;}). + sortBy(function(name){ + var path = []; + do { + if (_(path).include(name)) throw "Infinite loop in join: " + path.join(" -> "); + path.push(name); + if (!join[name]) throw _("Named entity '<%=name%>' is undefined.").template({name:name}); + name = base(join[name].on); + } while(name); + return path.length; + }). + value(); + if (_(joinOrder).select(function($){return join[$].on;}).length != joinOrder.length - 1) + throw "Exactly one entity needs to be primary."; + fn['query'] = function(exp, value) { + var joinedResult = []; + var baseName = base(exp); + if (baseName != joinOrder[0]) throw _("Named entity '<%=name%>' is not a primary entity.").template({name:baseName}); + var Entity = join[baseName].join; + var joinIndex = 1; + Entity['query'](next(exp), value, function(result){ + var nextJoinName = joinOrder[joinIndex++]; + var nextJoin = join[nextJoinName]; + var nextJoinOn = nextJoin.on; + var joinIds = {}; + _(result).each(function(doc){ + var row = {}; + joinedResult.push(row); + row[baseName] = doc; + var id = Scope.getter(row, nextJoinOn); + joinIds[id] = id; + }); + nextJoin.join.loadMany(_.toArray(joinIds), function(result){ + var byId = {}; + _(result).each(function(doc){ + byId[doc.$id] = doc; + }); + _(joinedResult).each(function(row){ + var id = Scope.getter(row, nextJoinOn); + row[nextJoinName] = byId[id]; + }); + }); + }); + return joinedResult; + }; + return fn; + } +}; diff --git a/src/moveToAngularCom/Model.js b/src/moveToAngularCom/Model.js new file mode 100644 index 00000000..b09efd0e --- /dev/null +++ b/src/moveToAngularCom/Model.js @@ -0,0 +1,65 @@ +// Single $ is special and does not get searched +// Double $$ is special an is client only (does not get sent to server) + +function Model(entity, initial) { + this['$$entity'] = entity; + this['$loadFrom'](initial||{}); + this['$entity'] = entity['title']; + this['$migrate'](); +}; + +Model.copyDirectFields = function(src, dst) { + if (src === dst || !src || !dst) return; + var isDataField = function(src, dst, field) { + return (field.substring(0,2) !== '$$') && + (typeof src[field] !== 'function') && + (typeof dst[field] !== 'function'); + }; + for (var field in dst) { + if (isDataField(src, dst, field)) + delete dst[field]; + } + for (field in src) { + if (isDataField(src, dst, field)) + dst[field] = src[field]; + } +}; + +extend(Model.prototype, { + '$migrate': function() { + merge(this['$$entity']['defaults'], this); + return this; + }, + + '$merge': function(other) { + merge(other, this); + return this; + }, + + '$save': function(callback) { + this['$$entity'].datastore.save(this, callback === true ? undefined : callback); + if (callback === true) this['$$entity'].datastore.flush(); + return this; + }, + + '$delete': function(callback) { + this['$$entity'].datastore.remove(this, callback === true ? undefined : callback); + if (callback === true) this['$$entity'].datastore.flush(); + return this; + }, + + '$loadById': function(id, callback) { + this['$$entity'].datastore.load(this, id, callback); + return this; + }, + + '$loadFrom': function(other) { + Model.copyDirectFields(other, this); + return this; + }, + + '$saveTo': function(other) { + Model.copyDirectFields(this, other); + return this; + } +}); \ No newline at end of file diff --git a/src/moveToAngularCom/Server.js b/src/moveToAngularCom/Server.js new file mode 100644 index 00000000..5c4ec3c6 --- /dev/null +++ b/src/moveToAngularCom/Server.js @@ -0,0 +1,68 @@ +function Server(url, getScript) { + this.url = url; + this.nextId = 0; + this.getScript = getScript; + this.uuid = "_" + ("" + Math.random()).substr(2) + "_"; + this.maxSize = 1800; +}; + +Server.prototype = { + base64url: function(txt) { + return Base64.encode(txt); + }, + + request: function(method, url, request, callback) { + var requestId = this.uuid + (this.nextId++); + var payload = this.base64url(toJson({'u':url, 'm':method, 'p':request})); + var totalPockets = Math.ceil(payload.length / this.maxSize); + var baseUrl = this.url + "/$/" + requestId + "/" + totalPockets + "/"; + angularCallbacks[requestId] = function(response) { + delete angularCallbacks[requestId]; + callback(200, response); + }; + for ( var pocketNo = 0; pocketNo < totalPockets; pocketNo++) { + var pocket = payload.substr(pocketNo * this.maxSize, this.maxSize); + this.getScript(baseUrl + (pocketNo+1) + "?h=" + pocket, noop); + } + } +}; + +function FrameServer(frame) { + this.frame = frame; +}; +FrameServer.PREFIX = "$DATASET:"; + +FrameServer.prototype = { + read:function(){ + this.data = fromJson(this.frame.name.substr(FrameServer.PREFIX.length)); + }, + write:function(){ + this.frame.name = FrameServer.PREFIX + toJson(this.data); + }, + request: function(method, url, request, callback) { + //alert(method + " " + url + " " + toJson(request) + " " + toJson(callback)); + } +}; + + +function VisualServer(delegate, status, update) { + this.delegate = delegate; + this.update = update; + this.status = status; +}; + +VisualServer.prototype = { + request:function(method, url, request, callback) { + var self = this; + this.status.beginRequest(request); + this.delegate.request(method, url, request, function() { + self.status.endRequest(); + try { + callback.apply(this, arguments); + } catch (e) { + alert(toJson(e)); + } + self.update(); + }); + } +}; diff --git a/src/moveToAngularCom/Users.js b/src/moveToAngularCom/Users.js new file mode 100644 index 00000000..fb5845d3 --- /dev/null +++ b/src/moveToAngularCom/Users.js @@ -0,0 +1,35 @@ +function Users(server, controlBar) { + this.server = server; + this.controlBar = controlBar; +}; + +extend(Users.prototype, { + 'fetchCurrentUser':function(callback) { + var self = this; + this.server.request("GET", "/account.json", {}, function(code, response){ + self['current'] = response['user']; + callback(response['user']); + }); + }, + + 'logout': function(callback) { + var self = this; + this.controlBar.logout(function(){ + delete self['current']; + (callback||noop)(); + }); + }, + + 'login': function(callback) { + var self = this; + this.controlBar.login(function(){ + self['fetchCurrentUser'](function(){ + (callback||noop)(); + }); + }); + }, + + 'notAuthorized': function(){ + this.controlBar.notAuthorized(); + } +}); diff --git a/src/widgets2.js b/src/widgets2.js deleted file mode 100644 index 04045426..00000000 --- a/src/widgets2.js +++ /dev/null @@ -1,129 +0,0 @@ -function modelAccessor(scope, element) { - var expr = element.attr('name'), - farmatterName = element.attr('ng-format') || NOOP, - formatter = angularFormatter(farmatterName); - if (!expr) throw "Required field 'name' not found."; - if (!formatter) throw "Formatter named '" + farmatterName + "' not found."; - return { - get: function() { - return formatter['format'](scope.$eval(expr)); - }, - set: function(value) { - scope.$eval(expr + '=' + toJson(formatter['parse'](value))); - } - }; -} - -function valueAccessor(element) { - var validatorName = element.attr('ng-validate') || NOOP, - validator = angularValidator(validatorName), - required = element.attr('ng-required'), - lastError; - required = required || required == ''; - if (!validator) throw "Validator named '" + validatorName + "' not found."; - function validate(value) { - var error = required && !trim(value) ? "Required" : validator(value); - if (error !== lastError) { - if (error) { - element.addClass(NG_VALIDATION_ERROR); - element.attr(NG_ERROR, error); - } else { - element.removeClass(NG_VALIDATION_ERROR); - element.removeAttr(NG_ERROR); - } - lastError = error; - } - return value; - } - return { - get: function(){ return validate(element.val()); }, - set: function(value){ element.val(validate(value)); } - }; -} - -function checkedAccessor(element) { - var domElement = element[0]; - return { - get: function(){ return !!domElement.checked; }, - set: function(value){ domElement.checked = !!value; } - }; -} - -function radioAccessor(element) { - var domElement = element[0]; - return { - get: function(){ return domElement.checked ? domElement.value : null; }, - set: function(value){ domElement.checked = value == domElement.value; } - }; -} - -function optionsAccessor(element) { - var options = element[0].options; - return { - get: function(){ - var values = []; - foreach(options, function(option){ - if (option.selected) values.push(option.value); - }); - return values; - }, - set: function(values){ - var keys = {}; - foreach(values, function(value){ keys[value] = true; }); - foreach(options, function(option){ - option.selected = keys[option.value]; - }); - } - }; -} - -function noopAccessor() { return { get: noop, set: noop }; } - -var NG_ERROR = 'ng-error', - NG_VALIDATION_ERROR = 'ng-validation-error', - textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, ''), - buttonWidget = inputWidget('click', noopAccessor, noopAccessor, undefined), - INPUT_TYPE = { - 'text': textWidget, - 'textarea': textWidget, - 'hidden': textWidget, - 'password': textWidget, - 'button': buttonWidget, - 'submit': buttonWidget, - 'reset': buttonWidget, - 'image': buttonWidget, - 'checkbox': inputWidget('click', modelAccessor, checkedAccessor, false), - 'radio': inputWidget('click', modelAccessor, radioAccessor, undefined), - 'select-one': inputWidget('click', modelAccessor, valueAccessor, null), - 'select-multiple': inputWidget('click', modelAccessor, optionsAccessor, []) -// 'file': fileWidget??? - }; - -function inputWidget(events, modelAccessor, viewAccessor, initValue) { - return function(element) { - var scope = this, - model = modelAccessor(scope, element), - view = viewAccessor(element), - action = element.attr('ng-action') || '', - value = view.get() || copy(initValue); - if (isDefined(value)) model.set(value); - this.$eval(element.attr('ng-init')||''); - element.bind(events, function(){ - model.set(view.get()); - scope.$tryEval(action, element); - }); - scope.$watch(model.get, view.set); - }; -} - -function inputWidgetSelector(element){ - return INPUT_TYPE[lowercase(element[0].type)] || noop; -} - -angularWidget('INPUT', inputWidgetSelector); -angularWidget('TEXTAREA', inputWidgetSelector); -angularWidget('BUTTON', inputWidgetSelector); -angularWidget('SELECT', function(element){ - this.descend(true); - return inputWidgetSelector.call(this, element); -}); -- cgit v1.2.3