/** * @requires javelin-util * javelin-magical-init * @provides javelin-install * * @javelin-installs JX.install * @javelin-installs JX.createClass * * @javelin */ /** * Install a class into the Javelin ("JX") namespace. The first argument is the * name of the class you want to install, and the second is a map of these * attributes (all of which are optional): * * - ##construct## //(function)// Class constructor. If you don't provide one, * one will be created for you (but it will be very boring). * - ##extend## //(string)// The name of another JX-namespaced class to extend * via prototypal inheritance. * - ##members## //(map)// A map of instance methods and properties. * - ##statics## //(map)// A map of static methods and properties. * - ##initialize## //(function)// A function which will be run once, after * this class has been installed. * - ##properties## //(map)// A map of properties that should have instance * getters and setters automatically generated for them. The key is the * property name and the value is its default value. For instance, if you * provide the property "size", the installed class will have the methods * "getSize()" and "setSize()". It will **NOT** have a property ".size" * and no guarantees are made about where install is actually chosing to * store the data. The motivation here is to let you cheaply define a * stable interface and refine it later as necessary. * - ##events## //(list)// List of event types this class is capable of * emitting. * * For example: * * JX.install('Dog', { * construct : function(name) { * this.setName(name); * }, * members : { * bark : function() { * // ... * } * }, * properites : { * name : null, * } * }); * * This creates a new ##Dog## class in the ##JX## namespace: * * var d = new JX.Dog(); * d.bark(); * * Javelin classes are normal Javascript functions and generally behave in * the expected way. Some properties and methods are automatically added to * all classes: * * - ##instance.__id__## Globally unique identifier attached to each instance. * - ##prototype.__class__## Reference to the class constructor. * - ##constructor.__path__## List of path tokens used emit events. It is * probably never useful to access this directly. * - ##constructor.__readable__## Readable class name. You could use this * for introspection. * - ##constructor.__events__## //DEV ONLY!// List of events supported by * this class. * - ##constructor.listen()## Listen to all instances of this class. See * @{JX.Base}. * - ##instance.listen()## Listen to one instance of this class. See * @{JX.Base}. * - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}. * * * @param string Name of the class to install. It will appear in the JX * "namespace" (e.g., JX.Pancake). * @param map Map of properties, see method documentation. * @return void */ JX.install = function(new_name, new_junk) { // If we've already installed this, something is up. if (new_name in JX) { if (__DEV__) { JX.$E( 'JX.install("' + new_name + '", ...): ' + 'trying to reinstall something that has already been installed.'); } return; } if (__DEV__) { if ('name' in new_junk) { JX.$E( 'JX.install("' + new_name + '", {"name": ...}): ' + 'trying to install with "name" property.' + 'Either remove it or call JX.createClass directly.'); } } // Since we may end up loading things out of order (e.g., Dog extends Animal // but we load Dog first) we need to keep a list of things that we've been // asked to install but haven't yet been able to install around. (JX.install._queue || (JX.install._queue = [])).push([new_name, new_junk]); var name; do { var junk; var initialize; name = null; for (var ii = 0; ii < JX.install._queue.length; ++ii) { junk = JX.install._queue[ii][1]; if (junk.extend && !JX[junk.extend]) { // We need to extend something that we haven't been able to install // yet, so just keep this in queue. continue; } // Install time! First, get this out of the queue. name = JX.install._queue.splice(ii, 1)[0][0]; --ii; if (junk.extend) { junk.extend = JX[junk.extend]; } initialize = junk.initialize; delete junk.initialize; junk.name = 'JX.' + name; JX[name] = JX.createClass(junk); if (initialize) { if (JX['Stratcom'] && JX['Stratcom'].ready) { initialize.apply(null); } else { // This is a holding queue, defined in init.js. JX['install-init'](initialize); } } } // In effect, this exits the loop as soon as we didn't make any progress // installing things, which means we've installed everything we have the // dependencies for. } while (name); }; /** * Creates a class from a map of attributes. Requires ##extend## property to * be an actual Class object and not a "String". Supports ##name## property * to give the created Class a readable name. * * @see JX.install for description of supported attributes. * * @param junk Map of properties, see method documentation. * @return function Constructor of a class created */ JX.createClass = function(junk) { var name = junk.name || ''; var k; var ii; if (__DEV__) { var valid = { construct : 1, statics : 1, members : 1, extend : 1, properties : 1, events : 1, name : 1 }; for (k in junk) { if (!(k in valid)) { JX.$E( 'JX.createClass("' + name + '", {"' + k + '": ...}): ' + 'trying to create unknown property `' + k + '`.'); } } if (junk.constructor !== {}.constructor) { JX.$E( 'JX.createClass("' + name + '", {"constructor": ...}): ' + 'property `constructor` should be called `construct`.'); } } // First, build the constructor. If construct is just a function, this // won't change its behavior (unless you have provided a really awesome // function, in which case it will correctly punish you for your attempt // at creativity). var Class = (function(name, junk) { var result = function() { this.__id__ = '__obj__' + (++JX.install._nextObjectID); return (junk.construct || junk.extend || JX.bag).apply(this, arguments); // TODO: Allow mixins to initialize here? // TODO: Also, build mixins? }; if (__DEV__) { var inner = result; result = function() { if (this == window || this == JX) { JX.$E( '<' + Class.__readable__ + '>: ' + 'Tried to construct an instance without the "new" operator.'); } return inner.apply(this, arguments); }; } return result; })(name, junk); Class.__readable__ = name; // Copy in all the static methods and properties. for (k in junk.statics) { // Can't use JX.copy() here yet since it may not have loaded. Class[k] = junk.statics[k]; } var proto; if (junk.extend) { var Inheritance = function() {}; Inheritance.prototype = junk.extend.prototype; proto = Class.prototype = new Inheritance(); } else { proto = Class.prototype = {}; } proto.__class__ = Class; var setter = function(prop) { return function(v) { this[prop] = v; return this; }; }; var getter = function(prop) { return function(v) { return this[prop]; }; }; // Build getters and setters from the `prop' map. for (k in (junk.properties || {})) { var base = k.charAt(0).toUpperCase() + k.substr(1); var prop = '__auto__' + k; proto[prop] = junk.properties[k]; proto['set' + base] = setter(prop); proto['get' + base] = getter(prop); } if (__DEV__) { // Check for aliasing in default values of members. If we don't do this, // you can run into a problem like this: // // JX.install('List', { members : { stuff : [] }}); // // var i_love = new JX.List(); // var i_hate = new JX.List(); // // i_love.stuff.push('Psyduck'); // I love psyduck! // JX.log(i_hate.stuff); // Show stuff I hate. // // This logs ["Psyduck"] because the push operation modifies // JX.List.prototype.stuff, which is what both i_love.stuff and // i_hate.stuff resolve to. To avoid this, set the default value to // null (or any other scalar) and do "this.stuff = [];" in the // constructor. for (var member_name in junk.members) { if (junk.extend && member_name[0] == '_') { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" must not be named with ' + 'a leading underscore because it is in a subclass. Variables ' + 'are analyzed and crushed one file at a time, and crushed ' + 'member variables in subclasses alias crushed member variables ' + 'in superclasses. Remove the underscore, refactor the class so ' + 'it does not extend anything, or fix the minifier to be ' + 'capable of safely crushing subclasses.'); } var member_value = junk.members[member_name]; if (typeof member_value == 'object' && member_value !== null) { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" is not a scalar or ' + 'function. Prototypal inheritance in Javascript aliases object ' + 'references across instances so all instances are initialized ' + 'to point at the exact same object. This is almost certainly ' + 'not what you intended. Make this member static to share it ' + 'across instances, or initialize it in the constructor to ' + 'prevent reference aliasing and give each instance its own ' + 'copy of the value.'); } } } // This execution order intentionally allows you to override methods // generated from the "properties" initializer. for (k in junk.members) { proto[k] = junk.members[k]; } // IE does not enumerate some properties on objects var enumerables = JX.install._enumerables; if (junk.members && enumerables) { ii = enumerables.length; while (ii--){ var property = enumerables[ii]; if (junk.members[property]) { proto[property] = junk.members[property]; } } } // Build this ridiculous event model thing. Basically, this defines // two instance methods, invoke() and listen(), and one static method, // listen(). If you listen to an instance you get events for that // instance; if you listen to a class you get events for all instances // of that class (including instances of classes which extend it). // // This is rigged up through Stratcom. Each class has a path component // like "class:Dog", and each object has a path component like // "obj:23". When you invoke on an object, it emits an event with // a path that includes its class, all parent classes, and its object // ID. // // Calling listen() on an instance listens for just the object ID. // Calling listen() on a class listens for that class's name. This // has the effect of working properly, but installing them is pretty // messy. var parent = junk.extend || {}; var old_events = parent.__events__; var new_events = junk.events || []; var has_events = old_events || new_events.length; if (has_events) { var valid_events = {}; // If we're in dev, we build up a list of valid events (for this class // and our parent class), and then check them on listen and invoke. if (__DEV__) { for (var key in old_events || {}) { valid_events[key] = true; } for (ii = 0; ii < new_events.length; ++ii) { valid_events[junk.events[ii]] = true; } } Class.__events__ = valid_events; // Build the class name chain. Class.__name__ = 'class:' + name; var ancestry = parent.__path__ || []; Class.__path__ = ancestry.concat([Class.__name__]); proto.invoke = function(type) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.invoke("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } // Here and below, this nonstandard access notation is used to mask // these callsites from the static analyzer. JX.Stratcom is always // available by the time we hit these execution points. return JX['Stratcom'].invoke( 'obj:' + type, this.__class__.__path__.concat([this.__id__]), {args : JX.$A(arguments).slice(1)}); }; proto.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__id__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; Class.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__events__)) { JX.$E( this.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__name__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; } else if (__DEV__) { var error_message = 'class does not define any events. Pass an "events" property to ' + 'JX.createClass() to define events.'; Class.listen = Class.listen || function() { JX.$E( this.__readable__ + '.listen(...): ' + error_message); }; Class.invoke = Class.invoke || function() { JX.$E( this.__readable__ + '.invoke(...): ' + error_message); }; proto.listen = proto.listen || function() { JX.$E( this.__class__.__readable__ + '.listen(...): ' + error_message); }; proto.invoke = proto.invoke || function() { JX.$E( this.__class__.__readable__ + '.invoke(...): ' + error_message); }; } return Class; }; JX.install._nextObjectID = 0; JX.flushHoldingQueue('install', JX.install); (function() { // IE does not enter this loop. for (var i in {toString: 1}) { return; } JX.install._enumerables = [ 'toString', 'hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'constructor' ]; })();