diff options
| author | Rob Spies | 2010-06-22 17:09:55 -0700 | 
|---|---|---|
| committer | Rob Spies | 2010-06-22 17:09:55 -0700 | 
| commit | 1500e91defa4020bfe9608749b25e585cd1d8e3d (patch) | |
| tree | 8c2872252b62567dc4eb00f7d7547661d5674c55 /src | |
| parent | eaa397c76b7d28343cde9f3a0338b9b0e79197c8 (diff) | |
| parent | b129a1094e6b42ed82c3ccecc2f40daaa0a6cb6a (diff) | |
| download | angular.js-1500e91defa4020bfe9608749b25e585cd1d8e3d.tar.bz2 | |
Merge http://github.com/angular/angular.js into angular
Conflicts:
	.gitignore
Diffstat (limited to 'src')
34 files changed, 6634 insertions, 0 deletions
diff --git a/src/Angular.js b/src/Angular.js new file mode 100644 index 00000000..2b26c88d --- /dev/null +++ b/src/Angular.js @@ -0,0 +1,401 @@ +//////////////////////////////////// + +if (typeof document.getAttribute == 'undefined') +  document.getAttribute = function() {}; + +if (!window['console']) window['console']={'log':noop, 'error':noop}; + +var consoleNode, +    PRIORITY_FIRST    = -99999, +    PRIORITY_WATCH    = -1000, +    PRIORITY_LAST     =  99999, +    PRIORITY          = {'FIRST': PRIORITY_FIRST, 'LAST': PRIORITY_LAST, 'WATCH':PRIORITY_WATCH}, +    NOOP              = 'noop', +    NG_EXCEPTION      = 'ng-exception', +    NG_VALIDATION_ERROR = 'ng-validation-error', +    jQuery            = window['jQuery'] || window['$'], // weirdness to make IE happy +    _                 = window['_'], +    msie              = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)), +    jqLite            = jQuery || jqLiteWrap, +    slice             = Array.prototype.slice, +    angular           = window['angular']    || (window['angular'] = {}), +    angularTextMarkup = extensionMap(angular, 'textMarkup'), +    angularAttrMarkup = extensionMap(angular, 'attrMarkup'), +    angularDirective  = extensionMap(angular, 'directive'), +    angularWidget     = extensionMap(angular, 'widget'), +    angularValidator  = extensionMap(angular, 'validator'), +    angularFilter     = extensionMap(angular, 'filter'), +    angularFormatter  = extensionMap(angular, 'formatter'), +    angularService    = extensionMap(angular, 'service'), +    angularCallbacks  = extensionMap(angular, 'callbacks'), +    nodeName; + +function angularAlert(){ +  log(arguments); window.alert.apply(window, arguments); +} + +function foreach(obj, iterator, context) { +  var key; +  if (obj) { +    if (isFunction(obj)){ +      for (key in obj) { +        if (key != 'prototype' && key != 'length' && key != 'name' && obj.hasOwnProperty(key)) { +          iterator.call(context, obj[key], key); +        } +      } +    } else if (obj.forEach) { +      obj.forEach(iterator, context); +    } else if (isObject(obj) && isNumber(obj.length)) { +      for (key = 0; key < obj.length; key++) +        iterator.call(context, obj[key], key); +    } else { +      for (key in obj) +        iterator.call(context, obj[key], key); +    } +  } +  return obj; +} + +function foreachSorted(obj, iterator, context) { +  var keys = []; +  for (var key in obj) keys.push(key); +  keys.sort(); +  for ( var i = 0; i < keys.length; i++) { +    iterator.call(context, obj[keys[i]], keys[i]); +  } +  return keys; +} + + +function extend(dst) { +  foreach(arguments, function(obj){ +    if (obj !== dst) { +      foreach(obj, function(value, key){ +        dst[key] = value; +      }); +    } +  }); +  return dst; +} + +function noop() {} +function identity($) {return $;} +function extensionMap(angular, name) { +  var extPoint; +  return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ +    if (isDefined(fn)) { +      extPoint[name] = extend(fn, prop || {}); +    } +    return extPoint[name]; +  }); +} + +function jqLiteWrap(element) { +  // for some reasons the parentNode of an orphan looks like null but its typeof is object. +  if (element) { +    if (isString(element)) { +      var div = document.createElement('div'); +      div.innerHTML = element; +      element = new JQLite(div.childNodes); +    } else if (!(element instanceof JQLite) && isElement(element)) { +      element =  new JQLite(element); +    } +  } +  return element; +} +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';} +function isNumber(value){ return typeof value == 'number';} +function isArray(value) { return value instanceof Array; } +function isFunction(value){ return typeof value == 'function';} +function isTextNode(node) { return nodeName(node) == '#text'; } +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 isElement(node) { +  return node && (node.nodeName || node instanceof JQLite || (jQuery && node instanceof jQuery)); +} + +function HTML(html) { +  this.html = html; +} + +if (msie) { +  nodeName = function(element) { +    element = element[0] || element; +    return (element.scopeName && element.scopeName != 'HTML' ) ? uppercase(element.scopeName + ':' + element.nodeName) : element.nodeName; +  }; +} else { +  nodeName = function(element) { +    return (element[0] || element).nodeName; +  }; +} + +function isVisible(element) { +  var rect = element[0].getBoundingClientRect(), +      width = (rect.width || (rect.right||0 - rect.left||0)), +      height = (rect.height || (rect.bottom||0 - rect.top||0)); +  return width>0 && height>0; +} + +function map(obj, iterator, context) { +  var results = []; +  foreach(obj, function(value, index, list) { +    results.push(iterator.call(context, value, index, list)); +  }); +  return results; +} +function size(obj) { +  var size = 0; +  if (obj) { +    if (isNumber(obj.length)) { +      return obj.length; +    } else if (isObject(obj)){ +      for (key in obj) +        size++; +    } +  } +  return size; +} +function includes(array, obj) { +  for ( var i = 0; i < array.length; i++) { +    if (obj === array[i]) return true; +  } +  return false; +} + +function indexOf(array, obj) { +  for ( var i = 0; i < array.length; i++) { +    if (obj === array[i]) return i; +  } +  return -1; +} + +function log(a, b, c){ +  var console = window['console']; +  switch(arguments.length) { +  case 1: +    console['log'](a); +    break; +  case 2: +    console['log'](a, b); +    break; +  default: +    console['log'](a, b, c); +    break; +  } +} + +function error(a, b, c){ +  var console = window['console']; +  switch(arguments.length) { +  case 1: +    console['error'](a); +    break; +  case 2: +    console['error'](a, b); +    break; +  default: +    console['error'](a, b, c); +    break; +  } +} + +function consoleLog(level, objs) { +  var log = document.createElement("div"); +  log.className = level; +  var msg = ""; +  var sep = ""; +  for ( var i = 0; i < objs.length; i++) { +    var obj = objs[i]; +    msg += sep + (typeof obj == 'string' ? obj : toJson(obj)); +    sep = " "; +  } +  log.appendChild(document.createTextNode(msg)); +  consoleNode.appendChild(log); +} + +function isLeafNode (node) { +  if (node) { +    switch (node.nodeName) { +    case "OPTION": +    case "PRE": +    case "TITLE": +      return true; +    } +  } +  return false; +} + +function copy(source, destination){ +  if (!destination) { +    if (source) { +      if (isArray(source)) { +        return copy(source, []); +      } else if (isObject(source)) { +        return copy(source, {}); +      } +    } +    return source; +  } else { +    if (isArray(source)) { +      while(destination.length) { +        destination.pop(); +      } +      for ( var i = 0; i < source.length; i++) { +        destination.push(copy(source[i])); +      } +    } else { +      foreach(destination, function(value, key){ +        delete destination[key]; +      }); +      for ( var key in source) { +        destination[key] = copy(source[key]); +      } +    } +    return destination; +  } +} + +function setHtml(node, html) { +  if (isLeafNode(node)) { +    if (msie) { +      node.innerText = html; +    } else { +      node.textContent = html; +    } +  } else { +    node.innerHTML = html; +  } +} + +function escapeHtml(html) { +  if (!html || !html.replace) +    return html; +  return html. +      replace(/&/g, '&'). +      replace(/</g, '<'). +      replace(/>/g, '>'); +} + + +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 elementError(element, type, error) { +  while (!isRenderableElement(element)) { +    element = element.parent() || jqLite(document.body); +  } +  if (element[0]['$NG_ERROR'] !== error) { +    element[0]['$NG_ERROR'] = error; +    if (error) { +      element.addClass(type); +      element.attr(type, error); +    } else { +      element.removeClass(type); +      element.removeAttr(type); +    } +  } +} + +function escapeAttr(html) { +  if (!html || !html.replace) +    return html; +  return html.replace(/</g, '<').replace(/>/g, '>').replace(/\"/g, +      '"'); +} + +function bind(_this, _function) { +  if (!isFunction(_function)) +    throw "Not a function!"; +  var curryArgs = slice.call(arguments, 2, arguments.length); +  return function() { +    return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); +  }; +} + +function outerHTML(node) { +  var temp = document.createElement('div'); +  temp.appendChild(node); +  var outerHTML = temp.innerHTML; +  temp.removeChild(node); +  return outerHTML; +} + +function toBoolean(value) { +  if (value && value.length !== 0) { +    var v = lowercase("" + value); +    value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == '[]'); +  } else { +    value = false; +  } +  return value; +} + +function merge(src, dst) { +  for ( var key in src) { +    var value = dst[key]; +    var type = typeof value; +    if (type == 'undefined') { +      dst[key] = fromJson(toJson(src[key])); +    } else if (type == 'object' && value.constructor != array && +        key.substring(0, 1) != "$") { +      merge(src[key], value); +    } +  } +} + +function compile(element, parentScope, overrides) { +  var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget), +      $element = jqLite(element), +      parent = extend({}, parentScope); +  parent.$element = $element; +  return compiler.compile($element)($element, parent, overrides); +} +///////////////////////////////////////////////// + +function parseKeyValue(keyValue) { +  var obj = {}, key_value, key; +  foreach((keyValue || "").split('&'), function(keyValue){ +    if (keyValue) { +      key_value = keyValue.split('='); +      key = decodeURIComponent(key_value[0]); +      obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true; +    } +  }); +  return obj; +} + +function toKeyValue(obj) { +  var parts = []; +  foreach(obj, function(value, key){ +    parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); +  }); +  return parts.length ? parts.join('&') : ''; +} + +function angularInit(config){ +  if (config.autobind) { +    var scope = compile(window.document, null, {'$config':config}); +    // TODO default to the source of angular.js +    scope.$browser.addCss('css/angular.css'); +    scope.$init(); +  } +} + +function angularJsConfig(document) { +  var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/, +      scripts = document.getElementsByTagName("SCRIPT"), +      match; +  for(var j = 0; j < scripts.length; j++) { +    match = (scripts[j].src || "").match(filename); +    if (match) { +      return match[5]; +    } +  } +  return ""; +} diff --git a/src/AngularPublic.js b/src/AngularPublic.js new file mode 100644 index 00000000..7230c3e5 --- /dev/null +++ b/src/AngularPublic.js @@ -0,0 +1,29 @@ +var browserSingleton; +angularService('$browser', function browserFactory(){ +  if (!browserSingleton) { +    browserSingleton = new Browser(window.location, window.document); +    browserSingleton.startUrlWatcher(); +    browserSingleton.bind(); +  } +  return browserSingleton; +}); + +extend(angular, { +  'element': jqLite, +  'compile': compile, +  'scope': createScope, +  'copy': copy, +  'extend': extend, +  'foreach': foreach, +  'noop':noop, +  'bind':bind, +  'identity':identity, +  'isUndefined': isUndefined, +  'isDefined': isDefined, +  'isString': isString, +  'isFunction': isFunction, +  'isObject': isObject, +  'isNumber': isNumber, +  'isArray': isArray +}); + diff --git a/src/Browser.js b/src/Browser.js new file mode 100644 index 00000000..0552b3ae --- /dev/null +++ b/src/Browser.js @@ -0,0 +1,130 @@ +////////////////////////////// +// Browser +////////////////////////////// + +function Browser(location, document) { +  this.delay = 50; +  this.expectedUrl = location.href; +  this.urlListeners = []; +  this.hoverListener = noop; +  this.isMock = false; +  this.outstandingRequests = { count: 0, callbacks:[]}; + +  this.XHR = window.XMLHttpRequest || function () { +    try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} +    try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} +    try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} +    throw new Error("This browser does not support XMLHttpRequest."); +  }; +  this.setTimeout = function(fn, delay) { +   window.setTimeout(fn, delay); +  }; + +  this.location = location; +  this.document = jqLite(document); +  this.body = jqLite(document.body); +} + +Browser.prototype = { + +  bind: function() { +    var self = this; +    self.document.bind("mouseover", function(event){ +      self.hoverListener(jqLite(msie ? event.srcElement : event.target), true); +      return true; +    }); +    self.document.bind("mouseleave mouseout click dblclick keypress keyup", function(event){ +      self.hoverListener(jqLite(event.target), false); +      return true; +    }); +  }, + +  hover: function(hoverListener) { +    this.hoverListener = hoverListener; +  }, + +  addCss: function(url) { +    var doc = this.document[0], +        head = jqLite(doc.getElementsByTagName('head')[0]), +        link = jqLite(doc.createElement('link')); +    link.attr('rel', 'stylesheet'); +    link.attr('type', 'text/css'); +    link.attr('href', url); +    head.append(link); +  }, + +  xhr: function(method, url, post, callback){ +    if (isFunction(post)) { +      callback = post; +      post = null; +    } +    var xhr = new this.XHR(), +        self = this; +    xhr.open(method, url, true); +    this.outstandingRequests.count ++; +    xhr.onreadystatechange = function() { +      if (xhr.readyState == 4) { +        try { +          callback(xhr.status || 200, xhr.responseText); +        } finally { +          self.outstandingRequests.count--; +          self.processRequestCallbacks(); +        } +      } +    }; +    xhr.send(post || ''); +  }, + +  processRequestCallbacks: function(){ +    if (this.outstandingRequests.count === 0) { +      while(this.outstandingRequests.callbacks.length) { +        try { +          this.outstandingRequests.callbacks.pop()(); +        } catch (e) { +        } +      } +    } +  }, + +  notifyWhenNoOutstandingRequests: function(callback){ +    if (this.outstandingRequests.count === 0) { +      callback(); +    } else { +      this.outstandingRequests.callbacks.push(callback); +    } +  }, + +  watchUrl: function(fn){ +    this.urlListeners.push(fn); +  }, + +  startUrlWatcher: function() { +   var self = this; +   (function pull () { +     if (self.expectedUrl !== self.location.href) { +       foreach(self.urlListeners, function(listener){ +         try { +           listener(self.location.href); +         } catch (e) { +           error(e); +         } +       }); +       self.expectedUrl = self.location.href; +     } +     self.setTimeout(pull, self.delay); +   })(); +  }, + +  setUrl: function(url) { +   var existingURL = this.location.href; +   if (!existingURL.match(/#/)) existingURL += '#'; +   if (!url.match(/#/)) url += '#'; +   if (existingURL != url) { +     this.location.href = this.expectedUrl = url; +   } +  }, + +  getUrl: function() { +   return this.location.href; +  } +}; diff --git a/src/Compiler.js b/src/Compiler.js new file mode 100644 index 00000000..c8910c27 --- /dev/null +++ b/src/Compiler.js @@ -0,0 +1,212 @@ +/** + * Template provides directions an how to bind to a given element. + * It contains a list of init functions which need to be called to + * bind to a new instance of elements. It also provides a list + * of child paths which contain child templates + */ +function Template(priority) { +  this.paths = []; +  this.children = []; +  this.inits = []; +  this.priority = priority || 0; +} + +Template.prototype = { +  init: function(element, scope) { +    var inits = {}; +    this.collectInits(element, inits); +    foreachSorted(inits, function(queue){ +      foreach(queue, function(fn){ +        fn(scope); +      }); +    }); +  }, + +  collectInits: function(element, inits) { +    var queue = inits[this.priority]; +    if (!queue) { +      inits[this.priority] = queue = []; +    } +    element = jqLite(element); +    foreach(this.inits, function(fn) { +      queue.push(function(scope) { +        scope.$tryEval(fn, element, element); +      }); +    }); + +    var i, +        childNodes = element[0].childNodes, +        children = this.children, +        paths = this.paths, +        length = paths.length; +    for (i = 0; i < length; i++) { +      children[i].collectInits(childNodes[paths[i]], inits); +    } +  }, + + +  addInit:function(init) { +    if (init) { +      this.inits.push(init); +    } +  }, + + +  addChild: function(index, template) { +    if (template) { +      this.paths.push(index); +      this.children.push(template); +    } +  }, + +  empty: function() { +    return this.inits.length === 0 && this.paths.length === 0; +  } +}; + +/////////////////////////////////// +//Compiler +////////////////////////////////// +function Compiler(textMarkup, attrMarkup, directives, widgets){ +  this.textMarkup = textMarkup; +  this.attrMarkup = attrMarkup; +  this.directives = directives; +  this.widgets = widgets; +} + +Compiler.prototype = { +  compile: function(rawElement) { +    rawElement = jqLite(rawElement); +    var index = 0, +        template, +        parent = rawElement.parent(); +    if (parent && parent[0]) { +      parent = parent[0]; +      for(var i = 0; i < parent.childNodes.length; i++) { +        if (parent.childNodes[i] == rawElement[0]) { +          index = i; +        } +      } +    } +    template = this.templatize(rawElement, index, 0) || new Template(); +    return function(element, parentScope){ +      element = jqLite(element); +      var scope = parentScope && parentScope.$eval ? +          parentScope : +          createScope(parentScope || {}, angularService); +      return extend(scope, { +        $element:element, +        $init: function() { +          template.init(element, scope); +          scope.$eval(); +          delete scope.$init; +          return scope; +        } +      }); +    }; +  }, + +  templatize: function(element, elementIndex, priority){ +    var self = this, +        widget, +        directiveFns = self.directives, +        descend = true, +        directives = true, +        template, +        selfApi = { +          compile: bind(self, self.compile), +          comment:function(text) {return jqLite(document.createComment(text));}, +          element:function(type) {return jqLite(document.createElement(type));}, +          text:function(text) {return jqLite(document.createTextNode(text));}, +          descend: function(value){ if(isDefined(value)) descend = value; return descend;}, +          directives: function(value){ if(isDefined(value)) directives = value; return directives;} +        }; +    priority = element.attr('ng-eval-order') || priority || 0; +    if (isString(priority)) { +      priority = PRIORITY[uppercase(priority)] || 0; +    } +    template = new Template(priority); +    eachAttribute(element, function(value, name){ +      if (!widget) { +        if (widget = self.widgets['@' + name]) { +          widget = bind(selfApi, widget, value, element); +        } +      } +    }); +    if (!widget) { +      if (widget = self.widgets[nodeName(element)]) { +        widget = bind(selfApi, widget, element); +      } +    } +    if (widget) { +      descend = false; +      directives = false; +      var parent = element.parent(); +      template.addInit(widget.call(selfApi, element)); +      if (parent && parent[0]) { +        element = jqLite(parent[0].childNodes[elementIndex]); +      } +    } +    if (descend){ +      // process markup for text nodes only +      eachTextNode(element, function(textNode){ +        var text = textNode.text(); +        foreach(self.textMarkup, function(markup){ +          markup.call(selfApi, text, textNode, element); +        }); +      }); +    } + +    if (directives) { +      // Process attributes/directives +      eachAttribute(element, function(value, name){ +        foreach(self.attrMarkup, function(markup){ +          markup.call(selfApi, value, name, element); +        }); +      }); +      eachAttribute(element, function(value, name){ +        template.addInit((directiveFns[name]||noop).call(selfApi, value, element)); +      }); +    } +    // Process non text child nodes +    if (descend) { +      eachNode(element, function(child, i){ +        template.addChild(i, self.templatize(child, i, priority)); +      }); +    } +    return template.empty() ? null : template; +  } +}; + +function eachTextNode(element, fn){ +  var i, chldNodes = element[0].childNodes || [], chld; +  for (i = 0; i < chldNodes.length; i++) { +    if(isTextNode(chld = chldNodes[i])) { +      fn(jqLite(chld), i); +    } +  } +} + +function eachNode(element, fn){ +  var i, chldNodes = element[0].childNodes || [], chld; +  for (i = 0; i < chldNodes.length; i++) { +    if(!isTextNode(chld = chldNodes[i])) { +      fn(jqLite(chld), i); +    } +  } +} + +function eachAttribute(element, fn){ +  var i, attrs = element[0].attributes || [], chld, attr, name, value, attrValue = {}; +  for (i = 0; i < attrs.length; i++) { +    attr = attrs[i]; +    name = attr.name.replace(':', '-'); +    value = attr.value; +    if (msie && name == 'href') { +      value = decodeURIComponent(element[0].getAttribute(name, 2)); +    } +    attrValue[name] = value; +  } +  foreachSorted(attrValue, fn); +} + diff --git a/src/JSON.js b/src/JSON.js new file mode 100644 index 00000000..340b075a --- /dev/null +++ b/src/JSON.js @@ -0,0 +1,105 @@ +array = [].constructor; + +function toJson(obj, pretty){ +  var buf = []; +  toJsonArray(buf, obj, pretty ? "\n  " : null, []); +  return buf.join(''); +} + +function toPrettyJson(obj)  { +  return toJson(obj, true); +} + +function fromJson(json) { +  if (!json) return json; +  try { +    var parser = new Parser(json, true); +    var expression =  parser.primary(); +    parser.assertAllConsumed(); +    return expression(); +  } catch (e) { +    error("fromJson error: ", json, e); +    throw e; +  } +} + +angular['toJson'] = toJson; +angular['fromJson'] = fromJson; + +function toJsonArray(buf, obj, pretty, stack){ +  if (typeof obj == "object") { +    if (includes(stack, obj)) { +      buf.push("RECURSION"); +      return; +    } +    stack.push(obj); +  } +  var type = typeof obj; +  if (obj === null) { +    buf.push("null"); +  } else if (type === 'function') { +    return; +  } else if (type === 'boolean') { +    buf.push('' + obj); +  } else if (type === 'number') { +    if (isNaN(obj)) { +      buf.push('null'); +    } else { +      buf.push('' + obj); +    } +  } else if (type === 'string') { +    return buf.push(angular['String']['quoteUnicode'](obj)); +  } else if (type === 'object') { +    if (obj instanceof Array) { +      buf.push("["); +      var len = obj.length; +      var sep = false; +      for(var i=0; i<len; i++) { +        var item = obj[i]; +        if (sep) buf.push(","); +        if (typeof item == 'function' || typeof item == 'undefined') { +          buf.push("null"); +        } else { +          toJsonArray(buf, item, pretty, stack); +        } +        sep = true; +      } +      buf.push("]"); +    } else if (obj instanceof Date) { +      buf.push(angular['String']['quoteUnicode'](angular['Date']['toString'](obj))); +    } else { +      buf.push("{"); +      if (pretty) buf.push(pretty); +      var comma = false; +      var childPretty = pretty ? pretty + "  " : false; +      var keys = []; +      for(var k in obj) { +        if (k.indexOf('$$') === 0 || obj[k] === undefined) +          continue; +        keys.push(k); +      } +      keys.sort(); +      for ( var keyIndex = 0; keyIndex < keys.length; keyIndex++) { +        var key = keys[keyIndex]; +        try { +          var value = obj[key]; +          if (typeof value != 'function') { +            if (comma) { +              buf.push(","); +              if (pretty) buf.push(pretty); +            } +            buf.push(angular['String']['quote'](key)); +            buf.push(":"); +            toJsonArray(buf, value, childPretty, stack); +            comma = true; +          } +        } catch (e) { +        } +      } +      buf.push("}"); +    } +  } +  if (typeof obj == "object") { +    stack.pop(); +  } +} diff --git a/src/Parser.js b/src/Parser.js new file mode 100644 index 00000000..df270792 --- /dev/null +++ b/src/Parser.js @@ -0,0 +1,730 @@ +function Lexer(text, parsStrings){ +  this.text = text; +  // UTC dates have 20 characters, we send them through parser +  this.dateParseLength = parsStrings ? 20 : -1; +  this.tokens = []; +  this.index = 0; +} + +Lexer.OPERATORS = { +    'null':function(self){return null;}, +    'true':function(self){return true;}, +    'false':function(self){return false;}, +    'undefined':noop, +    '+':function(self, a,b){return (isDefined(a)?a:0)+(isDefined(b)?b:0);}, +    '-':function(self, a,b){return (isDefined(a)?a:0)-(isDefined(b)?b:0);}, +    '*':function(self, a,b){return a*b;}, +    '/':function(self, a,b){return a/b;}, +    '%':function(self, a,b){return a%b;}, +    '^':function(self, a,b){return a^b;}, +    '=':function(self, a,b){return setter(self, a, b);}, +    '==':function(self, a,b){return a==b;}, +    '!=':function(self, a,b){return a!=b;}, +    '<':function(self, a,b){return a<b;}, +    '>':function(self, a,b){return a>b;}, +    '<=':function(self, a,b){return a<=b;}, +    '>=':function(self, a,b){return a>=b;}, +    '&&':function(self, a,b){return a&&b;}, +    '||':function(self, a,b){return a||b;}, +    '&':function(self, a,b){return a&b;}, +//    '|':function(self, a,b){return a|b;}, +    '|':function(self, a,b){return b(self, a);}, +    '!':function(self, a){return !a;} +}; +Lexer.ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'}; + +Lexer.prototype = { +  peek: function() { +    if (this.index + 1 < this.text.length) { +      return this.text.charAt(this.index + 1); +    } else { +      return false; +    } +  }, + +  parse: function() { +    var tokens = this.tokens; +    var OPERATORS = Lexer.OPERATORS; +    var canStartRegExp = true; +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index); +      if (ch == '"' || ch == "'") { +        this.readString(ch); +        canStartRegExp = true; +      } else if (ch == '(' || ch == '[') { +        tokens.push({index:this.index, text:ch}); +        this.index++; +      } else if (ch == '{' ) { +        var peekCh = this.peek(); +        if (peekCh == ':' || peekCh == '(') { +          tokens.push({index:this.index, text:ch + peekCh}); +          this.index++; +        } else { +          tokens.push({index:this.index, text:ch}); +        } +        this.index++; +        canStartRegExp = true; +      } else if (ch == ')' || ch == ']' || ch == '}' ) { +        tokens.push({index:this.index, text:ch}); +        this.index++; +        canStartRegExp = false; +      } else if ( ch == ':' || ch == '.' || ch == ',' || ch == ';') { +        tokens.push({index:this.index, text:ch}); +        this.index++; +        canStartRegExp = true; +      } else if ( canStartRegExp && ch == '/' ) { +        this.readRegexp(); +        canStartRegExp = false; +      } else if ( this.isNumber(ch) ) { +        this.readNumber(); +        canStartRegExp = false; +      } else if (this.isIdent(ch)) { +        this.readIdent(); +        canStartRegExp = false; +      } else if (this.isWhitespace(ch)) { +        this.index++; +      } else { +        var ch2 = ch + this.peek(); +        var fn = OPERATORS[ch]; +        var fn2 = OPERATORS[ch2]; +        if (fn2) { +          tokens.push({index:this.index, text:ch2, fn:fn2}); +          this.index += 2; +        } else if (fn) { +          tokens.push({index:this.index, text:ch, fn:fn}); +          this.index += 1; +        } else { +          throw "Lexer Error: Unexpected next character [" + +              this.text.substring(this.index) + +              "] in expression '" + this.text + +              "' at column '" + (this.index+1) + "'."; +        } +        canStartRegExp = true; +      } +    } +    return tokens; +  }, + +  isNumber: function(ch) { +    return '0' <= ch && ch <= '9'; +  }, + +  isWhitespace: function(ch) { +    return ch == ' ' || ch == '\r' || ch == '\t' || +           ch == '\n' || ch == '\v'; +  }, + +  isIdent: function(ch) { +    return 'a' <= ch && ch <= 'z' || +           'A' <= ch && ch <= 'Z' || +           '_' == ch || ch == '$'; +  }, + +  readNumber: function() { +    var number = ""; +    var start = this.index; +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index); +      if (ch == '.' || this.isNumber(ch)) { +        number += ch; +      } else { +        break; +      } +      this.index++; +    } +    number = 1 * number; +    this.tokens.push({index:start, text:number, +      fn:function(){return number;}}); +  }, + +  readIdent: function() { +    var ident = ""; +    var start = this.index; +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index); +      if (ch == '.' || this.isIdent(ch) || this.isNumber(ch)) { +        ident += ch; +      } else { +        break; +      } +      this.index++; +    } +    var fn = Lexer.OPERATORS[ident]; +    if (!fn) { +      fn = getterFn(ident); +      fn.isAssignable = ident; +    } +    this.tokens.push({index:start, text:ident, fn:fn}); +  }, + +  readString: function(quote) { +    var start = this.index; +    var dateParseLength = this.dateParseLength; +    this.index++; +    var string = ""; +    var rawString = quote; +    var escape = false; +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index); +      rawString += ch; +      if (escape) { +        if (ch == 'u') { +          var hex = this.text.substring(this.index + 1, this.index + 5); +          this.index += 4; +          string += String.fromCharCode(parseInt(hex, 16)); +        } else { +          var rep = Lexer.ESCAPE[ch]; +          if (rep) { +            string += rep; +          } else { +            string += ch; +          } +        } +        escape = false; +      } else if (ch == '\\') { +        escape = true; +      } else if (ch == quote) { +        this.index++; +        this.tokens.push({index:start, text:rawString, string:string, +          fn:function(){ +            return (string.length == dateParseLength) ? +              angular['String']['toDate'](string) : string; +          }}); +        return; +      } else { +        string += ch; +      } +      this.index++; +    } +    throw "Lexer Error: Unterminated quote [" + +        this.text.substring(start) + "] starting at column '" + +        (start+1) + "' in expression '" + this.text + "'."; +  }, + +  readRegexp: function(quote) { +    var start = this.index; +    this.index++; +    var regexp = ""; +    var escape = false; +    while (this.index < this.text.length) { +      var ch = this.text.charAt(this.index); +      if (escape) { +        regexp += ch; +        escape = false; +      } else if (ch === '\\') { +        regexp += ch; +        escape = true; +      } else if (ch === '/') { +        this.index++; +        var flags = ""; +        if (this.isIdent(this.text.charAt(this.index))) { +          this.readIdent(); +          flags = this.tokens.pop().text; +        } +        var compiledRegexp = new RegExp(regexp, flags); +        this.tokens.push({index:start, text:regexp, flags:flags, +          fn:function(){return compiledRegexp;}}); +        return; +      } else { +        regexp += ch; +      } +      this.index++; +    } +    throw "Lexer Error: Unterminated RegExp [" + +        this.text.substring(start) + "] starting at column '" + +        (start+1) + "' in expression '" + this.text + "'."; +  } +}; + +///////////////////////////////////////// + +function Parser(text, parseStrings){ +  this.text = text; +  this.tokens = new Lexer(text, parseStrings).parse(); +  this.index = 0; +} + +Parser.ZERO = function(){ +  return 0; +}; + +Parser.prototype = { +  error: function(msg, token) { +    throw "Token '" + token.text + +      "' is " + msg + " at column='" + +      (token.index + 1) + "' of expression '" + +      this.text + "' starting at '" + this.text.substring(token.index) + "'."; +  }, + +  peekToken: function() { +    if (this.tokens.length === 0) +      throw "Unexpected end of expression: " + this.text; +    return this.tokens[0]; +  }, + +  peek: function(e1, e2, e3, e4) { +    var tokens = this.tokens; +    if (tokens.length > 0) { +      var token = tokens[0]; +      var t = token.text; +      if (t==e1 || t==e2 || t==e3 || t==e4 || +          (!e1 && !e2 && !e3 && !e4)) { +        return token; +      } +    } +    return false; +  }, + +  expect: function(e1, e2, e3, e4){ +    var token = this.peek(e1, e2, e3, e4); +    if (token) { +      this.tokens.shift(); +      this.currentToken = token; +      return token; +    } +    return false; +  }, + +  consume: function(e1){ +    if (!this.expect(e1)) { +      var token = this.peek(); +      throw "Expecting '" + e1 + "' at column '" + +          (token.index+1) + "' in '" + +          this.text + "' got '" + +          this.text.substring(token.index) + "'."; +    } +  }, + +  _unary: function(fn, right) { +    return function(self) { +      return fn(self, right(self)); +    }; +  }, + +  _binary: function(left, fn, right) { +    return function(self) { +      return fn(self, left(self), right(self)); +    }; +  }, + +  hasTokens: function () { +    return this.tokens.length > 0; +  }, + +  assertAllConsumed: function(){ +    if (this.tokens.length !== 0) { +      throw "Did not understand '" + this.text.substring(this.tokens[0].index) + +          "' while evaluating '" + this.text + "'."; +    } +  }, + +  statements: function(){ +    var statements = []; +    while(true) { +      if (this.tokens.length > 0 && !this.peek('}', ')', ';', ']')) +        statements.push(this.filterChain()); +      if (!this.expect(';')) { +        return function (self){ +          var value; +          for ( var i = 0; i < statements.length; i++) { +            var statement = statements[i]; +            if (statement) +              value = statement(self); +          } +          return value; +        }; +      } +    } +  }, + +  filterChain: function(){ +    var left = this.expression(); +    var token; +    while(true) { +      if ((token = this.expect('|'))) { +        left = this._binary(left, token.fn, this.filter()); +      } else { +        return left; +      } +    } +  }, + +  filter: function(){ +    return this._pipeFunction(angularFilter); +  }, + +  validator: function(){ +    return this._pipeFunction(angularValidator); +  }, + +  _pipeFunction: function(fnScope){ +    var fn = this.functionIdent(fnScope); +    var argsFn = []; +    var token; +    while(true) { +      if ((token = this.expect(':'))) { +        argsFn.push(this.expression()); +      } else { +        var fnInvoke = function(self, input){ +          var args = [input]; +          for ( var i = 0; i < argsFn.length; i++) { +            args.push(argsFn[i](self)); +          } +          return fn.apply(self, args); +        }; +        return function(){ +          return fnInvoke; +        }; +      } +    } +  }, + +  expression: function(){ +    return this.throwStmt(); +  }, + +  throwStmt: function(){ +    if (this.expect('throw')) { +      var throwExp = this.assignment(); +      return function (self) { +        throw throwExp(self); +      }; +    } else { +     return this.assignment(); +    } +  }, + +  assignment: function(){ +    var left = this.logicalOR(); +    var token; +    if (token = this.expect('=')) { +      if (!left.isAssignable) { +        throw "Left hand side '" + +            this.text.substring(0, token.index) + "' of assignment '" + +            this.text.substring(token.index) + "' is not assignable."; +      } +      var ident = function(){return left.isAssignable;}; +      return this._binary(ident, token.fn, this.logicalOR()); +    } else { +     return left; +    } +  }, + +  logicalOR: function(){ +    var left = this.logicalAND(); +    var token; +    while(true) { +      if ((token = this.expect('||'))) { +        left = this._binary(left, token.fn, this.logicalAND()); +      } else { +        return left; +      } +    } +  }, + +  logicalAND: function(){ +    var left = this.equality(); +    var token; +    if ((token = this.expect('&&'))) { +      left = this._binary(left, token.fn, this.logicalAND()); +    } +    return left; +  }, + +  equality: function(){ +    var left = this.relational(); +    var token; +    if ((token = this.expect('==','!='))) { +      left = this._binary(left, token.fn, this.equality()); +    } +    return left; +  }, + +  relational: function(){ +    var left = this.additive(); +    var token; +    if (token = this.expect('<', '>', '<=', '>=')) { +      left = this._binary(left, token.fn, this.relational()); +    } +    return left; +  }, + +  additive: function(){ +    var left = this.multiplicative(); +    var token; +    while(token = this.expect('+','-')) { +      left = this._binary(left, token.fn, this.multiplicative()); +    } +    return left; +  }, + +  multiplicative: function(){ +    var left = this.unary(); +    var token; +    while(token = this.expect('*','/','%')) { +        left = this._binary(left, token.fn, this.unary()); +    } +    return left; +  }, + +  unary: function(){ +    var token; +    if (this.expect('+')) { +      return this.primary(); +    } else if (token = this.expect('-')) { +      return this._binary(Parser.ZERO, token.fn, this.unary()); +    } else if (token = this.expect('!')) { +      return this._unary(token.fn, this.unary()); +    } else { +     return this.primary(); +    } +  }, + +  functionIdent: function(fnScope) { +    var token = this.expect(); +    var element = token.text.split('.'); +    var instance = fnScope; +    var key; +    for ( var i = 0; i < element.length; i++) { +      key = element[i]; +      if (instance) +        instance = instance[key]; +    } +    if (typeof instance != 'function') { +      throw "Function '" + token.text + "' at column '" + +      (token.index+1)  + "' in '" + this.text + "' is not defined."; +    } +    return instance; +  }, + +  primary: function() { +    var primary; +    if (this.expect('(')) { +      var expression = this.filterChain(); +      this.consume(')'); +      primary = expression; +    } else if (this.expect('[')) { +      primary = this.arrayDeclaration(); +    } else if (this.expect('{')) { +      primary = this.object(); +    } else if (this.expect('{:')) { +      primary = this.closure(false); +    } else if (this.expect('{(')) { +      primary = this.closure(true); +    } else { +      var token = this.expect(); +      primary = token.fn; +      if (!primary) { +        this.error("not a primary expression", token); +      } +    } +    var next; +    while (next = this.expect('(', '[', '.')) { +      if (next.text === '(') { +        primary = this.functionCall(primary); +      } else if (next.text === '[') { +        primary = this.objectIndex(primary); +      } else if (next.text === '.') { +        primary = this.fieldAccess(primary); +      } else { +        throw "IMPOSSIBLE"; +      } +    } +    return primary; +  }, + +  closure: function(hasArgs) { +    var args = []; +    if (hasArgs) { +      if (!this.expect(')')) { +        args.push(this.expect().text); +        while(this.expect(',')) { +          args.push(this.expect().text); +        } +        this.consume(')'); +      } +      this.consume(":"); +    } +    var statements = this.statements(); +    this.consume("}"); +    return function(self) { +      return function($){ +        var scope = createScope(self); +        scope['$'] = $; +        for ( var i = 0; i < args.length; i++) { +          setter(scope, args[i], arguments[i]); +        } +        return statements(scope); +      }; +    }; +  }, + +  fieldAccess: function(object) { +    var field = this.expect().text; +    var getter = getterFn(field); +    var fn = function (self){ +      return getter(object(self)); +    }; +    fn.isAssignable = field; +    return fn; +  }, + +  objectIndex: function(obj) { +    var indexFn = this.expression(); +    this.consume(']'); +    if (this.expect('=')) { +      var rhs = this.expression(); +      return function (self){ +        return obj(self)[indexFn(self)] = rhs(self); +      }; +    } else { +      return function (self){ +        var o = obj(self); +        var i = indexFn(self); +        return (o) ? o[i] : undefined; +      }; +    } +  }, + +  functionCall: function(fn) { +    var argsFn = []; +    if (this.peekToken().text != ')') { +      do { +        argsFn.push(this.expression()); +      } while (this.expect(',')); +    } +    this.consume(')'); +    return function (self){ +      var args = []; +      for ( var i = 0; i < argsFn.length; i++) { +        args.push(argsFn[i](self)); +      } +      var fnPtr = fn(self); +      if (typeof fnPtr === 'function') { +        return fnPtr.apply(self, args); +      } else { +        throw "Expression '" + fn.isAssignable + "' is not a function."; +      } +    }; +  }, + +  // This is used with json array declaration +  arrayDeclaration: function () { +    var elementFns = []; +    if (this.peekToken().text != ']') { +      do { +        elementFns.push(this.expression()); +      } while (this.expect(',')); +    } +    this.consume(']'); +    return function (self){ +      var array = []; +      for ( var i = 0; i < elementFns.length; i++) { +        array.push(elementFns[i](self)); +      } +      return array; +    }; +  }, + +  object: function () { +    var keyValues = []; +    if (this.peekToken().text != '}') { +      do { +        var token = this.expect(), +            key = token.string || token.text; +        this.consume(":"); +        var value = this.expression(); +        keyValues.push({key:key, value:value}); +      } while (this.expect(',')); +    } +    this.consume('}'); +    return function (self){ +      var object = {}; +      for ( var i = 0; i < keyValues.length; i++) { +        var keyValue = keyValues[i]; +        var value = keyValue.value(self); +        object[keyValue.key] = value; +      } +      return object; +    }; +  }, + +  entityDeclaration: function () { +    var decl = []; +    while(this.hasTokens()) { +      decl.push(this.entityDecl()); +      if (!this.expect(';')) { +        this.assertAllConsumed(); +      } +    } +    return function (self){ +      var code = ""; +      for ( var i = 0; i < decl.length; i++) { +        code += decl[i](self); +      } +      return code; +    }; +  }, + +  entityDecl: function () { +    var entity = this.expect().text; +    var instance; +    var defaults; +    if (this.expect('=')) { +      instance = entity; +      entity = this.expect().text; +    } +    if (this.expect(':')) { +      defaults = this.primary()(null); +    } +    return function(self) { +      var Entity = self.datastore.entity(entity, defaults); +      setter(self, entity, Entity); +      if (instance) { +        var document = Entity(); +        document['$$anchor'] = instance; +        setter(self, instance, document); +        return "$anchor." + instance + ":{" + +            instance + "=" + entity + ".load($anchor." + instance + ");" + +            instance + ".$$anchor=" + angular['String']['quote'](instance) + ";" + +          "};"; +      } else { +        return ""; +      } +    }; +  }, + +  watch: function () { +    var decl = []; +    while(this.hasTokens()) { +      decl.push(this.watchDecl()); +      if (!this.expect(';')) { +        this.assertAllConsumed(); +      } +    } +    this.assertAllConsumed(); +    return function (self){ +      for ( var i = 0; i < decl.length; i++) { +        var d = decl[i](self); +        self.addListener(d.name, d.fn); +      } +    }; +  }, + +  watchDecl: function () { +    var anchorName = this.expect().text; +    this.consume(":"); +    var expression; +    if (this.peekToken().text == '{') { +      this.consume("{"); +      expression = this.statements(); +      this.consume("}"); +    } else { +      expression = this.expression(); +    } +    return function(self) { +      return {name:anchorName, fn:expression}; +    }; +  } +}; + diff --git a/src/Resource.js b/src/Resource.js new file mode 100644 index 00000000..ba460c30 --- /dev/null +++ b/src/Resource.js @@ -0,0 +1,140 @@ +function Route(template, defaults) { +  this.template = template = template + '#'; +  this.defaults = defaults || {}; +  var urlParams = this.urlParams = {}; +  foreach(template.split(/\W/), function(param){ +    if (param && template.match(new RegExp(":" + param + "\\W"))) { +      urlParams[param] = true; +    } +  }); +} + +Route.prototype = { +  url: function(params) { +    var path = []; +    var self = this; +    var url = this.template; +    params = params || {}; +    foreach(this.urlParams, function(_, urlParam){ +      var value = params[urlParam] || self.defaults[urlParam] || ""; +      url = url.replace(new RegExp(":" + urlParam + "(\\W)"), value + "$1"); +    }); +    url = url.replace(/\/?#$/, ''); +    var query = []; +    foreachSorted(params, function(value, key){ +      if (!self.urlParams[key]) { +        query.push(encodeURI(key) + '=' + encodeURI(value)); +      } +    }); +    return url + (query.length ? '?' + query.join('&') : ''); +  } +}; + +function ResourceFactory(xhr) { +  this.xhr = xhr; +} + +ResourceFactory.DEFAULT_ACTIONS = { +  'get':    {method:'GET'}, +  'save':   {method:'POST'}, +  'query':  {method:'GET', isArray:true}, +  'remove': {method:'DELETE'}, +  'delete': {method:'DELETE'} +}; + +ResourceFactory.prototype = { +  route: function(url, paramDefaults, actions){ +    var self = this; +    var route = new Route(url); +    actions = extend({}, ResourceFactory.DEFAULT_ACTIONS, actions); +    function extractParams(data){ +      var ids = {}; +      foreach(paramDefaults || {}, function(value, key){ +        ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value; +      }); +      return ids; +    } + +    function Resource(value){ +      copy(value || {}, this); +    } + +    foreach(actions, function(action, name){ +      var isGet = action.method == 'GET'; +      var isPost = action.method == 'POST'; +      Resource[name] = function (a1, a2, a3) { +        var params = {}; +        var data; +        var callback = noop; +        switch(arguments.length) { +        case 3: callback = a3; +        case 2: +          if (isFunction(a2)) { +            callback = a2; +          } else { +            params = a1; +            data = a2; +            break; +          } +        case 1: +          if (isFunction(a1)) callback = a1; +          else if (isPost) data = a1; +          else params = a1; +          break; +        case 0: break; +        default: +          throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; +        } + +        var value = action.isArray ? [] : new Resource(data;) +        self.xhr( +          action.method, +          route.url(extend({}, action.params || {}, extractParams(data), params)), +          data, +          function(status, response, clear) { +            if (status == 200) { +              if (action.isArray) { +                if (action.cacheThenRetrieve) +                  value = []; +                foreach(response, function(item){ +                  value.push(new Resource(item)); +                }); +              } else { +                copy(response, value); +              } +              (callback||noop)(value); +            } else { +              throw {status: status, response:response, message: status + ": " + response}; +            } +          }, +          action.cacheThenRetrieve +        ); +        return value; +      }; + +      Resource.bind = function(additionalParamDefaults){ +        return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); +      }; + +      if (!isGet) { +        Resource.prototype['$' + name] = function(a1, a2){ +          var params = {}; +          var callback = noop; +          switch(arguments.length) { +          case 2: params = a1; callback = a2; +          case 1: if (typeof a1 == 'function') callback = a1; else params = a1; +          case 0: break; +          default: +            throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; +          } +          var self = this; +          Resource[name](params, this, function(response){ +            copy(response, self); +            callback(self); +          }); +        }; +      } +    }); +    return Resource; +  } +}; diff --git a/src/Scope.js b/src/Scope.js new file mode 100644 index 00000000..637fc25e --- /dev/null +++ b/src/Scope.js @@ -0,0 +1,224 @@ +function getter(instance, path, unboundFn) { +  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(lastInstance, fn, lastInstance); +        return instance; +      } +    } +  } +  if (!unboundFn && isFunction(instance) && !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 getterFnCache = {}; +function getterFn(path){ +  var fn = getterFnCache[path]; +  if (fn) return fn; + +  var code = 'function (self){\n'; +  code += '  var last, fn, type;\n'; +  foreach(path.split('.'), function(key) { +    key = (key == 'this')  ? '["this"]' : '.' + key; +    code += '  if(!self) return self;\n'; +    code += '  last = self;\n'; +    code += '  self = self' + key + ';\n'; +    code += '  if(typeof self == "function") \n'; +    code += '    self = function(){ return last'+key+'.apply(last, arguments); };\n'; +    if (key.charAt(1) == '$') { +      // special code for super-imposed functions +      var name = key.substr(2); +      code += '  if(!self) {\n'; +      code += '    type = angular.Global.typeOf(last);\n'; +      code += '    fn = (angular[type.charAt(0).toUpperCase() + type.substring(1)]||{})["' + name + '"];\n'; +      code += '    if (fn)\n'; +      code += '      self = function(){ return fn.apply(last, [last].concat(slice.call(arguments, 0, arguments.length))); };\n'; +      code += '  }\n'; +    } +  }); +  code += '  return self;\n}'; +  fn = eval('(' + code + ')'); +  fn.toString = function(){ return code; }; + +  return getterFnCache[path] = fn; +} + +/////////////////////////////////// + +var compileCache = {}; +function expressionCompile(exp){ +  if (isFunction(exp)) return exp; +  var fn = compileCache[exp]; +  if (!fn) { +    var parser = new Parser(exp); +    var fnSelf = parser.statements(); +    parser.assertAllConsumed(); +    fn = compileCache[exp] = extend( +      function(){ return fnSelf(this);}, +      {fnSelf: fnSelf}); +  } +  return fn; +} + +function rethrow(e) { throw e; } +function errorHandlerFor(element, error) { +  elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error); +} + +var scopeId = 0; +function createScope(parent, services, existing) { +  function Parent(){} +  function API(){} +  function Behavior(){} + +  var instance, behavior, api, evalLists = {sorted:[]}, servicesCache = extend({}, existing); + +  parent = Parent.prototype = (parent || {}); +  api = API.prototype = new Parent(); +  behavior = Behavior.prototype = new API(); +  instance = new Behavior(); + +  extend(api, { +    'this': instance, +    $id: (scopeId++), +    $parent: parent, +    $bind: bind(instance, bind, instance), +    $get: bind(instance, getter, instance), +    $set: bind(instance, setter, instance), + +    $eval: function $eval(exp) { +      if (exp !== undefined) { +        return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length)); +      } else { +        for ( var i = 0, iSize = evalLists.sorted.length; i < iSize; i++) { +          for ( var queue = evalLists.sorted[i], +                    jSize = queue.length, +                    j= 0; j < jSize; j++) { +            instance.$tryEval(queue[j].fn, queue[j].handler); +          } +        } +      } +    }, + +    $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); +        } else if (isFunction(instance.$exceptionHandler)) { +          instance.$exceptionHandler(e); +        } +      } +    }, + +    $watch: function(watchExp, listener, exceptionHandler) { +      var watch = expressionCompile(watchExp), +          last; +      function watcher(){ +        var value = watch.call(instance), +            lastValue = last; +        if (last !== value) { +          last = value; +          instance.$tryEval(listener, exceptionHandler, value, lastValue); +        } +      } +      instance.$onEval(PRIORITY_WATCH, watcher); +      watcher(); +    }, + +    $onEval: function(priority, expr, exceptionHandler){ +      if (!isNumber(priority)) { +        exceptionHandler = expr; +        expr = priority; +        priority = 0; +      } +      var evalList = evalLists[priority]; +      if (!evalList) { +        evalList = evalLists[priority] = []; +        evalList.priority = priority; +        evalLists.sorted.push(evalList); +        evalLists.sorted.sort(function(a,b){return a.priority-b.priority;}); +      } +      evalList.push({ +        fn: expressionCompile(expr), +        handler: exceptionHandler +      }); +    }, + +    $become: function(Class) { +      // remove existing +      foreach(behavior, function(value, key){ delete behavior[key]; }); +      foreach((Class || noop).prototype, function(fn, name){ +        behavior[name] = bind(instance, fn); +      }); +      (Class || noop).call(instance); +    } + +  }); + +  if (!parent.$root) { +    api.$root = instance; +    api.$parent = instance; +  } + +  function inject(name){ +    var service = servicesCache[name], factory, args = []; +    if (isUndefined(service)) { +      factory = services[name]; +      if (!isFunction(factory)) +        throw "Don't know how to inject '" + name + "'."; +      foreach(factory.inject, function(dependency){ +        args.push(inject(dependency)); +      }); +      servicesCache[name] = service = factory.apply(instance, args); +    } +    return service; +  } + +  foreach(services, function(_, name){ +    var service = inject(name); +    if (service) { +      setter(instance, name, service); +    } +  }); + +  return instance; +} diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js new file mode 100644 index 00000000..90e1104e --- /dev/null +++ b/src/angular-bootstrap.js @@ -0,0 +1,70 @@ +/** + * 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 + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(previousOnLoad){ +  var filename = /(.*)\/angular-(.*).js(#(.*))?/, +      scripts = document.getElementsByTagName("SCRIPT"), +      serverPath, +      config, +      match; +  for(var j = 0; j < scripts.length; j++) { +    match = (scripts[j].src || "").match(filename); +    if (match) { +      serverPath = match[1]; +      config = match[4]; +    } +  } + +  function addScript(file){ +    document.write('<script type="text/javascript" src="' + serverPath + file +'"></script>'); +  } + +  addScript("/Angular.js"); +  addScript("/JSON.js"); +  addScript("/Compiler.js"); +  addScript("/Scope.js"); +  addScript("/jqLite.js"); +  addScript("/Parser.js"); +  addScript("/Resource.js"); +  addScript("/Browser.js"); +  addScript("/AngularPublic.js"); + +  // Extension points +  addScript("/services.js"); +  addScript("/apis.js"); +  addScript("/filters.js"); +  addScript("/formatters.js"); +  addScript("/validators.js"); +  addScript("/directives.js"); +  addScript("/markups.js"); +  addScript("/widgets.js"); + +  window.onload = function(){ +    try { +      if (previousOnLoad) previousOnLoad(); +    } catch(e) {} +    angularInit(parseKeyValue(config)); +  }; + +})(window.onload); + diff --git a/src/angular.prefix b/src/angular.prefix new file mode 100644 index 00000000..a1b4e151 --- /dev/null +++ b/src/angular.prefix @@ -0,0 +1,24 @@ +/** + * 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 + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window, document, previousOnLoad){ diff --git a/src/angular.suffix b/src/angular.suffix new file mode 100644 index 00000000..36d73df2 --- /dev/null +++ b/src/angular.suffix @@ -0,0 +1,9 @@ + +  window.onload = function(){ +    try { +      if (previousOnLoad) previousOnLoad(); +    } catch(e) {} +    angularInit(parseKeyValue(angularJsConfig(document))); +  }; + +})(window, document, window.onload); diff --git a/src/apis.js b/src/apis.js new file mode 100644 index 00000000..306d0ce8 --- /dev/null +++ b/src/apis.js @@ -0,0 +1,338 @@ +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 = { +  'size': size +}; +var angularObject = { +  'extend': extend +}; +var angularArray = { +  'indexOf': indexOf, +  'include': includes, +  '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); +    foreach(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 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), count = 0; +    foreach(array, function(value){ +      if (fn(value)) { +        count ++; +      } +    }); +    return count; +  }, +  '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 = $ ? expressionCompile($).fnSelf : 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; +    }; +    var arrayCopy = []; +    for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } +    return arrayCopy.sort(reverse(comparator, descend)); +  }, +  'orderByToggle':function(predicate, attribute) { +    var STRIP = /^([+|-])?(.*)/; +    var ascending = false; +    var index = -1; +    foreach(predicate, function($, i){ +      if (index == -1) { +        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){ +      return expressionCompile(expression).fnSelf; +    } else { +      return identity; +    } +  } +}; + +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('<select/>').append(jQuery(node).clone()).html(); +      if (!html.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\s*option\s*>/gi)) { +        if (Binder.hasBinding(node.text)) { +          jQuery(node).attr('ng-bind-attr', angular.toJson({'value':node.text})); +        } else { +          node.value = node.text; +        } +      } +    } + +    var children = node.childNodes; +    for (var k = 0; k < children.length; k++) { +      this.precompileNode(children[k], path.concat(k), factories); +    } +  }, + +  ng_eval: function(node) { +    return new EvalUpdater(node, node.getAttribute('ng-eval')); +  }, + +  ng_bind: function(node) { +    return new BindUpdater(node, "{{" + node.getAttribute('ng-bind') + "}}"); +  }, + +  ng_bind_attr: function(node) { +    return new BindAttrUpdater(node, fromJson(node.getAttribute('ng-bind-attr'))); +  }, + +  ng_hide: function(node) { +    return new HideUpdater(node, node.getAttribute('ng-hide')); +  }, + +  ng_show: function(node) { +    return new ShowUpdater(node, node.getAttribute('ng-show')); +  }, + +  ng_class: function(node) { +    return new ClassUpdater(node, node.getAttribute('ng-class')); +  }, + +  ng_class_even: function(node) { +    return new ClassEvenUpdater(node, node.getAttribute('ng-class-even')); +  }, + +  ng_class_odd: function(node) { +    return new ClassOddUpdater(node, node.getAttribute('ng-class-odd')); +  }, + +  ng_style: function(node) { +    return new StyleUpdater(node, node.getAttribute('ng-style')); +  }, + +  ng_watch: function(node, scope) { +    scope.watch(node.getAttribute('ng-watch')); +  } +}; diff --git a/src/delete/Model.js b/src/delete/Model.js new file mode 100644 index 00000000..b09efd0e --- /dev/null +++ b/src/delete/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/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..96b63793 --- /dev/null +++ b/src/delete/Widgets.js @@ -0,0 +1,806 @@ +function WidgetFactory(serverUrl, database) { +  this.nextUploadId = 0; +  this.serverUrl = serverUrl; +  this.database = database; +  if (window['swfobject']) { +    this.createSWF = window['swfobject']['createSWF']; +  } else { +    this.createSWF = function(){ +      alert("ERROR: swfobject not loaded!"); +    }; +  } +}; + +WidgetFactory.prototype = { +  createController: function(input, scope) { +    var controller; +    var type = input.attr('type').toLowerCase(); +    var exp = input.attr('name'); +    if (exp) exp = exp.split(':').pop(); +    var event = "change"; +    var bubbleEvent = true; +    var formatter = angularFormatter[input.attr('ng-format')] || angularFormatter['noop']; +    if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') { +      controller = new ButtonController(input[0], exp, formatter); +      event = "click"; +      bubbleEvent = false; +    } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') { +      controller = new TextController(input[0], exp, formatter); +      event = "keyup change"; +    } else if (type == 'checkbox') { +      controller = new CheckboxController(input[0], exp, formatter); +      event = "click"; +    } else if (type == 'radio') { +      controller = new RadioController(input[0], exp, formatter); +      event="click"; +    } else if (type == 'select-one') { +      controller = new SelectController(input[0], exp, formatter); +    } else if (type == 'select-multiple') { +      controller = new MultiSelectController(input[0], exp, formatter); +    } else if (type == 'file') { +      controller = this.createFileController(input, exp, formatter); +    } else { +      throw 'Unknown type: ' + type; +    } +    input.data('controller', controller); +    var updateView = scope.get('$updateView'); +    var action = function() { +      if (controller.updateModel(scope)) { +        var action = jQuery(controller.view).attr('ng-action') || ""; +        if (scope.evalWidget(controller, action)) { +          updateView(scope); +        } +      } +      return bubbleEvent; +    }; +    jQuery(controller.view, ":input"). +      bind(event, action); +    return controller; +  }, + +  createFileController: function(fileInput) { +    var uploadId = '__uploadWidget_' + (this.nextUploadId++); +    var view = FileController.template(uploadId); +    fileInput.after(view); +    var att = { +        'data':this.serverUrl + "/admin/ServerAPI.swf", +        'width':"95", 'height':"20", 'align':"top", +        'wmode':"transparent"}; +    var par = { +        'flashvars':"uploadWidgetId=" + uploadId, +        'allowScriptAccess':"always"}; +    var swfNode = this.createSWF(att, par, uploadId); +    fileInput.remove(); +    var cntl = new FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database); +    jQuery(swfNode).parent().data('controller', cntl); +    return cntl; +  } +}; +///////////////////// +// FileController +/////////////////////// + +function FileController(view, scopeName, uploader, databaseUrl) { +  this.view = view; +  this.uploader = uploader; +  this.scopeName = scopeName; +  this.attachmentsPath = databaseUrl + '/_attachments'; +  this.value = null; +  this.lastValue = undefined; +}; + +angularCallbacks['flashEvent'] = function(id, event, args) { +  var object = document.getElementById(id); +  var jobject = jQuery(object); +  var controller = jobject.parent().data("controller"); +  FileController.prototype[event].apply(controller, args); +  _.defer(jobject.scope().get('$updateView')); +}; + +FileController.template = function(id) { +  return jQuery('<span class="ng-upload-widget">' + +      '<input type="checkbox" ng-non-bindable="true"/>' + +      '<object id="' + id + '" />' + +      '<a></a>' + +      '<span/>' + +    '</span>'); +}; + +extend(FileController.prototype, { +  'cancel': noop, +  'complete': noop, +  'httpStatus': function(status) { +    alert("httpStatus:" + this.scopeName + " status:" + status); +  }, +  'ioError': function() { +    alert("ioError:" + this.scopeName); +  }, +  'open': function() { +    alert("open:" + this.scopeName); +  }, +  'progress':noop, +  'securityError':  function() { +    alert("securityError:" + this.scopeName); +  }, +  'uploadCompleteData': function(data) { +    var value = fromJson(data); +    value.url = this.attachmentsPath + '/' + value.id + '/' + value.text; +    this.view.find("input").attr('checked', true); +    var scope = this.view.scope(); +    this.value = value; +    this.updateModel(scope); +    this.value = null; +  }, +  'select': function(name, size, type) { +    this.name = name; +    this.view.find("a").text(name).attr('href', name); +    this.view.find("span").text(angular['filter']['bytes'](size)); +    this.upload(); +  }, + +  updateModel: function(scope) { +    var isChecked = this.view.find("input").attr('checked'); +    var value = isChecked ? this.value : null; +    if (this.lastValue === value) { +      return false; +    } else { +      scope.set(this.scopeName, value); +      return true; +    } +  }, + +  updateView: function(scope) { +    var modelValue = scope.get(this.scopeName); +    if (modelValue && this.value !== modelValue) { +      this.value = modelValue; +      this.view.find("a"). +        attr("href", this.value.url). +        text(this.value.text); +      this.view.find("span").text(angular['filter']['bytes'](this.value.size)); +    } +    this.view.find("input").attr('checked', !!modelValue); +  }, + +  upload: function() { +    if (this.name) { +      this.uploader['uploadFile'](this.attachmentsPath); +    } +  } +}); + +/////////////////////// +// NullController +/////////////////////// +function NullController(view) {this.view = view;}; +NullController.prototype = { +  updateModel: function() { return true; }, +  updateView: noop +}; +NullController.instance = new NullController(); + + +/////////////////////// +// ButtonController +/////////////////////// +var ButtonController = NullController; + +/////////////////////// +// TextController +/////////////////////// +function TextController(view, exp, formatter) { +  this.view = view; +  this.formatter = formatter; +  this.exp = exp; +  this.validator = view.getAttribute('ng-validate'); +  this.required = typeof view.attributes['ng-required'] != "undefined"; +  this.lastErrorText = null; +  this.lastValue = undefined; +  this.initialValue = this.formatter['parse'](view.value); +  var widget = view.getAttribute('ng-widget'); +  if (widget === 'datepicker') { +    jQuery(view).datepicker(); +  } +}; + +TextController.prototype = { +  updateModel: function(scope) { +    var value = this.formatter['parse'](this.view.value); +    if (this.lastValue === value) { +      return false; +    } else { +      scope.setEval(this.exp, value); +      this.lastValue = value; +      return true; +    } +  }, + +  updateView: function(scope) { +    var view = this.view; +    var value = scope.get(this.exp); +    if (typeof value === "undefined") { +      value = this.initialValue; +      scope.setEval(this.exp, value); +    } +    value = value ? value : ''; +    if (!_(this.lastValue).isEqual(value)) { +      view.value = this.formatter['format'](value); +      this.lastValue = value; +    } + +    var isValidationError = false; +    view.removeAttribute('ng-error'); +    if (this.required) { +      isValidationError = !(value && $.trim("" + value).length > 0); +    } +    var errorText = isValidationError ? "Required Value" : null; +    if (!isValidationError && this.validator && value) { +      errorText = scope.validate(this.validator, value, view); +      isValidationError = !!errorText; +    } +    if (this.lastErrorText !== errorText) { +      this.lastErrorText = isValidationError; +      if (errorText && isVisible(view)) { +        view.setAttribute('ng-error', errorText); +        scope.markInvalid(this); +      } +      jQuery(view).toggleClass('ng-validation-error', isValidationError); +    } +  } +}; + +/////////////////////// +// CheckboxController +/////////////////////// +function CheckboxController(view, exp, formatter) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.formatter = formatter; +  this.initialValue = this.formatter['parse'](view.checked ? view.value : ""); +}; + +CheckboxController.prototype = { +  updateModel: function(scope) { +    var input = this.view; +    var value = input.checked ? input.value : ''; +    value = this.formatter['parse'](value); +    value = this.formatter['format'](value); +    if (this.lastValue === value) { +      return false; +    } else { +      scope.setEval(this.exp, this.formatter['parse'](value)); +      this.lastValue = value; +      return true; +    } +  }, + +  updateView: function(scope) { +    var input = this.view; +    var value = scope.eval(this.exp); +    if (typeof value === "undefined") { +      value = this.initialValue; +      scope.setEval(this.exp, value); +    } +    input.checked = this.formatter['parse'](input.value) == value; +  } +}; + +/////////////////////// +// SelectController +/////////////////////// +function SelectController(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.initialValue = view.value; +}; + +SelectController.prototype = { +  updateModel: function(scope) { +    var input = this.view; +    if (input.selectedIndex < 0) { +      scope.setEval(this.exp, null); +    } else { +      var value = this.view.value; +      if (this.lastValue === value) { +        return false; +      } else { +        scope.setEval(this.exp, value); +        this.lastValue = value; +        return true; +      } +    } +  }, + +  updateView: function(scope) { +    var input = this.view; +    var value = scope.get(this.exp); +    if (typeof value === 'undefined') { +      value = this.initialValue; +      scope.setEval(this.exp, value); +    } +    if (value !== this.lastValue) { +      input.value = value ? value : ""; +      this.lastValue = value; +    } +  } +}; + +/////////////////////// +// MultiSelectController +/////////////////////// +function MultiSelectController(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.initialValue = this.selected(); +}; + +MultiSelectController.prototype = { +  selected: function () { +    var value = []; +    var options = this.view.options; +    for ( var i = 0; i < options.length; i++) { +      var option = options[i]; +      if (option.selected) { +        value.push(option.value); +      } +    } +    return value; +  }, + +  updateModel: function(scope) { +    var value = this.selected(); +    // TODO: This is wrong! no caching going on here as we are always comparing arrays +    if (this.lastValue === value) { +      return false; +    } else { +      scope.setEval(this.exp, value); +      this.lastValue = value; +      return true; +    } +  }, + +  updateView: function(scope) { +    var input = this.view; +    var selected = scope.get(this.exp); +    if (typeof selected === "undefined") { +      selected = this.initialValue; +      scope.setEval(this.exp, selected); +    } +    if (selected !== this.lastValue) { +      var options = input.options; +      for ( var i = 0; i < options.length; i++) { +        var option = options[i]; +        option.selected = _.include(selected, option.value); +      } +      this.lastValue = selected; +    } +  } +}; + +/////////////////////// +// RadioController +/////////////////////// +function RadioController(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastChecked = undefined; +  this.lastValue = undefined; +  this.inputValue = view.value; +  this.initialValue = view.checked ? view.value : null; +}; + +RadioController.prototype = { +  updateModel: function(scope) { +    var input = this.view; +    if (this.lastChecked) { +      return false; +    } else { +      input.checked = true; +      this.lastValue = scope.setEval(this.exp, this.inputValue); +      this.lastChecked = true; +      return true; +    } +  }, + +  updateView: function(scope) { +    var input = this.view; +    var value = scope.get(this.exp); +    if (this.initialValue && typeof value === "undefined") { +      value = this.initialValue; +      scope.setEval(this.exp, value); +    } +    if (this.lastValue != value) { +      this.lastChecked = input.checked = this.inputValue == (''+value); +      this.lastValue = value; +    } +  } +}; + +/////////////////////// +//ElementController +/////////////////////// +function BindUpdater(view, exp) { +  this.view = view; +  this.exp = Binder.parseBindings(exp); +  this.hasError = false; +}; + +BindUpdater.toText = function(obj) { +  var e = escapeHtml; +  switch(typeof obj) { +    case "string": +    case "boolean": +    case "number": +      return e(obj); +    case "function": +      return BindUpdater.toText(obj()); +    case "object": +      if (isNode(obj)) { +        return outerHTML(obj); +      } else if (obj instanceof angular.filter.Meta) { +        switch(typeof obj.html) { +          case "string": +          case "number": +            return obj.html; +          case "function": +            return obj.html(); +          case "object": +            if (isNode(obj.html)) +              return outerHTML(obj.html); +          default: +            break; +        } +        switch(typeof obj.text) { +          case "string": +          case "number": +            return e(obj.text); +          case "function": +            return e(obj.text()); +          default: +            break; +        } +      } +      if (obj === null) +        return ""; +      return e(toJson(obj, true)); +    default: +      return ""; +  } +}; + +BindUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    var html = []; +    var parts = this.exp; +    var length = parts.length; +    for(var i=0; i<length; i++) { +      var part = parts[i]; +      var binding = Binder.binding(part); +      if (binding) { +        scope.evalWidget(this, binding, {$element:this.view}, function(value){ +          html.push(BindUpdater.toText(value)); +        }, function(e, text){ +          setHtml(this.view, text); +        }); +        if (this.hasError) { +          return; +        } +      } else { +        html.push(escapeHtml(part)); +      } +    } +    setHtml(this.view, html.join('')); +  } +}; + +function BindAttrUpdater(view, attrs) { +  this.view = view; +  this.attrs = attrs; +}; + +BindAttrUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    var jNode = jQuery(this.view); +    var attributeTemplates = this.attrs; +    if (this.hasError) { +      this.hasError = false; +      jNode. +        removeClass('ng-exception'). +        removeAttr('ng-error'); +    } +    var isImage = jNode.is('img'); +    for (var attrName in attributeTemplates) { +      var attributeTemplate = Binder.parseBindings(attributeTemplates[attrName]); +      var attrValues = []; +      for ( var i = 0; i < attributeTemplate.length; i++) { +        var binding = Binder.binding(attributeTemplate[i]); +        if (binding) { +          try { +            var value = scope.eval(binding, {$element:jNode[0], attrName:attrName}); +            if (value && (value.constructor !== array || value.length !== 0)) +              attrValues.push(value); +          } catch (e) { +            this.hasError = true; +            error('BindAttrUpdater', e); +            var jsonError = toJson(e, true); +            attrValues.push('[' + jsonError + ']'); +            jNode. +              addClass('ng-exception'). +              attr('ng-error', jsonError); +          } +        } else { +          attrValues.push(attributeTemplate[i]); +        } +      } +      var attrValue = attrValues.length ? attrValues.join('') : null; +      if(isImage && attrName == 'src' && !attrValue) +        attrValue = scope.get('$config.blankImage'); +      jNode.attr(attrName, attrValue); +    } +  } +}; + +function EvalUpdater(view, exp) { +  this.view = view; +  this.exp = exp; +  this.hasError = false; +}; +EvalUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp); +  } +}; + +function HideUpdater(view, exp) { this.view = view; this.exp = exp; }; +HideUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(hideValue){ +      var view = jQuery(this.view); +      if (toBoolean(hideValue)) { +        view.hide(); +      } else { +        view.show(); +      } +    }); +  } +}; + +function ShowUpdater(view, exp) { this.view = view; this.exp = exp; }; +ShowUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(hideValue){ +      var view = jQuery(this.view); +      if (toBoolean(hideValue)) { +        view.show(); +      } else { +        view.hide(); +      } +    }); +  } +}; + +function ClassUpdater(view, exp) { this.view = view; this.exp = exp; }; +ClassUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(classValue){ +      if (classValue !== null && classValue !== undefined) { +        this.view.className = classValue; +      } +    }); +  } +}; + +function ClassEvenUpdater(view, exp) { this.view = view; this.exp = exp; }; +ClassEvenUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(classValue){ +      var index = scope.get('$index'); +      jQuery(this.view).toggleClass(classValue, index % 2 === 1); +    }); +  } +}; + +function ClassOddUpdater(view, exp) { this.view = view; this.exp = exp; }; +ClassOddUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(classValue){ +      var index = scope.get('$index'); +      jQuery(this.view).toggleClass(classValue, index % 2 === 0); +    }); +  } +}; + +function StyleUpdater(view, exp) { this.view = view; this.exp = exp; }; +StyleUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.exp, {}, function(styleValue){ +      jQuery(this.view).attr('style', "").css(styleValue); +    }); +  } +}; + +/////////////////////// +// RepeaterUpdater +/////////////////////// +function RepeaterUpdater(view, repeaterExpression, template, prefix) { +  this.view = view; +  this.template = template; +  this.prefix = prefix; +  this.children = []; +  var match = repeaterExpression.match(/^\s*(.+)\s+in\s+(.*)\s*$/); +  if (! match) { +    throw "Expected ng-repeat in form of 'item in collection' but got '" + +      repeaterExpression + "'."; +  } +  var keyValue = match[1]; +  this.iteratorExp = match[2]; +  match = keyValue.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); +  if (!match) { +    throw "'item' in 'item in collection' should be identifier or (key, value) but get '" + +      keyValue + "'."; +  } +  this.valueExp = match[3] || match[1]; +  this.keyExp = match[2]; +}; + +RepeaterUpdater.prototype = { +  updateModel: noop, +  updateView: function(scope) { +    scope.evalWidget(this, this.iteratorExp, {}, function(iterator){ +      var self = this; +      if (!iterator) { +        iterator = []; +        if (scope.isProperty(this.iteratorExp)) { +          scope.set(this.iteratorExp, iterator); +        } +      } +      var childrenLength = this.children.length; +      var cursor = this.view; +      var time = 0; +      var child = null; +      var keyExp = this.keyExp; +      var valueExp = this.valueExp; +      var iteratorCounter = 0; +      foreach(iterator, function(value, key){ +        if (iteratorCounter < childrenLength) { +          // reuse children +          child = self.children[iteratorCounter]; +          child.scope.set(valueExp, value); +        } else { +          // grow children +          var name = self.prefix + +            valueExp + " in " + self.iteratorExp + "[" + iteratorCounter + "]"; +          var childScope = new Scope(scope.state, name); +          childScope.set('$index', iteratorCounter); +          if (keyExp) +            childScope.set(keyExp, key); +          childScope.set(valueExp, value); +          child = { scope:childScope, element:self.template(childScope, self.prefix, iteratorCounter) }; +          cursor.after(child.element); +          self.children.push(child); +        } +        cursor = child.element; +        var s = new Date().getTime(); +        child.scope.updateView(); +        time += new Date().getTime() - s; +        iteratorCounter++; +      }); +      // shrink children +      for ( var r = childrenLength; r > iteratorCounter; --r) { +        this.children.pop().element.remove(); +      } +      // Special case for option in select +      if (child && child.element[0].nodeName === "OPTION") { +        var select = jQuery(child.element[0].parentNode); +        var cntl = select.data('controller'); +        if (cntl) { +          cntl.lastValue = undefined; +          cntl.updateView(scope); +        } +      } +    }); +  } +}; + +////////////////////////////////// +// PopUp +////////////////////////////////// + +function PopUp(doc) { +  this.doc = doc; +}; + +PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; + +PopUp.onOver = function(e) { +  PopUp.onOut(); +  var jNode = jQuery(this); +  jNode.bind(PopUp.OUT_EVENT, PopUp.onOut); +  var position = jNode.position(); +  var de = document.documentElement; +  var w = self.innerWidth || (de && de.clientWidth) || document.body.clientWidth; +  var hasArea = w - position.left; +  var width = 300; +  var title = jNode.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."; +  var msg = jNode.attr("ng-error"); + +  var x; +  var arrowPos = hasArea>(width+75) ? "left" : "right"; +  var tip = jQuery( +    "<div id='ng-callout' style='width:"+width+"px'>" + +      "<div class='ng-arrow-"+arrowPos+"'/>" + +      "<div class='ng-title'>"+title+"</div>" + +      "<div class='ng-content'>"+msg+"</div>" + +    "</div>"); +  jQuery("body").append(tip); +  if(arrowPos === 'left'){ +    x = position.left + this.offsetWidth + 11; +  }else{ +    x = position.left - (width + 15); +    tip.find('.ng-arrow-right').css({left:width+1}); +  } + +  tip.css({left: x+"px", top: (position.top - 3)+"px"}); +  return true; +}; + +PopUp.onOut = function() { +  jQuery('#ng-callout'). +    unbind(PopUp.OUT_EVENT, PopUp.onOut). +    remove(); +  return true; +}; + +PopUp.prototype = { +  bind: function () { +    var self = this; +    this.doc.find('.ng-validation-error,.ng-exception'). +      live("mouseover", PopUp.onOver); +  } +}; + +////////////////////////////////// +// Status +////////////////////////////////// + +function NullStatus(body) { +}; + +NullStatus.prototype = { + beginRequest:function(){}, + endRequest:function(){} +}; + +function Status(body) { +  this.requestCount = 0; +  this.body = body; +}; + +Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>'; + +Status.prototype = { +  beginRequest: function () { +    if (this.requestCount === 0) { +      (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show(); +    } +    this.requestCount++; +  }, + +  endRequest: function () { +    this.requestCount--; +    if (this.requestCount === 0) { +      this.loader.hide("fold"); +    } +  } +}; diff --git a/src/directives.js b/src/directives.js new file mode 100644 index 00000000..cabf0c23 --- /dev/null +++ b/src/directives.js @@ -0,0 +1,261 @@ +angularDirective("ng-init", function(expression){ +  return function(element){ +    this.$tryEval(expression, element); +  }; +}); + +angularDirective("ng-controller", function(expression){ +  return function(element){ +    var controller = getter(window, expression, true) || getter(this, expression, true); +    if (!controller) +      throw "Can not find '"+expression+"' controller."; +    if (!isFunction(controller)) +      throw "Reference '"+expression+"' is not a class."; +    this.$become(controller); +    (this.init || noop)(); +  }; +}); + +angularDirective("ng-eval", function(expression){ +  return function(element){ +    this.$onEval(expression, element); +  }; +}); + +angularDirective("ng-bind", function(expression){ +  return function(element) { +    var lastValue = noop, lastError = noop; +    this.$onEval(function() { +      var error, +          value = this.$tryEval(expression, function(e){ +            error = toJson(e); +          }), +          isHtml, +          isDomElement; +      if (lastValue === value && lastError == error) return; +      isHtml = value instanceof HTML, +      isDomElement = isElement(value); +      if (!isHtml && !isDomElement && isObject(value)) { +        value = toJson(value); +      } +      if (value != lastValue || error != lastError) { +        lastValue = value; +        lastError = error; +        elementError(element, NG_EXCEPTION, error); +        if (error) value = error; +        if (isHtml) { +          element.html(value.html); +        } else if (isDomElement) { +          element.html(''); +          element.append(value); +        } else { +          element.text(value); +        } +      } +    }, element); +  }; +}); + +var bindTemplateCache = {}; +function compileBindTemplate(template){ +  var fn = bindTemplateCache[template]; +  if (!fn) { +    var bindings = []; +    foreach(parseBindings(template), function(text){ +      var exp = binding(text); +      bindings.push(exp ? function(element){ +        var error, value = this.$tryEval(exp, function(e){ +          error = toJson(e); +        }); +        elementError(element, NG_EXCEPTION, error); +        return error ? error : value; +      } : function() { +        return text; +      }); +    }); +    bindTemplateCache[template] = fn = function(element){ +      var parts = [], self = this; +      for ( var i = 0; i < bindings.length; i++) { +        var value = bindings[i].call(self, element); +        if (isElement(value)) +          value = ''; +        else if (isObject(value)) +          value = toJson(value, true); +        parts.push(value); +      }; +      return parts.join(''); +    }; +  } +  return fn; +} + +angularDirective("ng-bind-template", function(expression){ +  var templateFn = compileBindTemplate(expression); +  return function(element) { +    var lastValue; +    this.$onEval(function() { +      var value = templateFn.call(this, element); +      if (value != lastValue) { +        element.text(value); +        lastValue = value; +      } +    }, element); +  }; +}); + +var REMOVE_ATTRIBUTES = { +  'disabled':'disabled', +  'readonly':'readOnly', +  'checked':'checked' +}; +angularDirective("ng-bind-attr", function(expression){ +  return function(element){ +    var lastValue = {}; +    this.$onEval(function(){ +      var values = this.$eval(expression); +      for(var key in values) { +        var value = compileBindTemplate(values[key]).call(this, element), +            specialName = REMOVE_ATTRIBUTES[lowercase(key)]; +        if (lastValue[key] !== value) { +          lastValue[key] = value; +          if (specialName) { +            if (element[specialName] = toBoolean(value)) { +              element.attr(specialName, value); +            } else { +              element.removeAttr(key); +            } +            (element.data('$validate')||noop)(); +          } else { +            element.attr(key, value); +          } +        } +      }; +    }, element); +  }; +}); + +angularWidget("@ng-non-bindable", noop); + +angularWidget("@ng-repeat", function(expression, element){ +  element.removeAttr('ng-repeat'); +  element.replaceWith(this.comment("ng-repeat: " + expression)); +  var template = this.compile(element); +  return function(reference){ +    var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/), +        lhs, rhs, valueIdent, keyIdent; +    if (! match) { +      throw "Expected ng-repeat in form of 'item in collection' but got '" + +      expression + "'."; +    } +    lhs = match[1]; +    rhs = match[2]; +    match = lhs.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); +    if (!match) { +      throw "'item' in 'item in collection' should be identifier or (key, value) but got '" + +      keyValue + "'."; +    } +    valueIdent = match[3] || match[1]; +    keyIdent = match[2]; + +    if (isUndefined(this.$eval(rhs))) this.$set(rhs, []); + +    var children = [], currentScope = this; +    this.$onEval(function(){ +      var index = 0, childCount = children.length, childScope, lastElement = reference, +          collection = this.$tryEval(rhs, reference); +      for ( var key in collection) { +        if (index < childCount) { +          // reuse existing child +          childScope = children[index]; +          childScope[valueIdent] = collection[key]; +          if (keyIdent) childScope[keyIdent] = key; +        } else { +          // grow children +          childScope = template(element.clone(), createScope(currentScope)); +          childScope[valueIdent] = collection[key]; +          if (keyIdent) childScope[keyIdent] = key; +          lastElement.after(childScope.$element); +          childScope.$index = index; +          childScope.$element.attr('ng-repeat-index', index); +          childScope.$init(); +          children.push(childScope); +        } +        childScope.$eval(); +        lastElement = childScope.$element; +        index ++; +      }; +      // shrink children +      while(children.length > index) { +        children.pop().$element.remove(); +      } +    }, reference); +  }; +}); + +angularDirective("ng-click", function(expression, element){ +  return function(element){ +    var self = this; +    element.bind('click', function(){ +      self.$tryEval(expression, element); +      self.$root.$eval(); +      return false; +    }); +  }; +}); + +angularDirective("ng-watch", function(expression, element){ +  return function(element){ +    var self = this; +    new Parser(expression).watch()({ +      addListener:function(watch, exp){ +        self.$watch(watch, function(){ +          return exp(self); +        }, element); +      } +    }); +  }; +}); + +function ngClass(selector) { +  return function(expression, element){ +    var existing = element[0].className + ' '; +    return function(element){ +      this.$onEval(function(){ +        var value = this.$eval(expression); +        if (selector(this.$index)) { +          if (isArray(value)) value = value.join(' '); +          element[0].className = trim(existing + value); +        } +      }, element); +    }; +  }; +} + +angularDirective("ng-class", ngClass(function(){return true;})); +angularDirective("ng-class-odd", ngClass(function(i){return i % 2 === 0;})); +angularDirective("ng-class-even", ngClass(function(i){return i % 2 === 1;})); + +angularDirective("ng-show", function(expression, element){ +  return function(element){ +    this.$onEval(function(){ +      element.css('display', toBoolean(this.$eval(expression)) ? '' : 'none'); +    }, element); +  }; +}); + +angularDirective("ng-hide", function(expression, element){ +  return function(element){ +    this.$onEval(function(){ +      element.css('display', toBoolean(this.$eval(expression)) ? 'none' : ''); +    }, element); +  }; +}); + +angularDirective("ng-style", function(expression, element){ +  return function(element){ +    this.$onEval(function(){ +      element.css(this.$eval(expression)); +    }, element); +  }; +}); + diff --git a/src/filters.js b/src/filters.js new file mode 100644 index 00000000..a911b935 --- /dev/null +++ b/src/filters.js @@ -0,0 +1,298 @@ +var angularFilterGoogleChartApi; + +foreach({ +  'currency': function(amount){ +    this.$element.toggleClass('ng-format-negative', amount < 0); +    return '$' + angularFilter['number'].apply(this, [amount, 2]); +  }, + +  'number': function(amount, fractionSize){ +    if (isNaN(amount) || !isFinite(amount)) { +      return ''; +    } +    fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; +    var isNegative = amount < 0; +    amount = Math.abs(amount); +    var pow = Math.pow(10, fractionSize); +    var text = "" + Math.round(amount * pow); +    var whole = text.substring(0, text.length - fractionSize); +    whole = whole || '0'; +    var frc = text.substring(text.length - fractionSize); +    text = isNegative ? '-' : ''; +    for (var i = 0; i < whole.length; i++) { +      if ((whole.length - i)%3 === 0 && i !== 0) { +        text += ','; +      } +      text += whole.charAt(i); +    } +    if (fractionSize > 0) { +      for (var j = frc.length; j < fractionSize; j++) { +        frc += '0'; +      } +      text += '.' + frc.substring(0, fractionSize); +    } +    return text; +  }, + +  'date': function(amount) { +  }, + +  'json': function(object) { +    this.$element.addClass("ng-monospace"); +    return toJson(object, true); +  }, + +  'trackPackage': (function(){ +    var MATCHERS = [ +      { name: "UPS", +        url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=", +        regexp: [ +          /^1Z[0-9A-Z]{16}$/i]}, +      { name: "FedEx", +        url: "http://www.fedex.com/Tracking?tracknumbers=", +        regexp: [ +          /^96\d{10}?$/i, +          /^96\d{17}?$/i, +          /^96\d{20}?$/i, +          /^\d{15}$/i, +          /^\d{12}$/i]}, +      { name: "USPS", +        url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=", +        regexp: [ +          /^(91\d{20})$/i, +          /^(91\d{18})$/i]}]; +    return function(trackingNo, noMatch) { +      trackingNo = trim(trackingNo); +      var tNo = trackingNo.replace(/ /g, ''); +      var returnValue; +      foreach(MATCHERS, function(carrier){ +        foreach(carrier.regexp, function(regexp){ +          if (!returnValue && regexp.test(tNo)) { +            var text = carrier.name + ": " + trackingNo; +            var url = carrier.url + trackingNo; +            returnValue = jqLite('<a></a>'); +            returnValue.text(text); +            returnValue.attr('href', url); +          } +        }); +      }); +      if (returnValue) +        return returnValue; +      else if (trackingNo) +        return noMatch || trackingNo + " is not recognized"; +      else +        return null; +    };})(), + +  'link': function(obj, title) { +    if (obj) { +      var text = title || obj.text || obj; +      var url = obj.url || obj; +      if (url) { +        if (angular.validator.email(url) === null) { +          url = "mailto:" + url; +        } +        var a = jqLite('<a></a>'); +        a.attr('href', url); +        a.text(text); +        return a; +      } +    } +    return obj; +  }, + + +  'bytes': (function(){ +    var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; +    return function(size) { +      if(size === null) return ""; + +      var suffix = 0; +      while (size > 1000) { +        size = size / 1024; +        suffix++; +      } +      var txt = "" + size; +      var dot = txt.indexOf('.'); +      if (dot > -1 && dot + 2 < txt.length) { +        txt = txt.substring(0, dot + 2); +      } +      return txt + " " + SUFFIX[suffix]; +    }; +  })(), + +  'image': function(obj, width, height) { +    if (obj && obj.url) { +      var style = "", img = jqLite('<img>'); +      if (width) { +        img.css('max-width', width + 'px'); +        img.css('max-height', (height || width) + 'px'); +      } +      img.attr('src', obj.url); +      return img; +    } +    return null; +  }, + +  'lowercase': lowercase, + +  'uppercase': uppercase, + +  'linecount': function (obj) { +    if (isString(obj)) { +      if (obj==='') return 1; +      return obj.split(/\n|\f/).length; +    } +    return 1; +  }, + +  'if': function (result, expression) { +    return expression ? result : undefined; +  }, + +  'unless': function (result, expression) { +    return expression ? undefined : result; +  }, + +  'googleChartApi': extend( +    function(type, data, width, height) { +      data = data || {}; +      var chart = { +          'cht':type, +          'chco':angularFilterGoogleChartApi['collect'](data, 'color'), +          'chtt':angularFilterGoogleChartApi['title'](data), +          'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), +          'chd':angularFilterGoogleChartApi['values'](data), +          'chf':'bg,s,FFFFFF00' +        }; +      if (_.isArray(data['xLabels'])) { +        chart['chxt']='x'; +        chart['chxl']='0:|' + data.xLabels.join('|'); +      } +      return angularFilterGoogleChartApi['encode'](chart, width, height); +    }, +    { +      'values': function(data){ +        var seriesValues = []; +        foreach(data['series']||[], function(serie){ +          var values = []; +          foreach(serie['values']||[], function(value){ +            values.push(value); +          }); +          seriesValues.push(values.join(',')); +        }); +        var values = seriesValues.join('|'); +        return values === "" ? null : "t:" + values; +      }, + +      'title': function(data){ +        var titles = []; +        var title = data['title'] || []; +        foreach(_.isArray(title)?title:[title], function(text){ +          titles.push(encodeURIComponent(text)); +        }); +        return titles.join('|'); +      }, + +      'collect': function(data, key){ +        var outterValues = []; +        var count = 0; +        foreach(data['series']||[], function(serie){ +          var innerValues = []; +          var value = serie[key] || []; +          foreach(_.isArray(value)?value:[value], function(color){ +              innerValues.push(encodeURIComponent(color)); +              count++; +            }); +          outterValues.push(innerValues.join('|')); +        }); +        return count?outterValues.join(','):null; +      }, + +      'encode': function(params, width, height) { +        width = width || 200; +        height = height || width; +        var url = "http://chart.apis.google.com/chart?", +            urlParam = [], +            img = jqLite('<img>'); +        params['chs'] = width + "x" + height; +        foreach(params, function(value, key){ +          if (value) { +            urlParam.push(key + "=" + value); +          } +        }); +        urlParam.sort(); +        url += urlParam.join("&"); +        img.attr('src', url); +        img.css({width: width + 'px', height: height + 'px'}); +        return img; +      } +    } +  ), + + +  'qrcode': function(value, width, height) { +    return angularFilterGoogleChartApi['encode']({ +      'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); +  }, +  'chart': { +    'pie':function(data, width, height) { +      return angularFilterGoogleChartApi('p', data, width, height); +    }, +    'pie3d':function(data, width, height) { +      return angularFilterGoogleChartApi('p3', data, width, height); +    }, +    'pieConcentric':function(data, width, height) { +      return angularFilterGoogleChartApi('pc', data, width, height); +    }, +    'barHorizontalStacked':function(data, width, height) { +      return angularFilterGoogleChartApi('bhs', data, width, height); +    }, +    'barHorizontalGrouped':function(data, width, height) { +      return angularFilterGoogleChartApi('bhg', data, width, height); +    }, +    'barVerticalStacked':function(data, width, height) { +      return angularFilterGoogleChartApi('bvs', data, width, height); +    }, +    'barVerticalGrouped':function(data, width, height) { +      return angularFilterGoogleChartApi('bvg', data, width, height); +    }, +    'line':function(data, width, height) { +      return angularFilterGoogleChartApi('lc', data, width, height); +    }, +    'sparkline':function(data, width, height) { +      return angularFilterGoogleChartApi('ls', data, width, height); +    }, +    'scatter':function(data, width, height) { +      return angularFilterGoogleChartApi('s', data, width, height); +    } +  }, + +  'html': function(html){ +    return new HTML(html); +  }, + +  'linky': function(text){ +    if (!text) return text; +    function regExpEscape(text) { +      return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); +    } +    var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; +    var match; +    var raw = text; +    var html = []; +    while (match=raw.match(URL)) { +      var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); +      var i = raw.indexOf(url); +      html.push(escapeHtml(raw.substr(0, i))); +      html.push('<a href="' + url + '">'); +      html.push(url); +      html.push('</a>'); +      raw = raw.substring(i + url.length); +    } +    html.push(escapeHtml(raw)); +    return new HTML(html.join('')); +  } +}, function(v,k){angularFilter[k] = v;}); + +angularFilterGoogleChartApi = angularFilter['googleChartApi']; diff --git a/src/formatters.js b/src/formatters.js new file mode 100644 index 00000000..40462cf3 --- /dev/null +++ b/src/formatters.js @@ -0,0 +1,32 @@ +function formatter(format, parse) {return {'format':format, 'parse':parse || format};} +function toString(obj) {return (isDefined(obj) && obj !== null) ? "" + obj : obj;} + +var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/; + +extend(angularFormatter, { +  'noop':formatter(identity, identity), +  'boolean':formatter(toString, toBoolean), +  'number':formatter(toString, +      function(obj){ +        if (isString(obj) && NUMBER.exec(obj)) { +          return obj ? 1*obj : null; +        } +        throw "Not a number"; +      }), + +  'list':formatter( +    function(obj) { return obj ? obj.join(", ") : obj; }, +    function(value) { +      var list = []; +      foreach((value || '').split(','), function(item){ +        item = trim(item); +        if (item) list.push(item); +      }); +      return list; +    } +  ), + +  'trim':formatter( +    function(obj) { return obj ? trim("" + obj) : ""; } +  ) +}); diff --git a/src/jqLite.js b/src/jqLite.js new file mode 100644 index 00000000..68172fd8 --- /dev/null +++ b/src/jqLite.js @@ -0,0 +1,248 @@ +////////////////////////////////// +//JQLite +////////////////////////////////// + +var jqCache = {}; +var jqName = 'ng-' + new Date().getTime(); +var jqId = 1; +function jqNextId() { return (jqId++); } + +var addEventListener = window.document.attachEvent ? +    function(element, type, fn) { +      element.attachEvent('on' + type, fn); +    } : function(element, type, fn) { +      element.addEventListener(type, fn, false); +    }; + +var removeEventListener = window.document.detachEvent ? +    function(element, type, fn) { +      element.detachEvent('on' + type, fn); +    } : function(element, type, fn) { +      element.removeEventListener(type, fn, false); +    }; + +function jqClearData(element) { +  var cacheId = element[jqName], +      cache = jqCache[cacheId]; +  if (cache) { +    foreach(cache.bind || {}, function(fn, type){ +      removeEventListener(element, type, fn); +    }); +    delete jqCache[cacheId]; +    if (msie) +      element[jqName] = ''; // ie does not allow deletion of attributes on elements. +    else +      delete element[jqName]; +  } +} + +function JQLite(element) { +  if (isElement(element)) { +    this[0] = element; +    this.length = 1; +  } else if (isDefined(element.length) && element.item) { +    for(var i=0; i < element.length; i++) { +      this[i] = element[i]; +    } +    this.length = element.length; +  } +} + +JQLite.prototype = { +  data: function(key, value) { +    var element = this[0], +        cacheId = element[jqName], +        cache = jqCache[cacheId || -1]; +    if (isDefined(value)) { +      if (!cache) { +        element[jqName] = cacheId = jqNextId(); +        cache = jqCache[cacheId] = {}; +      } +      cache[key] = value; +    } else { +      return cache ? cache[key] : null; +    } +  }, + +  removeData: function(){ +    jqClearData(this[0]); +  }, + +  dealoc: function(){ +    (function dealoc(element){ +      jqClearData(element); +      for ( var i = 0, children = element.childNodes; i < children.length; i++) { +        dealoc(children[i]); +      } +    })(this[0]); +  }, + +  bind: function(type, fn){ +    var self = this, +        element = self[0], +        bind = self.data('bind'), +        eventHandler; +    if (!bind) this.data('bind', bind = {}); +    foreach(type.split(' '), function(type){ +      eventHandler = bind[type]; +      if (!eventHandler) { +        bind[type] = eventHandler = function(event) { +          var bubbleEvent = false; +          foreach(eventHandler.fns, function(fn){ +            bubbleEvent = bubbleEvent || fn.call(self, event); +          }); +          if (!bubbleEvent) { +            if (msie) { +              event.returnValue = false; +              event.cancelBubble = true; +            } else { +              event.preventDefault(); +              event.stopPropagation(); +            } +          } +        }; +        eventHandler.fns = []; +        addEventListener(element, type, eventHandler); +      } +      eventHandler.fns.push(fn); +    }); +  }, + +  trigger: function(type) { +    var evnt = document.createEvent('MouseEvent'); +    evnt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); +    this[0].dispatchEvent(evnt); +  }, + +  replaceWith: function(replaceNode) { +    this[0].parentNode.replaceChild(jqLite(replaceNode)[0], this[0]); +  }, + +  children: function() { +    return new JQLite(this[0].childNodes); +  }, + +  append: function(node) { +    var self = this[0]; +    node = jqLite(node); +    foreach(node, function(child){ +      self.appendChild(child); +    }); +  }, + +  remove: function() { +    this.dealoc(); +    var parentNode = this[0].parentNode; +    if (parentNode) parentNode.removeChild(this[0]); +  }, + +  removeAttr: function(name) { +    this[0].removeAttribute(name); +  }, + +  after: function(element) { +    this[0].parentNode.insertBefore(jqLite(element)[0], this[0].nextSibling); +  }, + +  hasClass: function(selector) { +    var className = " " + selector + " "; +    if ( (" " + this[0].className + " ").replace(/[\n\t]/g, " ").indexOf( className ) > -1 ) { +      return true; +    } +    return false; +  }, + +  removeClass: function(selector) { +    this[0].className = trim((" " + this[0].className + " ").replace(/[\n\t]/g, " ").replace(" " + selector + " ", "")); +  }, + +  toggleClass: function(selector, condition) { +   var self = this; +   (condition ? self.addClass : self.removeClass).call(self, selector); +  }, + +  addClass: function( selector ) { +    if (!this.hasClass(selector)) { +      this[0].className = trim(this[0].className + ' ' + selector); +    } +  }, + +  css: function(name, value) { +    var style = this[0].style; +    if (isString(name)) { +      if (isDefined(value)) { +        style[name] = value; +      } else { +        return style[name]; +      } +    } else { +      extend(style, name); +    } +  }, + +  attr: function(name, value){ +    var e = this[0]; +    if (isObject(name)) { +      foreach(name, function(value, name){ +        e.setAttribute(name, value); +      }); +    } else if (isDefined(value)) { +      e.setAttribute(name, value); +    } else { +      var attributes = e.attributes, +          item = attributes ? attributes.getNamedItem(name) : undefined; +      return item && item.specified ? item.value : undefined; +    } +  }, + +  text: function(value) { +    if (isDefined(value)) { +      this[0].textContent = value; +    } +    return this[0].textContent; +  }, + +  val: function(value) { +    if (isDefined(value)) { +      this[0].value = value; +    } +    return this[0].value; +  }, + +  html: function(value) { +    if (isDefined(value)) { +      var i = 0, childNodes = this[0].childNodes; +      for ( ; i < childNodes.length; i++) { +        jqLite(childNodes[i]).dealoc(); +      } +      this[0].innerHTML = value; +    } +    return this[0].innerHTML; +  }, + +  parent: function() { +    return jqLite(this[0].parentNode); +  }, + +  clone: function() { return jqLite(this[0].cloneNode(true)); } +}; + +if (msie) { +  extend(JQLite.prototype, { +    text: function(value) { +      var e = this[0]; +      // NodeType == 3 is text node +      if (e.nodeType == 3) { +        if (isDefined(value)) e.nodeValue = value; +        return e.nodeValue; +      } else { +        if (isDefined(value)) e.innerText = value; +        return e.innerText; +      } +    }, + +    trigger: function(type) { +      this[0].fireEvent('on' + type); +    } +  }); +} diff --git a/src/markups.js b/src/markups.js new file mode 100644 index 00000000..74b293b8 --- /dev/null +++ b/src/markups.js @@ -0,0 +1,85 @@ +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); +        } +        if (msie && text.charAt(0) == ' ') { +          newElement = jqLite('<span> </span>'); +          var nbsp = newElement.html(); +          newElement.text(text.substr(1)); +          newElement.html(nbsp + newElement.html()); +        } +        cursor.after(newElement); +        cursor = newElement; +      }); +    } +    textNode.remove(); +  } +}); + +// TODO: this should be widget not a markup +angularTextMarkup('OPTION', function(text, textNode, parentElement){ +  if (nodeName(parentElement) == "OPTION") { +    var select = document.createElement('select'); +    select.insertBefore(parentElement[0].cloneNode(true), null); +    if (!select.innerHTML.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\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-') { +    if (msie && name == 'src') +      value = decodeURI(value); +    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 = +  '<div>' + +    '<div class="ui-widget-overlay"></div>' + +    '<div id="ng-login" ng-non-bindable="true">' + +      '<div class="ng-login-container"></div>' + +    '</div>' + +  '</div>'; + + +ControlBar.FORBIDEN = +  '<div ng-non-bindable="true" title="Permission Error:">' + +    'Sorry, you do not have permission for this!'+ +  '</div>'; + +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('<div style="overflow:hidden; padding:2px 0 0 0;"><iframe name="'+ url +'" src="'+this.serverUrl + path + '" width="500" height="'+ iframeHeight +'"/></div>'); +    this._document.append(loginView); +    loginView['dialog']({ +      'height':iframeHeight + 33, 'width':500, +      'resizable': false, 'modal':true, +      'title': 'Authentication: <a href="http://www.getangular.com"><tt><angular/></tt></a>' +    }); +    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/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/moveToAngularCom/directivesAngularCom.js b/src/moveToAngularCom/directivesAngularCom.js new file mode 100644 index 00000000..84032bdd --- /dev/null +++ b/src/moveToAngularCom/directivesAngularCom.js @@ -0,0 +1,29 @@ + +angular.directive("auth", function(expression, element){ +  return function(){ +    if(expression == "eager") { +      this.$users.fetchCurrent(); +    } +  }; +}); + + +//expression = "book=Book:{year=2000}" +angular.directive("entity", function(expression, element){ +  //parse expression, ignore element +  var entityName; // "Book"; +  var instanceName; // "book"; +  var defaults; // {year: 2000}; + +  parse(expression); + +  return function(){ +    this[entityName] = this.$datastore.entity(entityName, defaults); +    this[instanceName] = this[entityName](); +    this.$watch("$anchor."+instanceName, function(newAnchor){ +      this[instanceName] = this[entityName].get(this.$anchor[instanceName]); +    }); +  }; +}); + + diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js new file mode 100644 index 00000000..194a28d6 --- /dev/null +++ b/src/scenario/DSL.js @@ -0,0 +1,63 @@ +angular.scenario.dsl.browser = { +  navigateTo: function(url){ +    $scenario.addStep('Navigate to: ' + url, function(done){ +      var self = this; +      this.testFrame.load(function(){ +        self.testFrame.unbind(); +        self.testWindow = self.testFrame[0].contentWindow; +        self.testDocument = jQuery(self.testWindow.document); +        self.$browser = self.testWindow.angular.service.$browser(); +        self.notifyWhenNoOutstandingRequests = bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests); +        self.notifyWhenNoOutstandingRequests(done); +      }); +      if (this.testFrame.attr('src') == url) { +        this.testFrame[0].contentWindow.location.reload(); +      } else { +        this.testFrame.attr('src', url); +      } +    }); +  } +}; + +angular.scenario.dsl.input = function(selector) { +  return { +    enter: function(value){ +      $scenario.addStep("Set input text of '" + selector + "' to '" + +        value + "'", function(done){ +          var input = this.testDocument.find('input[name=' + selector + ']'); +          input.val(value); +          this.testWindow.angular.element(input[0]).trigger('change'); +          done(); +      }); +    }, +    select: function(value){ +      $scenario.addStep("Select radio '" + selector + "' to '" + +              value + "'", function(done){ +        var input = this.testDocument. +          find(':radio[name$=@' + selector + '][value=' + value + ']'); +        var event = this.testWindow.document.createEvent('MouseEvent'); +        event.initMouseEvent('click', true, true, this.testWindow, 0,0,0,0,0, false, false, false, false, 0, null); +        input[0].dispatchEvent(event); +        done(); +      }); +    } +  }; +}; + +angular.scenario.dsl.expect = { +  repeater: function(selector) { +    return { +      count: { +        toEqual: function(number) { +          $scenario.addStep("Expect that there are " + number + " items in Repeater with selector '" + selector + "'", function(done) { +            var items = this.testDocument.find(selector); +            if (items.length != number) { +              this.result.fail("Expected " + number + " but was " + items.length); +            } +            done(); +          }); +        } +      } +    }; +  } +}; diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js new file mode 100644 index 00000000..8e0cc909 --- /dev/null +++ b/src/scenario/Runner.js @@ -0,0 +1,161 @@ +angular['scenario'] = angular['scenario'] || (angular['scenario'] = {}); +angular.scenario['dsl'] = angular.scenario['dsl'] || (angular.scenario['dsl'] = {}); + +angular.scenario.Runner = function(scope, jQuery){ +  var self = scope.$scenario = this; +  this.scope = scope; +  this.jQuery = jQuery; + +  var specs = this.specs = {}; +  var path = []; +  this.scope.describe = function(name, body){ +    path.push(name); +    body(); +    path.pop(); +  }; +  var beforeEach = noop; +  var afterEach = noop; +  this.scope.beforeEach = function(body) { +    beforeEach = body; +  }; +  this.scope.afterEach = function(body) { +    afterEach = body; +  }; +  this.scope.it = function(name, body) { +    var specName = path.join(' ') + ': it ' + name; +    self.currentSpec = specs[specName] = { +        name: specName, +        steps:[] +     }; +    try { +      beforeEach(); +      body(); +    } catch(err) { +      self.addStep(err.message || 'ERROR', function(){ +        throw err; +      }); +    } finally { +      afterEach(); +    } +    self.currentSpec = null; +  }; +  this.logger = function returnNoop(){ +    return extend(returnNoop, {close:noop, fail:noop});; +  }; +}; + +angular.scenario.Runner.prototype = { +  run: function(body){ +    var jQuery = this.jQuery; +    body.append( +      '<div id="runner">' + +        '<div class="console"></div>' + +      '</div>' + +      '<div id="testView">' + +        '<iframe></iframe>' + +      '</div>'); +    var console = body.find('#runner .console'); +    console.find('li').live('click', function(){ +      jQuery(this).toggleClass('collapsed'); +    }); +    this.testFrame = body.find('#testView iframe'); +    function logger(parent) { +      var container; +      return function(type, text) { +        if (!container) { +          container = jQuery('<ul></ul>'); +          parent.append(container); +        } +        var element = jQuery('<li class="running '+type+'"><span></span></li>'); +        element.find('span').text(text); +        container.append(element); +        return extend(logger(element), { +          close: function(){ +            element.removeClass('running'); +            if(!element.hasClass('fail')) +              element.addClass('collapsed'); +            console.scrollTop(console[0].scrollHeight); +          }, +          fail: function(){ +            element.removeClass('running'); +            var current = element; +            while (current[0] != console[0]) { +              if (current.is('li')) +                current.addClass('fail'); +              current = current.parent(); +            } +          } +        }); +      }; +    } +    this.logger = logger(console); +    var specNames = []; +    foreach(this.specs, function(spec, name){ +      specNames.push(name); +    }, this); +    specNames.sort(); +    var self = this; +    function callback(){ +      var next = specNames.shift(); +      if(next) { +        self.execute(next, callback); +      } +    }; +    callback(); +  }, + +  addStep: function(name, step) { +    this.currentSpec.steps.push({name:name, fn:step}); +  }, + +  execute: function(name, callback) { +   var spec = this.specs[name], +       self = this, +       result = { +         passed: false, +         failed: false, +         finished: false, +         fail: function(error) { +           result.passed = false; +           result.failed = true; +           result.error = error; +           result.log('fail', isString(error) ? error : toJson(error)).fail(); +         } +       }, +       specThis = createScope({ +         result: result, +         testFrame: this.testFrame, +         testWindow: this.testWindow +       }, angularService, {}); +   this.self = specThis; +   var stepLogger = this.logger('spec', name); +   spec.nextStepIndex = 0; +   function done() { +     result.finished = true; +     stepLogger.close(); +     self.self = null; +     (callback||noop).call(specThis); +   } +   function next(){ +     var step = spec.steps[spec.nextStepIndex]; +     (result.log || {close:noop}).close(); +     result.log = null; +     if (step) { +       spec.nextStepIndex ++; +       result.log = stepLogger('step', step.name); +       try { +         step.fn.call(specThis, next); +       } catch (e) { +         console.error(e); +         result.fail(e); +         done(); +       } +     } else { +       result.passed = !result.failed; +       done(); +     } +   }; +   next(); +   return specThis; +  } +};
\ No newline at end of file diff --git a/src/scenario/angular.prefix b/src/scenario/angular.prefix new file mode 100644 index 00000000..5b44e17c --- /dev/null +++ b/src/scenario/angular.prefix @@ -0,0 +1,30 @@ +/** + * 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 + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +(function(window, document, previousOnLoad){ +  window.angular = { +    scenario: { +      dsl: window +    } +  }; + diff --git a/src/scenario/angular.suffix b/src/scenario/angular.suffix new file mode 100644 index 00000000..fc861cbf --- /dev/null +++ b/src/scenario/angular.suffix @@ -0,0 +1,11 @@ + +  var $scenarioRunner = new angular.scenario.Runner(window, jQuery); + +  window.onload = function(){ +    try { +      if (previousOnLoad) previousOnLoad(); +    } catch(e) {} +    $scenarioRunner.run(jQuery(window.document.body)); +  }; + +})(window, document, window.onload); diff --git a/src/scenario/bootstrap.js b/src/scenario/bootstrap.js new file mode 100644 index 00000000..694d0e97 --- /dev/null +++ b/src/scenario/bootstrap.js @@ -0,0 +1,44 @@ +(function(onLoadDelegate){ +  var prefix = (function(){ +    var filename = /(.*\/)bootstrap.js(#(.*))?/; +    var scripts = document.getElementsByTagName("script"); +    for(var j = 0; j < scripts.length; j++) { +      var src = scripts[j].src; +      if (src && src.match(filename)) { +        var parts = src.match(filename); +        return parts[1]; +      } +    } +  })(); +  function addScript(path) { +    document.write('<script type="text/javascript" src="' + prefix + path + '"></script>'); +  } + +  function addCSS(path) { +    document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>'); +  } + +  window.angular = { +    scenario: { +      dsl: window +    } +  }; + +  window.onload = function(){ +    _.defer(function(){ +      $scenarioRunner.run(jQuery(window.document.body)); +    }); +    (onLoadDelegate||function(){})(); +  }; +  addCSS("../../css/angular-scenario.css"); +  addScript("../../lib/underscore/underscore.js"); +  addScript("../../lib/jquery/jquery-1.4.2.js"); +  addScript("Runner.js"); +  addScript("../Angular.js"); +  addScript("../JSON.js"); +  addScript("DSL.js"); +  document.write('<script type="text/javascript">' + +    '$scenarioRunner = new angular.scenario.Runner(window, jQuery);' + +    '</script>'); +})(window.onload); + diff --git a/src/services.js b/src/services.js new file mode 100644 index 00000000..64f2ea4f --- /dev/null +++ b/src/services.js @@ -0,0 +1,361 @@ +angularService("$window", bind(window, identity, window)); +angularService("$document", function(window){ +  return jqLite(window.document); +}, {inject:['$window']}); + +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/; +var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/; +var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; +angularService("$location", function(browser){ +  var scope = this, location = {parse:parseUrl, toString:toString}; +  var lastHash, lastUrl; +  function parseUrl(url){ +    if (isDefined(url)) { +      var match = URL_MATCH.exec(url); +      if (match) { +        location.href = url; +        location.protocol = match[1]; +        location.host = match[3] || ''; +        location.port = match[5] || DEFAULT_PORTS[location.href] || null; +        location.path = match[6]; +        location.search = parseKeyValue(match[8]); +        location.hash = match[9] || ''; +        if (location.hash) +          location.hash = location.hash.substr(1); +        parseHash(location.hash); +      } +    } +  } +  function parseHash(hash) { +    var match = HASH_MATCH.exec(hash); +    location.hashPath = match[1] || ''; +    location.hashSearch = parseKeyValue(match[3]); +    lastHash = hash; +  } +  function toString() { +    if (lastHash === location.hash) { +      var hashKeyValue = toKeyValue(location.hashSearch), +          hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''), +          url = location.href.split('#')[0] + '#' + (hash ? hash : ''); +      if (url !== location.href) parseUrl(url); +      return url; +    } else { +      parseUrl(location.href.split('#')[0] + '#' + location.hash); +      return toString(); +    } +  } +  browser.watchUrl(function(url){ +    parseUrl(url); +    scope.$root.$eval(); +  }); +  parseUrl(browser.getUrl()); +  this.$onEval(PRIORITY_FIRST, function(){ +    if (location.hash != lastHash) { +      parseHash(location.hash); +    } +  }); +  this.$onEval(PRIORITY_LAST, function(){ +    var url = toString(); +    if (lastUrl != url) { +      browser.setUrl(url); +      lastUrl = url; +    } +  }); +  return location; +}, {inject: ['$browser']}); + +angularService("$log", function($window){ +  var console = $window.console, +      log = console && console.log || noop; +  return { +    log: log, +    warn: console && console.warn || log, +    info: console && console.info || log, +    error: console && console.error || log +  }; +}, {inject:['$window']}); + +angularService("$hover", function(browser) { +  var tooltip, self = this, error, width = 300, arrowWidth = 10; +  browser.hover(function(element, show){ +    if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { +      if (!tooltip) { +        tooltip = { +            callout: jqLite('<div id="ng-callout"></div>'), +            arrow: jqLite('<div></div>'), +            title: jqLite('<div class="ng-title"></div>'), +            content: jqLite('<div class="ng-content"></div>') +        }; +        tooltip.callout.append(tooltip.arrow); +        tooltip.callout.append(tooltip.title); +        tooltip.callout.append(tooltip.content); +        self.$browser.body.append(tooltip.callout); +      } +      var docRect = self.$browser.body[0].getBoundingClientRect(), +          elementRect = element[0].getBoundingClientRect(), +          leftSpace = docRect.right - elementRect.right - arrowWidth; +      tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); +      tooltip.content.text(error); +      if (leftSpace < width) { +        tooltip.arrow.addClass('ng-arrow-right'); +        tooltip.arrow.css({left: (width + 1)+'px'}); +        tooltip.callout.css({ +          position: 'fixed', +          left: (elementRect.left - arrowWidth - width - 4) + "px", +          top: (elementRect.top - 3) + "px", +          width: width + "px" +        }); +      } else { +        tooltip.arrow.addClass('ng-arrow-left'); +        tooltip.callout.css({ +          position: 'fixed', +          left: (elementRect.right + arrowWidth) + "px", +          top: (elementRect.top - 3) + "px", +          width: width + "px" +        }); +      } +    } else if (tooltip) { +      tooltip.callout.remove(); +      tooltip = null; +    } +  }); +}, {inject:['$browser']}); + +angularService("$invalidWidgets", function(){ +  var invalidWidgets = []; +  invalidWidgets.markValid = function(element){ +    var index = indexOf(invalidWidgets, element); +    if (index != -1) +      invalidWidgets.splice(index, 1); +  }; +  invalidWidgets.markInvalid = function(element){ +    var index = indexOf(invalidWidgets, element); +    if (index === -1) +      invalidWidgets.push(element); +  }; +  invalidWidgets.visible = function() { +    var count = 0; +    foreach(invalidWidgets, function(widget){ +      count = count + (isVisible(widget) ? 1 : 0); +    }); +    return count; +  }; +  invalidWidgets.clearOrphans = function() { +    for(var i = 0; i < invalidWidgets.length;) { +      var widget = invalidWidgets[i]; +      if (isOrphan(widget[0])) { +        invalidWidgets.splice(i, 1); +      } else { +        i++; +      } +    } +  }; +  function isOrphan(widget) { +    if (widget == window.document) return false; +    var parent = widget.parentNode; +    return !parent || isOrphan(parent); +  } +  return invalidWidgets; +}); + +function switchRouteMatcher(on, when, dstName) { +  var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', +      params = [], +      dst = {}; +  foreach(when.split(/\W/), function(param){ +    if (param) { +      var paramRegExp = new RegExp(":" + param + "([\\W])"); +      if (regex.match(paramRegExp)) { +        regex = regex.replace(paramRegExp, "([^\/]*)$1"); +        params.push(param); +      } +    } +  }); +  var match = on.match(new RegExp(regex)); +  if (match) { +    foreach(params, function(name, index){ +      dst[name] = match[index + 1]; +    }); +    if (dstName) this.$set(dstName, dst); +  } +  return match ? dst : null; +} + +angularService('$route', function(location, params){ +  var routes = {}, +      onChange = [], +      matcher = switchRouteMatcher, +      parentScope = this, +      dirty = 0, +      $route = { +        routes: routes, +        onChange: bind(onChange, onChange.push), +        when:function (path, params){ +          if (angular.isUndefined(path)) return routes; +          var route = routes[path]; +          if (!route) route = routes[path] = {}; +          if (params) angular.extend(route, params); +          dirty++; +          return route; +        } +      }; +  function updateRoute(){ +    var childScope; +    $route.current = null; +    angular.foreach(routes, function(routeParams, route) { +      if (!childScope) { +        var pathParams = matcher(location.hashPath, route); +        if (pathParams) { +          childScope = angular.scope(parentScope); +          $route.current = angular.extend({}, routeParams, { +            scope: childScope, +            params: angular.extend({}, location.hashSearch, pathParams) +          }); +        } +      } +    }); +    angular.foreach(onChange, parentScope.$tryEval); +    if (childScope) { +      childScope.$become($route.current.controller); +      parentScope.$tryEval(childScope.init); +    } +  } +  this.$watch(function(){return dirty + location.hash;}, updateRoute); +  return $route; +}, {inject: ['$location']}); + +angularService('$xhr', function($browser, $error, $log){ +  var self = this; +  return function(method, url, post, callback){ +    if (isFunction(post)) { +      callback = post; +      post = null; +    } +    if (post && isObject(post)) { +      post = toJson(post); +    } +    $browser.xhr(method, url, post, function(code, response){ +      try { +        if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { +          response = fromJson(response); +        } +        if (code == 200) { +          callback(code, response); +        } else { +          $error( +            {method: method, url:url, data:post, callback:callback}, +            {status: code, body:response}); +        } +      } catch (e) { +        $log.error(e); +      } finally { +        self.$eval(); +      } +    }); +  }; +}, {inject:['$browser', '$xhr.error', '$log']}); + +angularService('$xhr.error', function($log){ +  return function(request, response){ +    $log.error('ERROR: XHR: ' + request.url, request, response); +  }; +}, {inject:['$log']}); + +angularService('$xhr.bulk', function($xhr, $error, $log){ +  var requests = [], +      scope = this; +  function bulkXHR(method, url, post, callback) { +    if (isFunction(post)) { +      callback = post; +      post = null; +    } +    var currentQueue; +    foreach(bulkXHR.urls, function(queue){ +      if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { +        currentQueue = queue; +      } +    }); +    if (currentQueue) { +      if (!currentQueue.requests) currentQueue.requests = []; +      currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); +    } else { +      $xhr(method, url, post, callback); +    } +  } +  bulkXHR.urls = {}; +  bulkXHR.flush = function(callback){ +    foreach(bulkXHR.urls, function(queue, url){ +      var currentRequests = queue.requests; +      if (currentRequests && currentRequests.length) { +        queue.requests = []; +        queue.callbacks = []; +        $xhr('POST', url, {requests:currentRequests}, function(code, response){ +          foreach(response, function(response, i){ +            try { +              if (response.status == 200) { +                (currentRequests[i].callback || noop)(response.status, response.response); +              } else { +                $error(currentRequests[i], response); +              } +            } catch(e) { +              $log.error(e); +            } +          }); +          (callback || noop)(); +        }); +        scope.$eval(); +      } +    }); +  }; +  this.$onEval(PRIORITY_LAST, bulkXHR.flush); +  return bulkXHR; +}, {inject:['$xhr', '$xhr.error', '$log']}); + +angularService('$xhr.cache', function($xhr){ +  var inflight = {}, self = this;; +  function cache(method, url, post, callback, cacheThenRetrieve){ +    if (isFunction(post)) { +      callback = post; +      post = null; +    } +    if (method == 'GET') { +      var data; +      if (data = cache.data[url]) { +        callback(200, copy(data.value)); +        if (!cacheThenRetrieve) +          return; +      } + +      if (data = inflight[url]) { +        data.callbacks.push(callback); +      } else { +        inflight[url] = {callbacks: [callback]}; +        cache.delegate(method, url, post, function(status, response){ +          if (status == 200) +            cache.data[url] = { value: response }; +          var callbacks = inflight[url].callbacks; +          delete inflight[url]; +          foreach(callbacks, function(callback){ +            try { +              (callback||noop)(status, copy(response)); +            } catch(e) { +              self.$log.error(e); +            } +          }); +        }); +      } + +    } else { +      cache.data = {}; +      cache.delegate(method, url, post, callback); +    } +  } +  cache.data = {}; +  cache.delegate = $xhr; +  return cache; +}, {inject:['$xhr.bulk']}); + +angularService('$resource', function($xhr){ +  var resource = new ResourceFactory($xhr); +  return bind(resource, resource.route); +}, {inject: ['$xhr.cache']}); diff --git a/src/validators.js b/src/validators.js new file mode 100644 index 00000000..5c7fc952 --- /dev/null +++ b/src/validators.js @@ -0,0 +1,132 @@ +foreach({ +  'noop': function() { return null; }, + +  'regexp': function(value, regexp, msg) { +    if (!value.match(regexp)) { +      return msg || +        "Value does not match expected format " + regexp + "."; +    } else { +      return null; +    } +  }, + +  'number': function(value, min, max) { +    var num = 1 * value; +    if (num == value) { +      if (typeof min != 'undefined' && num < min) { +        return "Value can not be less than " + min + "."; +      } +      if (typeof min != 'undefined' && num > max) { +        return "Value can not be greater than " + max + "."; +      } +      return null; +    } else { +      return "Not a number"; +    } +  }, + +  'integer': function(value, min, max) { +    var numberError = angularValidator['number'](value, min, max); +    if (numberError) return numberError; +    if (!("" + value).match(/^\s*[\d+]*\s*$/) || value != Math.round(value)) { +      return "Not a whole number"; +    } +    return null; +  }, + +  'date': function(value, min, max) { +    if (value.match(/^\d\d?\/\d\d?\/\d\d\d\d$/)) { +      return null; +    } +    return "Value is not a date. (Expecting format: 12/31/2009)."; +  }, + +  'ssn': function(value) { +    if (value.match(/^\d\d\d-\d\d-\d\d\d\d$/)) { +      return null; +    } +    return "SSN needs to be in 999-99-9999 format."; +  }, + +  'email': function(value) { +    if (value.match(/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/)) { +      return null; +    } +    return "Email needs to be in username@host.com format."; +  }, + +  'phone': function(value) { +    if (value.match(/^1\(\d\d\d\)\d\d\d-\d\d\d\d$/)) { +      return null; +    } +    if (value.match(/^\+\d{2,3} (\(\d{1,5}\))?[\d ]+\d$/)) { +      return null; +    } +    return "Phone number needs to be in 1(987)654-3210 format in North America or +999 (123) 45678 906 internationaly."; +  }, + +  'url': function(value) { +    if (value.match(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/)) { +      return null; +    } +    return "URL needs to be in http://server[:port]/path format."; +  }, + +  'json': function(value) { +    try { +      fromJson(value); +      return null; +    } catch (e) { +      return e.toString(); +    } +  }, + +  /* +   * cache is attached to the element +   * cache: { +   *   inputs : { +   *     'user input': { +   *        response: server response, +   *        error: validation error +   *     }, +   *   current: 'current input' +   * } +   * +   */ +  'asynchronous': function(input, asynchronousFn, updateFn) { +    if (!input) return; +    var scope = this; +    var element = scope.$element; +    var cache = element.data('$asyncValidator'); +    if (!cache) { +      element.data('$asyncValidator', cache = {inputs:{}}); +    } + +    cache.current = input; + +    var inputState = cache.inputs[input]; +    if (!inputState) { +      cache.inputs[input] = inputState = { inFlight: true }; +      scope.$invalidWidgets.markInvalid(scope.$element); +      element.addClass('ng-input-indicator-wait'); +      asynchronousFn(input, function(error, data) { +        inputState.response = data; +        inputState.error = error; +        inputState.inFlight = false; +        if (cache.current == input) { +          element.removeClass('ng-input-indicator-wait'); +          scope.$invalidWidgets.markValid(element); +        } +        element.data('$validate')(); +        scope.$root.$eval(); +      }); +    } else if (inputState.inFlight) { +      // request in flight, mark widget invalid, but don't show it to user +      scope.$invalidWidgets.markInvalid(scope.$element); +    } else { +      (updateFn||noop)(inputState.response); +    } +    return inputState.error; +  } + +}, function(v,k) {angularValidator[k] = v;}); diff --git a/src/widgets.js b/src/widgets.js new file mode 100644 index 00000000..efafa9c5 --- /dev/null +++ b/src/widgets.js @@ -0,0 +1,328 @@ +function modelAccessor(scope, element) { +  var expr = element.attr('name'); +  if (!expr) throw "Required field 'name' not found."; +  return { +    get: function() { +      return scope.$eval(expr); +    }, +    set: function(value) { +      if (value !== undefined) { +        return scope.$tryEval(expr + '=' + toJson(value), element); +      } +    } +  }; +} + +function modelFormattedAccessor(scope, element) { +  var accessor = modelAccessor(scope, element), +      formatterName = element.attr('ng-format') || NOOP, +      formatter = angularFormatter(formatterName); +  if (!formatter) throw "Formatter named '" + formatterName + "' not found."; +  return { +    get: function() { +      return formatter.format(accessor.get()); +    }, +    set: function(value) { +      return accessor.set(formatter.parse(value)); +    } +  }; +} + +function compileValidator(expr) { +  return new Parser(expr).validator()(); +} + +function valueAccessor(scope, element) { +  var validatorName = element.attr('ng-validate') || NOOP, +      validator = compileValidator(validatorName), +      requiredExpr = element.attr('ng-required'), +      formatterName = element.attr('ng-format') || NOOP, +      formatter = angularFormatter(formatterName), +      format, parse, lastError, required; +      invalidWidgets = scope.$invalidWidgets || {markValid:noop, markInvalid:noop}; +  if (!validator) throw "Validator named '" + validatorName + "' not found."; +  if (!formatter) throw "Formatter named '" + formatterName + "' not found."; +  format = formatter.format; +  parse = formatter.parse; +  if (requiredExpr) { +    scope.$watch(requiredExpr, function(newValue) { +      required = newValue; +      validate(); +    }); +  } else { +    required = requiredExpr === ''; +  } + +  element.data('$validate', validate); +  return { +    get: function(){ +      if (lastError) +        elementError(element, NG_VALIDATION_ERROR, null); +      try { +        var value = parse(element.val()); +        validate(); +        return value; +      } catch (e) { +        lastError = e; +        elementError(element, NG_VALIDATION_ERROR, e); +      } +    }, +    set: function(value) { +      var oldValue = element.val(), +          newValue = format(value); +      if (oldValue != newValue) { +        element.val(newValue || ''); // needed for ie +      } +      validate(); +    } +  }; + +  function validate() { +    var value = trim(element.val()); +    if (element[0].disabled || element[0].readOnly) { +      elementError(element, NG_VALIDATION_ERROR, null); +      invalidWidgets.markValid(element); +    } else { +      var error, +          validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element}); +      error = required && !value ? +              'Required' : +              (value ? validator(validateScope, value) : null); +      elementError(element, NG_VALIDATION_ERROR, error); +      lastError = error; +      if (error) { +        invalidWidgets.markInvalid(element); +      } else { +        invalidWidgets.markValid(element); +      } +    } +  } +} + +function checkedAccessor(scope, element) { +  var domElement = element[0], elementValue = domElement.value; +  return { +    get: function(){ +      return !!domElement.checked; +    }, +    set: function(value){ +      domElement.checked = toBoolean(value); +    } +  }; +} + +function radioAccessor(scope, element) { +  var domElement = element[0]; +  return { +    get: function(){ +      return domElement.checked ? domElement.value : null; +    }, +    set: function(value){ +      domElement.checked = value == domElement.value; +    } +  }; +} + +function optionsAccessor(scope, 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 textWidget = inputWidget('keyup change', modelAccessor, valueAccessor, initWidgetValue()), +    buttonWidget = inputWidget('click', noopAccessor, noopAccessor, noop), +    INPUT_TYPE = { +      'text':            textWidget, +      'textarea':        textWidget, +      'hidden':          textWidget, +      'password':        textWidget, +      'button':          buttonWidget, +      'submit':          buttonWidget, +      'reset':           buttonWidget, +      'image':           buttonWidget, +      'checkbox':        inputWidget('click', modelFormattedAccessor, checkedAccessor, initWidgetValue(false)), +      'radio':           inputWidget('click', modelFormattedAccessor, radioAccessor, radioInit), +      'select-one':      inputWidget('change', modelFormattedAccessor, valueAccessor, initWidgetValue(null)), +      'select-multiple': inputWidget('change', modelFormattedAccessor, optionsAccessor, initWidgetValue([])) +//      'file':            fileWidget??? +    }; + +function initWidgetValue(initValue) { +  return function (model, view) { +    var value = view.get(); +    if (!value && isDefined(initValue)) { +      value = copy(initValue); +    } +    if (isUndefined(model.get()) && isDefined(value)) { +      model.set(value); +    } +  }; +} + +function radioInit(model, view, element) { + var modelValue = model.get(), viewValue = view.get(), input = element[0]; + input.checked = false; + input.name = this.$id + '@' + input.name; + if (isUndefined(modelValue)) { +   model.set(modelValue = null); + } + if (modelValue == null && viewValue !== null) { +   model.set(viewValue); + } + view.set(modelValue); +} + +function inputWidget(events, modelAccessor, viewAccessor, initFn) { +  return function(element) { +    var scope = this, +        model = modelAccessor(scope, element), +        view = viewAccessor(scope, element), +        action = element.attr('ng-change') || '', +        lastValue; +    initFn.call(scope, model, view, element); +    this.$eval(element.attr('ng-init')||''); +    // Don't register a handler if we are a button (noopAccessor) and there is no action +    if (action || modelAccessor !== noopAccessor) { +      element.bind(events, function(){ +        model.set(view.get()); +        lastValue = model.get(); +        scope.$tryEval(action, element); +        scope.$root.$eval(); +        // if we have noop initFn than we are just a button, +        // therefore we want to prevent default action +        return initFn != noop; +      }); +    } +    view.set(lastValue = model.get()); +    scope.$watch(model.get, function(value){ +      if (lastValue !== value) { +        view.set(lastValue = value); +      } +    }); +  }; +} + +function inputWidgetSelector(element){ +  this.directives(true); +  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); +}); + + +angularWidget('NG:INCLUDE', function(element){ +  var compiler = this, +      srcExp = element.attr("src"), +      scopeExp = element.attr("scope") || ''; +  if (element[0]['ng-compiled']) { +    this.descend(true); +    this.directives(true); +  } else { +    element[0]['ng-compiled'] = true; +    return function(element){ +      var scope = this, childScope; +      var changeCounter = 0; +      function incrementChange(){ changeCounter++;} +      this.$watch(srcExp, incrementChange); +      this.$watch(scopeExp, incrementChange); +      scope.$onEval(function(){ +        if (childScope) childScope.$eval(); +      }); +      this.$watch(function(){return changeCounter;}, function(){ +        var src = this.$eval(srcExp), +        useScope = this.$eval(scopeExp); +        if (src) { +          scope.$xhr.cache('GET', src, function(code, response){ +            element.html(response); +            childScope = useScope || createScope(scope); +            compiler.compile(element)(element, childScope); +            childScope.$init(); +          }); +        } +      }); +    }; +  } +}); + +var ngSwitch = angularWidget('NG:SWITCH', function (element){ +  var compiler = this, +      watchExpr = element.attr("on"), +      usingExpr = (element.attr("using") || 'equals'), +      usingExprParams = usingExpr.split(":"), +      usingFn = ngSwitch[usingExprParams.shift()], +      changeExpr = element.attr('change') || '', +      cases = []; +  if (!usingFn) throw "Using expression '" + usingExpr + "' unknown."; +  eachNode(element, function(caseElement){ +    var when = caseElement.attr('ng-switch-when'); +    if (when) { +      cases.push({ +        when: function(scope, value){ +          var args = [value, when]; +          foreach(usingExprParams, function(arg){ +            args.push(arg); +          }); +          return usingFn.apply(scope, args); +        }, +        change: changeExpr, +        element: caseElement, +        template: compiler.compile(caseElement) +      }); +    } +  }); + +  // this needs to be here for IE +  foreach(cases, function(_case){ +    _case.element.remove(); +  }); + +  element.html(''); +  return function(element){ +    var scope = this, childScope; +    this.$watch(watchExpr, function(value){ +      element.html(''); +      childScope = createScope(scope); +      foreach(cases, function(switchCase){ +        if (switchCase.when(childScope, value)) { +          var caseElement = switchCase.element.clone(); +          element.append(caseElement); +          childScope.$tryEval(switchCase.change, element); +          switchCase.template(caseElement, childScope); +          if (scope.$invalidWidgets) +            scope.$invalidWidgets.clearOrphans(); +          childScope.$init(); +        } +      }); +    }); +    scope.$onEval(function(){ +      if (childScope) childScope.$eval(); +    }); +  }; +}, { +  equals: function(on, when) { +    return on == when; +  }, +  route: switchRouteMatcher +});  | 
