|
||||||||
PREV NEXT | FRAMES NO FRAMES |
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 && 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; }
|
||||||||
PREV NEXT | FRAMES NO FRAMES |