diff options
Diffstat (limited to 'src/Widgets.js')
| -rw-r--r-- | src/Widgets.js | 774 | 
1 files changed, 774 insertions, 0 deletions
| diff --git a/src/Widgets.js b/src/Widgets.js new file mode 100644 index 00000000..de74533a --- /dev/null +++ b/src/Widgets.js @@ -0,0 +1,774 @@ +// Copyright (C) 2009 BRAT Tech LLC + + +nglr.WidgetFactory = function(serverUrl, database) { +  this.nextUploadId = 0; +  this.serverUrl = serverUrl; +  this.database = database; +  this.createSWF = swfobject.createSWF; +  this.onChangeListener = function(){}; +}; + +nglr.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; +  if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') { +    controller = new nglr.ButtonController(input[0], exp); +    event = "click"; +    bubbleEvent = false; +  } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') { +    controller = new nglr.TextController(input[0], exp); +    event = "keyup change"; +  } else if (type == 'checkbox') { +    controller = new nglr.CheckboxController(input[0], exp); +    event = "click"; +  } else if (type == 'radio') { +    controller = new nglr.RadioController(input[0], exp); +    event="click"; +  } else if (type == 'select-one') { +    controller = new nglr.SelectController(input[0], exp); +  } else if (type == 'select-multiple') { +    controller = new nglr.MultiSelectController(input[0], exp); +  } else if (type == 'file') { +    controller = this.createFileController(input, exp); +  } else { +    throw 'Unknown type: ' + type; +  } +  input.data('controller', controller); +  var binder = scope.get('$binder'); +  var action = function() { +    if (controller.updateModel(scope)) { +      var action = jQuery(controller.view).attr('ng-action') || ""; +      if (scope.evalWidget(controller, action)) { +        binder.updateView(scope); +      } +    } +    return bubbleEvent; +  }; +  jQuery(controller.view, ":input"). +    bind(event, action); +  return controller; +}; + +nglr.WidgetFactory.prototype.createFileController = function(fileInput) { +  var uploadId = '__uploadWidget_' + (this.nextUploadId++); +  var view = nglr.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 nglr.FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database); +  jQuery(swfNode).data('controller', cntl); +  return cntl; +}; + +nglr.WidgetFactory.prototype.createTextWidget = function(textInput) { +  var controller = new nglr.TextController(textInput); +  controller.onChange(this.onChangeListener); +  return controller; +}; + +///////////////////// +// FileController +/////////////////////// + +nglr.FileController = function(view, scopeName, uploader, databaseUrl) { +  this.view = view; +  this.uploader = uploader; +  this.scopeName = scopeName; +  this.attachmentsPath = databaseUrl + '/_attachments'; +  this.value = null; +  this.lastValue = undefined; +}; + +nglr.FileController.dispatchEvent = function(id, event, args) { +  var object = document.getElementById(id); +  var controller = jQuery(object).data("controller"); +  nglr.FileController.prototype['_on_' + event].apply(controller, args); +}; + +nglr.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>'); +}; + +nglr.FileController.prototype._on_cancel = function() { +}; + +nglr.FileController.prototype._on_complete = function() { +}; + +nglr.FileController.prototype._on_httpStatus = function(status) { +  nglr.alert("httpStatus:" + this.scopeName + " status:" + status); +}; + +nglr.FileController.prototype._on_ioError = function() { +  nglr.alert("ioError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_open = function() { +  nglr.alert("open:" + this.scopeName); +}; + +nglr.FileController.prototype._on_progress = function(bytesLoaded, bytesTotal) { +}; + +nglr.FileController.prototype._on_securityError = function() { +  nglr.alert("securityError:" + this.scopeName); +}; + +nglr.FileController.prototype._on_uploadCompleteData = function(data) { +  var value = nglr.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; +  scope.get('$binder').updateView(); +}; + +nglr.FileController.prototype._on_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(); +}; + +nglr.FileController.prototype.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; +  } +}; + +nglr.FileController.prototype.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); +}; + +nglr.FileController.prototype.upload = function() { +  if (this.name) { +    this.uploader.uploadFile(this.attachmentsPath); +  } +}; + + +/////////////////////// +// NullController +/////////////////////// +nglr.NullController = function(view) {this.view = view;}; +nglr.NullController.prototype.updateModel = function() { return true; }; +nglr.NullController.prototype.updateView = function() { }; +nglr.NullController.instance = new nglr.NullController(); + + +/////////////////////// +// ButtonController +/////////////////////// +nglr.ButtonController = function(view) {this.view = view;}; +nglr.ButtonController.prototype.updateModel = function(scope) { return true; }; +nglr.ButtonController.prototype.updateView = function(scope) {}; + +/////////////////////// +// TextController +/////////////////////// +nglr.TextController = function(view, exp) { +  this.view = view; +  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 = view.value; +  var widget = view.getAttribute('ng-widget'); +  if (widget === 'datepicker') { +    jQuery(view).datepicker(); +  } +}; + +nglr.TextController.prototype.updateModel = function(scope) { +  var value = this.view.value; +  if (this.lastValue === value) { +    return false; +  } else { +    scope.setEval(this.exp, value); +    this.lastValue = value; +    return true; +  } +}; + +nglr.TextController.prototype.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 != value) { +    view.value = value; +    this.lastValue = value; +  } +  var isValidationError = false; +  view.removeAttribute('ng-error'); +  if (this.required) { +    isValidationError = !(value && value.length > 0); +  } +  var errorText = isValidationError ? "Required Value" : null; +  if (!isValidationError && this.validator && value) { +    errorText = scope.validate(this.validator, value); +    isValidationError = !!errorText; +  } +  if (this.lastErrorText !== errorText) { +    this.lastErrorText = isValidationError; +    if (errorText !== null) { +      view.setAttribute('ng-error', errorText); +      scope.markInvalid(this); +    } +    jQuery(view).toggleClass('ng-validation-error', isValidationError); +  } +}; + +/////////////////////// +// CheckboxController +/////////////////////// +nglr.CheckboxController = function(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.initialValue = view.checked ? view.value : ""; +}; + +nglr.CheckboxController.prototype.updateModel = function(scope) { +  var input = this.view; +  var value = input.checked ? input.value : ''; +  if (this.lastValue === value) { +    return false; +  } else { +    scope.setEval(this.exp, value); +    this.lastValue = value; +    return true; +  } +}; + +nglr.CheckboxController.prototype.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 = input.value == (''+value); +}; + +/////////////////////// +// SelectController +/////////////////////// +nglr.SelectController = function(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.initialValue = view.value; +}; + +nglr.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; +    } +  } +}; + +nglr.SelectController.prototype.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 +/////////////////////// +nglr.MultiSelectController = function(view, exp) { +  this.view = view; +  this.exp = exp; +  this.lastValue = undefined; +  this.initialValue = this.selected(); +}; + +nglr.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; +}; + +nglr.MultiSelectController.prototype.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; +  } +}; + +nglr.MultiSelectController.prototype.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 +/////////////////////// +nglr.RadioController = function(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; +}; + +nglr.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; +  } +}; + +nglr.RadioController.prototype.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 +/////////////////////// +nglr.BindUpdater = function(view, exp) { +  this.view = view; +  this.exp = nglr.Binder.parseBindings(exp); +  this.hasError = false; +  this.scopeSelf = {element:view}; +}; + +nglr.BindUpdater.toText = function(obj) { +  var e = nglr.escapeHtml; +  switch(typeof obj) { +    case "string": +    case "boolean": +    case "number": +      return e(obj); +    case "function": +      return nglr.BindUpdater.toText(obj()); +    case "object": +      if (nglr.isNode(obj)) { +        return nglr.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 (nglr.isNode(obj.html)) +              return nglr.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(nglr.toJson(obj, true)); +    default: +      return ""; +  } +}; + +nglr.BindUpdater.prototype.updateModel = function(scope) {}; +nglr.BindUpdater.prototype.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 = nglr.Binder.binding(part); +    if (binding) { +      scope.evalWidget(this, binding, this.scopeSelf, function(value){ +        html.push(nglr.BindUpdater.toText(value)); +      }, function(e, text){ +        nglr.setHtml(this.view, text); +      }); +      if (this.hasError) { +        return; +      } +    } else { +      html.push(nglr.escapeHtml(part)); +    } +  } +  nglr.setHtml(this.view, html.join('')); +}; + +nglr.BindAttrUpdater = function(view, attrs) { +  this.view = view; +  this.attrs = attrs; +}; + +nglr.BindAttrUpdater.prototype.updateModel = function(scope) {}; +nglr.BindAttrUpdater.prototype.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 = nglr.Binder.parseBindings(attributeTemplates[attrName]); +    var attrValues = []; +    for ( var i = 0; i < attributeTemplate.length; i++) { +      var binding = nglr.Binder.binding(attributeTemplate[i]); +      if (binding) { +        try { +          var value = scope.eval(binding, {element:jNode[0], attrName:attrName}); +          if (value && (value.constructor !== nglr.array || value.length !== 0)) +            attrValues.push(value); +        } catch (e) { +          this.hasError = true; +          console.error('BindAttrUpdater', e); +          var jsonError = nglr.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.server') + '/images/blank.gif'; +    jNode.attr(attrName, attrValue); +  } +}; + +nglr.EvalUpdater = function(view, exp) { +  this.view = view; +  this.exp = exp; +  this.hasError = false; +}; +nglr.EvalUpdater.prototype.updateModel = function(scope) {}; +nglr.EvalUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp); +}; + +nglr.HideUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.HideUpdater.prototype.updateModel = function(scope) {}; +nglr.HideUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(hideValue){ +    var view = jQuery(this.view); +    if (nglr.toBoolean(hideValue)) { +      view.hide(); +    } else { +      view.show(); +    } +  }); +}; + +nglr.ShowUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ShowUpdater.prototype.updateModel = function(scope) {}; +nglr.ShowUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(hideValue){ +    var view = jQuery(this.view); +    if (nglr.toBoolean(hideValue)) { +      view.show(); +    } else { +      view.hide(); +    } +  }); +}; + +nglr.ClassUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(classValue){ +    if (classValue !== null && classValue !== undefined) { +      this.view.className = classValue; +    } +  }); +}; + +nglr.ClassEvenUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassEvenUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassEvenUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(classValue){ +    var index = scope.get('$index'); +    jQuery(this.view).toggleClass(classValue, index % 2 === 1); +  }); +}; + +nglr.ClassOddUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.ClassOddUpdater.prototype.updateModel = function(scope) {}; +nglr.ClassOddUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(classValue){ +    var index = scope.get('$index'); +    jQuery(this.view).toggleClass(classValue, index % 2 === 0); +  }); +}; + +nglr.StyleUpdater = function(view, exp) { this.view = view; this.exp = exp; }; +nglr.StyleUpdater.prototype.updateModel = function(scope) {}; +nglr.StyleUpdater.prototype.updateView = function(scope) { +  scope.evalWidget(this, this.exp, {}, function(styleValue){ +    jQuery(this.view).attr('style', "").css(styleValue); +  }); +}; + +/////////////////////// +// RepeaterUpdater +/////////////////////// +nglr.RepeaterUpdater = function(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]; +}; + +nglr.RepeaterUpdater.prototype.updateModel = function(scope) {}; +nglr.RepeaterUpdater.prototype.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 iteratorLength = iterator.length; +    var childrenLength = this.children.length; +    var cursor = this.view; +    var time = 0; +    var child = null; +    var keyExp = this.keyExp; +    var valueExp = this.valueExp; +    var i = 0; +    jQuery.each(iterator, function(key, value){ +      if (i < childrenLength) { +        // reuse children +        child = self.children[i]; +        child.scope.set(valueExp, value); +      } else { +        // grow children +        var name = self.prefix + +          valueExp + " in " + self.iteratorExp + "[" + i + "]"; +        var childScope = new nglr.Scope(scope.state, name); +        childScope.set('$index', i); +        if (keyExp) +          childScope.set(keyExp, key); +        childScope.set(valueExp, value); +        child = { scope:childScope, element:self.template(childScope, self.prefix, i) }; +        cursor.after(child.element); +        self.children.push(child); +      } +      cursor = child.element; +      var s = new Date().getTime(); +      child.scope.updateView(); +      time += new Date().getTime() - s; +      i++; +    }); +    // shrink children +    for ( var r = childrenLength; r > iteratorLength; --r) { +      var unneeded = this.children.pop(); +      unneeded.element.removeNode(); +    } +    // 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 +////////////////////////////////// + +nglr.PopUp = function(doc) { +  this.doc = doc; +}; + +nglr.PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; + +nglr.PopUp.prototype.bind = function () { +  var self = this; +  this.doc.find('.ng-validation-error,.ng-exception'). +    live("mouseover", nglr.PopUp.onOver); +}; + +nglr.PopUp.onOver = function(e) { +  nglr.PopUp.onOut(); +  var jNode = jQuery(this); +  jNode.bind(nglr.PopUp.OUT_EVENT, nglr.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; +}; + +nglr.PopUp.onOut = function() { +  jQuery('#ng-callout'). +    unbind(nglr.PopUp.OUT_EVENT, nglr.PopUp.onOut). +    remove(); +  return true; +}; + +////////////////////////////////// +// Status +////////////////////////////////// + + +nglr.Status = function(body) { +  this.loader = body.append(nglr.Status.DOM).find("#ng-loading"); +  this.requestCount = 0; +}; + +nglr.Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>'; + +nglr.Status.prototype.beginRequest = function () { +  if (this.requestCount === 0) { +    this.loader.show(); +  } +  this.requestCount++; +}; + +nglr.Status.prototype.endRequest = function () { +  this.requestCount--; +  if (this.requestCount === 0) { +    this.loader.hide("fold"); +  } +}; | 
