Changeset View
Changeset View
Standalone View
Standalone View
externals/javelin/src/core/install.js
- This file was added.
| /** | |||||
| * @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 | |||||
| * | |||||
| * @group install | |||||
| */ | |||||
| 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 | |||||
| * | |||||
| * @group install | |||||
| */ | |||||
| 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' | |||||
| ]; | |||||
| })(); | |||||