jobject.js

Summary

No overview generated for 'jobject.js'


// Some Type constants. I don't like depending on these staying the
// same across all browsers... it probably isn't a problem but it's
// a code smell to me
FUNCTION_TYPE = typeof(function () {});
STRING_TYPE = typeof("");
OBJECT_TYPE = typeof({});

/**
  JObject - a featureful object system for Javascript/ECMAScript

  @summary JObject is a featureful object system for Javascript or
  ECMAScript, primarily intended for use by XBLinJS. Generally, 
  for something like this NIH syndrome hits full force, so I don't
  really expect anyone to use this. Nevertheless, I document it here,
  as I intend to use it and others may need to consult this.

  <p>JObject abstracts out the parts of the Widget class that aren't
  really Widget specific, namely:</p>

  <ul>
    <li>The argument passing convention, including the specifying of
        defaults.</li>
    <li>The initialization sequence (abstracted into .initialize,
        use of the "prototypeOnly" parameter to nicely create and
        pass prototypes).</li>
    <li>Most of the .setAttribute machinery, with extensibility.</li>
    <li>deriveNewJObject for derivation from old ones.</li>
    <li>Automatic (optional) creation of properties from
        attributes, in interpreters that support it.</li> 
    </ul>

  <p>Like I said, NIH probably means you'll never use this. However,
  JObjects do work very nicely with Widgets, mostly due to sharing
  their ideas on .setAttribute and such.</p>

  <p>Note that JObjects have <i>no</i> connection to DOM nodes, or 
  in fact anything else; they are pure Javascript/ECMAscript
  objects.</p>

  <p>For reference, "JObject" stands for a Jerf (or Jeremy Bowers)
  Object; I couldn't think of a better name than Object (which is 
  taken and should not be modified on such a grand scale), or
  something that was misleading ("SuperObject"?).</p>

*/
function JObject(atts, prototypeOnly) {
  if (!prototypeOnly) {
    if (atts == undefined) atts = {};
    this.addDefaultsToAttObject(atts);
    this.initData(atts);
    this.init(atts);
  }
}

/**
  This initializes data needed by JObject for bookkeeping.
*/
JObject.prototype.initData = function (atts) {
  /**
    Stores the inheritances this widget has.

    @private
  */
  this.inherits = new AttributeInheritanceManager;

  // place to store vars; consider this private, except you may
  // (varname in this.vars) acceptably, as long as you still use
  // .get(varname) and .set(varname, value) normally
  /**
    Stores the Variables for this widget.

    <p>Mostly private, but it is acceptable to say <tt>somevarname
    in this.vars</tt>, though you should still use <tt>.get/.set</tt>,
    and I can't think of a good reason to need to do that, so I'm
    going to mark this private for safety.</p>
 
    @private
  */
  this.vars = {};

  // For every .set_* or .get_* in this object, register a
  // corresponding property
  // this maintains the way XBLinJS works in non-Mozilla browsers;
  // failure to declare a property getter or setter goes to the
  // default .get/.set mechanism, rather than throwing an error.
  if (this.TRY_TO_CREATE_PROPERTIES) {
    // tracks what properties we have, so we can avoid endless loops
    this.properties = {};

    var isProperty = {};
    for (var name in this) {
      if (name.substr(0, 4) == "set_") {
        isProperty[name.substr(4)] = true;
      }
      if (name.substr(0, 4) == "get_") {
        isProperty[name.substr(4)] = true;
      }
    }
    for (var name in isProperty) {
      this.createDefaultProperty(name);
    }
  }  
}

/**
 This declares a Variable for this widget. 

 <p>Normally, this is private and you should use the .Variables
 support for declaring variables, but it is permissible to call this
 manually.</p> 

 @param varName The name of the variable, which is used in
 <tt>.*Attribute</tt>.
 @param varType A reference to a <tt>Variable</tt> implementation class. By
 default, a <tt>ValueVar</tt> will be used.
 @param value The initial value of the variable, which will be set if 
 it is anything other than undefined.
 @param extra Any extra initialization needed by some <tt>Variable</tt>
 type. 
*/
JObject.prototype.declareVariable = function (varName, varType,
                                              value, extra) { 
  if (varType == undefined) varType = ValueVar;
  this.vars[varName] = new varType(this, extra);
  if (value != undefined) {
    this.setAttribute(varName, value);
  }
}

try {
  eval("a = {}; a.b getter = function(){return 1}; a.b == 1;");
  /**
    Set to true if this Javascript instance can create properties,
    false otherwise.

    <p>This should almost certainly not be set manually.</p>
  */
  JObject.prototype.CAN_CREATE_PROPERTIES = true;
} catch (e) {
  JObject.prototype.CAN_CREATE_PROPERTIES = false;
}

/**
  Indicates whether to auto-create properties.

  <p>This should not be directly manipulated under normal
  circumstances because it can not be counted on cross-platform,
  but you should &amp;&amp; it with CAN_CREATE_PROPERTIES, which will
  be true if the JS lets you create properties and false
  otherwise. If you do this, you can change it with subclasses;
  XBLinJS uses this for instance in its Flavors.</p>

  <p>This ships defaulting to "off"; replace the false with a true to
  make this happen.</p>
*/
JObject.prototype.TRY_TO_CREATE_PROPERTIES = false && CAN_CREATE_PROPERTIES;

/**
  If we're in an environment that supports Javascript properties,
  create a property for "propName" that sets and gets the
  property using .setAttribute and .getAttribute.
*/
JObject.prototype.createDefaultProperty = function (propName) {
  var widget = this;
  if (this.TRY_TO_CREATE_PROPERTIES) {
    eval("this[propName] getter = function () { " +
    "  return widget.getAttribute(propName);};" +
    "this[propName] setter = function (value) { " +
    "  return widget.setAttribute(propName, value);}");
    this.properties[propName] = true;
  }
}

/**
  The initialization order for <tt>.init</tt> to set the remaining
  attributes; an Array of strings.

  <p>Default is to define no special order.</p>
*/
JObject.prototype.initOrder = [];

/**
  The default JObject initialization routine.

  <p>The default initialization routine is to call all attributes
  with .setAttribute(key, value). If an <tt>.initOrder</tt>, an
  array of key names, is given, those attributes will be processed in
  order, then all remaining ones will be processed in hash order.</p>

  @param atts The atts object for the initialization sequence.
*/
JObject.prototype.init = function (atts) {
  if (atts) {
    for (var idx in this.initOrder) {
      var attToSet = this.initOrder[idx];
      this.processAtt(atts, attToSet);
    }
    for (var name in atts) {
      this.processAtt(atts, name);
    }
  }
}

/**
 The declared defaults, defaulting to <tt>{}</tt>, of course.

 <p>Children wishing to override this declare their defaults as key:
 value pairs in this object. Upon widget construction, they will
 be added to the 'atts' parameter if they do not already exist.</p>
 */
JObject.prototype.defaults = {};

/**
 The name of the class of this object.

 <p>All JObjects get stored in JObjectNameToType, where they can
 be retrieved by their className.</p>
*/
JObject.prototype.className = "JObject";

/**
  The registry of JObject classes, by name.
*/
JObjectNameToType = {JObject: JObject};

/**
 Implements adding the default parameters to the atts object before 
 running the .init function.

 @private
 */
JObject.prototype.addDefaultsToAttObject = function (atts) {
  for (var name in this.defaults) {
    if (atts[name] == undefined) {
      atts[name] = this.defaults[name];
    }
  }
}

/**
  A stub func; do class-specific initialization in subclasses.
  See Widget for an example.
*/
JObject.prototype.initClass = function () {
}

/**
 Performs the default processing on the <tt>atts</tt> parameter for
 the given attribute.

 <p>The default attribute processing to perform is to take the value
 indicated by the <tt>key</tt>, call <tt>.setAttribute(key,
 value)</tt>, and delete the key out of the params object. (Thus, the 
 atts object is <i>consumed</i>; be aware of this.)</p>

 <p>For many initialization functions, it often becomes
 necessary to consume some attribute settings before the final
 <tt>.init</tt> call, because some of the attributes may affect
 the initialization itself (like what DOM nodes are constructed). This
 is in some sense bad form since it 
 means those attributes must be set at construction and can't be
 reset later, but this is often easier and sometimes unavoidable.
 You can use this function to still tap into the full
 <tt>.setAttribute</tt> machinery safely, but not completely,
 depending on when you call this.</p>

 <p>Be aware when you call this manually of where you are in the
 initialization sequence for your object and what may not be ready
 yet; for instance, if you call this before Widget.prototype.initDOM,
 the following will not have occurred:<ul><li>The .domNode will not
 have been created (unless you did it yourself), so DOM manipulation
 will not work.</li><li>The Variables will not yet have been initialized
 (because in general they require the domNode), so <tt>.setAttribute</tt>
 and <tt>.getAttribute</tt> calls that "ought" to go through them will
 not. (You'll have to defer them.)<\li><\ul></p>

 @param atts The attributes object for this widget.
 @param name Either a string identifing a name of an attribute to
 pull out of the atts and call <tt>.setAttribute</tt> with, or a list
 of strings of such names. (i.e., either <tt>.processAtt(atts,
 "name")</tt> or <tt>.processAtt(atts, ["name", "value"])</tt>.)
 This function technically should have a name of indeterminate
 plurality, but English has no such thing.<p><tt>name</tt> can safely
 point at something such that <tt>!(name in atts)</tt>, but that
 probably indicates you need another <tt>.defaults</tt> entry;
 .setAttribute will be called with the <tt>undefined</tt>.</p>
*/
JObject.prototype.processAtt = function (atts, name) {
  if (typeof name == STRING_TYPE) {
    var value = atts[name];
    this.setAttribute(name, value);
    if (name in atts) {
      delete atts[name];
    }
    return;
  }

  if (name instanceof Array) {
    for (var idx in name) {
      this.processAtt(atts, name[idx]);
    }
    return;
  }

  throw "processAtt needs either a string or an array of strings.";
}

/**
 Gets the chosen attribute through the cascade lookup described in
 detail in the HTML documentation. 

 <p>Summary: Looks for a property defined via "get_[name]" method, a
 Variable, an inherited value, or an attribute on the Widget object.</p>

 @param name The name of the attribute to retrieve.
*/
JObject.prototype.getAttribute = function (name) {
  if (typeof(this["get_" + name]) == FUNCTION_TYPE) {
    return this["get_" + name]();
  }
  if (this.vars[name]) {
    return this.vars[name].get();
  }
  if (this.inherits.hasAtt(name)) {
    return this.inherits.retrieve(name);
  }
  if (!this.properties || !this.properties[name]) {
    return this[name];
  }
  return undefined; // tried to get something with no getter
}

/**
 Synonym for "getAttribute".
*/
JObject.prototype.get = function () {
  return this.getAttribute.apply(this, arguments);
}

/**
 Sets the chosen attribute through the cascade lookup described in 
 detail in the HTML documentation. 

 <p>Summary: Looks for a property defined via "get_[name]" method, a
 DOMVariable, an inherited value, or an attribute on the Widget
 object.</p>

 @param name The name of the attribute to be set.
 @param value The value to set. What can be legitimately used as a
 value depends on the chosen target.
 @param extra If setAttribute will end up calling a function, this
 can be used to send extra stuff to that function.
*/
JObject.prototype.setAttribute = function (name, value, extra) {
  if (typeof(this["set_" + name]) == FUNCTION_TYPE) {
    this["set_" + name](value, extra);
  } else if (this.vars[name]) {
    this.vars[name].set(value, extra);
  } else if (this.inherits.hasAtt(name)) {
    this.inherits.propagate(name, value);
  } else {
    // prevent endless loops on setting properties
    // with no set_* functions
    if (!this.properties || !this.properties[name]) {
      this[name] = value;
    }
  }
  // setting failed; set a property with no setter.
  // can only happend when creating properties
}

/**
 Synonym for ".setAttribute".
*/
JObject.prototype.set = function () {
  return this.setAttribute.apply(this, arguments);
}

/**
  @private
*/
JObject.prototype.toString = function () {
  return ("[JObject (" + this.className + " instance)]");
}

/**
  Retrieve an XMLHttpRequest in a cross-platform manner.
*/
function getRequest () {
  if (window.XMLHttpRequest) {
    return new XMLHttpRequest();
  } else if (window.ActiveXObject) {
    return new ActiveXObject("Microsoft.XMLHTTP");
  } else {
    alert("Unsupported browser! If you are using Opera please upgrade to 7.0 or later");
  }
}

/**
  Add a callback to the request for when the request completes.
*/
function addRequestCallback(request, callback) {
  // Adds an 'onload' callback. In Mozilla, we can just put it on
  // request.onload, in IE, we have to massage it a bit.
  if (window.XMLHttpRequest) {
    request.onload = callback;
  } else {
    request.onreadystatechange = function () {
      if (request.readyState == 4) {
        callback();
      }
    }
  }
}

/**
  Execute a remote script in the context of this object.

  <p>This is the JObject "AJAX" support; it takes in a URL
  specification (probably relative) and optionally info to post,
  retrieves the resulting Javascript code, and executes it.</p>

  <p>The XMLHttpRequest object is returned to you, so you can
  cancel the request if you want before it completes.</p>

  <p>(Note: I am looking into how to add better error detection,
  which will likely change the function signiture in later
  versions. IE sometimes fails to make it to "readystate 4", and I
  have not yet dug in enough to know why that is.)</p>

  <p>The resulting Javascript code will be executed in the context
  of this function, but since the eval is in a handler "this"
  will actually refer to the XMLHttpRequest object. The variable
  "obj" will be available which will refer to the JObject.
  The resulting API is simply "anything the object can do, the
  server can do". There is nothing particularly special about this
  code, so feel free to customize away on your own projects. (Consider
  this more a starter function than a real solution, which, in
  my experience, always ends up customized anyhow.)</p>

  @param url The URL to read the code from; include querystring
  parameters here just as you'd see them in the browser if you like.
  Note the querystring, for reasons beyond my control, at least in
  Mozilla, is relative to the location of the jobject.js file, not 
  the using page. You should probably use absolute URLs (starting from
  /), which you can construct with appropriate manipulations from
  <tt>window.location</tt>.
  @param postData The post data to send, if any. If this is blank,
  false, or undefined, the method used on the webserver will be
  "GET". If this is defined, the method used on the webserver will
  be "POST".
  @param errorInCode The code from the server will be run in a 
  "try" block; this function should be a function which will recieve
  one parameter, the exception that resulted from executing the code
  from the server. Note this is distinct from <i>an error in the 
  retrieval of the page</i>; currently you can not pass a handler for
  that.
  @param sync Run the remote exection synchronously. This is not
  useful for deployed code and probably isn't even very useful for
  debugging; this is for the test code and only needed then since
  Javascript has no threading model. <b>Don't use this</b> unless
  you <i>really</i> know what you are doing.
*/
JObject.prototype.remoteExecute = function (url, postData,
                                            errorInCode, sync) {
  var request = getRequest();
  var obj = this;
  if (!postData) postData = "";

  request.open(postData == "" ? "GET" : "POST", url, !sync);
  addRequestCallback(request, function () {
    try {
      eval(request.responseText);
    } catch (e) {
      if (errorInCode) {
        errorInCode(e);
      }
    }
  });
  request.send(postData);
}

/**
 deriveNewJObject is the preferred way to create a new JObject class,
 since it is moderately complicated. See source or HTML documentation
 for a full description of what it does, but in summary, it
 does all the bookwork so you don't have to.

 @param newName A string, indication the name of the class to create.
 @param baseObject A string naming the base JObject class, or a
 reference to the desired base JObject class.
 @param extraInstanceInit A function that will be called with the
 newly-created instance as the only parameter. This is rarely used for
 things that don't really belong in the widget itself, but need to be
 done.
 @param extraClassInit A function that will be called as the class
 initialization, i.e., Object.prototype.initClass = extraClassInit.
 (This is necessary because you can't define it later in the code,
 as this function can't see it yet.)
*/
function deriveNewJObject (newName, baseObject, extraInstanceInit, 
                           extraClassInit) {
  if (typeof(baseObject) != FUNCTION_TYPE) {
    baseObject = JObjectNameToType[baseObject];
  }
  if (typeof(baseObject) != FUNCTION_TYPE) {
    throw ("Can't create a new JObject class named '" + newName + "' " +
           "because the baseObject could not be resolved.");
  }
  if (baseObject != JObject &&
      !(baseObject.prototype instanceof JObject)) {
    throw ("Can't create a new JObject class named '" + newName +
           "' because the given baseObject isn't a Widget.");
  }

  var objectFunc = function (atts, prototypeOnly) {
    if (!prototypeOnly) {
      if (atts == undefined) atts = {};
      this.addDefaultsToAttObject(atts);
      this.initData(atts);
      this.init(atts);
      if (extraInstanceInit) {
        extraInstanceInit(this);
      }
    }
  }

  objectFunc.prototype = new baseObject(undefined, true);
  objectFunc.prototype.className = newName;
  JObjectNameToType[newName] = objectFunc;
  window[newName] = objectFunc;

  if (extraClassInit) {
    objectFunc.prototype.initClass = extraClassInit;
  }
  
  // Finally, give the object the chance to do class specific
  // initialization
  var obj = new objectFunc(undefined, true);
  obj.initClass();
}

/**
 The AttributeInheritanceManager is a private class that implements
 the inheritance rules as defined by the framework.

 @constructor
 @private
*/
function AttributeInheritanceManager () {
}

/**
 Pass in the thing to be inherited and the node inheriting it,
 and this will store the attachment. "as" is the actual attribute 
 of the node that will be changed, defaulting to the given att.
 Think of it "attribute LENGTH on the outer Widget will be
 inherited 'as' 'size'" on the inner widget.

 @private
 */
AttributeInheritanceManager.prototype.register = function (att, node, as) {
  if (!(att in this)) {
    this[att] = [];
  }
  if (as == undefined) as = att;
  this[att].push([node, as]);
}

/** 
 Propagates the attribute setting on the widget to the registered
 nodes.

 <p>This special-cases the setting of 'className' on DOM nodes because
 while Mozilla supports it as a direct-access attribute (i.e,
 node.className), it does *not* support it in .setAttribute calls;
 there you have to use "class", where IE *requires* "className" in
 the setAttribute calls.</p>

 <p>Propagating an attribute that isn't managed by this manager
 is not an error, it just won't do anything.</p>

 @private
 */
AttributeInheritanceManager.prototype.propagate = function (att,
                                                            value) {
  var targets;

  if (att in this) {
    targets = this[att];
  } else if ("*" in this) {
    targets = this["*"];
  }
  if (targets) {
    for (var idx in targets) {
      var node = targets[idx][0];
      var attributeToSet = targets[idx][1];
      if (attributeToSet == "*") {
        attributeToSet = att; // can't redirect these, so ignore that field
      }
      if (attributeToSet == "className" &&
          !(node instanceof Widget)) {
        node.className = value;
      } else {
        node.setAttribute(attributeToSet, value);
      }
    }
  }
}

/**
 This returns whether the given attribute is handled by the
 AttributeInheritanceManager.

 <p>This implements the checking for "*".</p>
*/
AttributeInheritanceManager.prototype.hasAtt = function (attName) {
  return (attName in this) || ("*" in this);
}

/**
 This should only be called after doing a "att in AIM" check;
 this just assumes there is an att by that name here. (This
 makes the .*Attribute calls work better; otherwise you have
 to try to return some sort of in-band sentinal to say "This
 att didn't exist" to distinguish from a returned value, and
 that's *always* asking for trouble.

 @private
 */
AttributeInheritanceManager.prototype.retrieve = function (att) {
  var target;
  if (att in this) {
    target = this[att];
  } else if ("*" in this) {
    target = this["*"];
  }
  var attName = target[0][1];
  if (attName == "*") attName = att; // same as propagate
  return getAttributeFrom(target[0][0], attName);
}

/**
 @summary Variable is a superclass for defining variables that 
 work with JObject's .set and .get.

 <p>For one-off properties, create .set_* and .get_* methods. For
 multiple use properties, you can create subclasses of the Variable
 objects and re-use the code multiple times. For instance, see
 XBLinJS's ValueVar object.

 
 @constructor
 */
function Variable() {
}

/**
 get the value, through whatever method makes sense in this class
 */
Variable.prototype.get = function () {};

/**
 set the value, through whatever method makes sense in this class

 @param value The desired value; what this means will vary from 
 implementation to implementation.
 */
Variable.prototype.set = function (value) {};

/**
 Creates a one-layer-deep deep copy of another object. 

 <p>Useful in the Widget context for dynamically modifying certain things
 out of a .prototype without affecting the original.</p>
*/
function objCopy(obj) {
  var newObj = {};
  for (var att in obj) {
    newObj[att] = obj[att];
  }
  return newObj;
}



Documentation generated by JSDoc on Tue May 3 17:16:26 2005