diff --git a/src/core/Stratcom.js b/src/core/Stratcom.js index fabe8ae..c473692 100644 --- a/src/core/Stratcom.js +++ b/src/core/Stratcom.js @@ -1,563 +1,560 @@ /** * @requires javelin-install javelin-event javelin-util javelin-magical-init * @provides javelin-stratcom * @javelin */ /** * Javelin strategic command, the master event delegation core. This class is * a sort of hybrid between Arbiter and traditional event delegation, and * serves to route event information to handlers in a general way. * * Each Javelin :JX.Event has a 'type', which may be a normal Javascript type * (for instance, a click or a keypress) or an application-defined type. It * also has a "path", based on the path in the DOM from the root node to the * event target. Note that, while the type is required, the path may be empty * (it often will be for application-defined events which do not originate * from the DOM). * * The path is determined by walking down the tree to the event target and * looking for nodes that have been tagged with metadata. These names are used * to build the event path, and unnamed nodes are ignored. Each named node may * also have data attached to it. * * Listeners specify one or more event types they are interested in handling, * and, optionally, one or more paths. A listener will only receive events * which occurred on paths it is listening to. See listen() for more details. * * @author epriestley * * @task invoke Invoking Events * @task listen Listening to Events * @task handle Responding to Events * @task sigil Managing Sigils * @task meta Managing Metadata * @task internal Internals */ JX.install('Stratcom', { statics : { ready : false, _targets : {}, _handlers : [], _need : {}, _auto : '*', _data : {}, _execContext : [], _typeMap : {focusin: 'focus', focusout: 'blur'}, /** * Node metadata is stored in a series of blocks to prevent collisions * between indexes that are generated on the server side (and potentially * concurrently). Block 0 is for metadata on the initial page load, block 1 * is for metadata added at runtime with JX.Stratcom.siglize(), and blocks * 2 and up are for metadata generated from other sources (e.g. JX.Request). * Use allocateMetadataBlock() to reserve a block, and mergeData() to fill * a block with data. * * When a JX.Request is sent, a block is allocated for it and any metadata * it returns is filled into that block. */ _dataBlock : 2, /** * Within each datablock, data is identified by a unique index. The data * pointer (data-meta attribute) on a node looks like this: * * 1_2 * * ...where 1 is the block, and 2 is the index within that block. Normally, * blocks are filled on the server side, so index allocation takes place * there. However, when data is provided with JX.Stratcom.addData(), we * need to allocate indexes on the client. */ _dataIndex : 0, /** * Dispatch a simple event that does not have a corresponding native event * object. It is unusual to call this directly. Generally, you will instead * dispatch events from an object using the invoke() method present on all * objects. See @{JX.Base.invoke()} for documentation. * * @param string Event type. * @param list? Optionally, a path to attach to the event. This is * rarely meaingful for simple events. * @param object? Optionally, arbitrary data to send with the event. * @return @{JX.Event} The event object which was dispatched to listeners. * The main use of this is to test whether any * listeners prevented the event. * @task invoke */ invoke : function(type, path, data) { var proxy = new JX.Event() .setType(type) .setData(data || {}) .setPath(path || []); return this._dispatchProxy(proxy); }, /** * Listen for events on given paths. Specify one or more event types, and * zero or more paths to filter on. If you don't specify a path, you will * receive all events of the given type: * * // Listen to all clicks. * JX.Stratcom.listen('click', null, handler); * * This will notify you of all clicks anywhere in the document (unless * they are intercepted and killed by a higher priority handler before they * get to you). * * Often, you may be interested in only clicks on certain elements. You * can specify the paths you're interested in to filter out events which * you do not want to be notified of. * * // Listen to all clicks inside elements annotated "news-feed". * JX.Stratcom.listen('click', 'news-feed', handler); * * By adding more elements to the path, you can create a finer-tuned * filter: * * // Listen to only "like" clicks inside "news-feed". * JX.Stratcom.listen('click', ['news-feed', 'like'], handler); * * * TODO: Further explain these shenanigans. * * @param string|list Event type (or list of event names) to * listen for. For example, ##'click'## or * ##['keydown', 'keyup']##. * * @param wild Sigil paths to listen for this event on. See discussion * in method documentation. * * @param function Callback to invoke when this event is triggered. It * should have the signature ##f(:JX.Event e)##. * * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @author epriestley * @task listen */ listen : function(types, paths, func) { if (__DEV__) { if (arguments.length == 4) { throw new Error( 'JX.Stratcom.listen(...): '+ 'requires exactly 3 arguments. Did you mean JX.DOM.listen?'); } if (arguments.length != 3) { throw new Error( 'JX.Stratcom.listen(...): '+ 'requires exactly 3 arguments.'); } if (typeof func != 'function') { throw new Error( 'JX.Stratcom.listen(...): '+ 'callback is not a function.'); } } var ids = []; types = JX.$AX(types); if (!paths) { paths = this._auto; } if (!JX.isArray(paths)) { paths = [[paths]]; } else if (!JX.isArray(paths[0])) { paths = [paths]; } // To listen to multiple event types on multiple paths, we just install // the same listener a whole bunch of times: if we install for two // event types on three paths, we'll end up with six references to the // listener. // // TODO: we'll call your listener twice if you install on two paths where // one path is a subset of another. The solution is "don't do that", but // it would be nice to verify that the caller isn't doing so, in __DEV__. for (var ii = 0; ii < types.length; ++ii) { var type = types[ii]; if (('onpagehide' in window) && type == 'unload') { // If we use "unload", we break the bfcache ("Back-Forward Cache") in // Safari and Firefox. The BFCache makes using the back/forward // buttons really fast since the pages can come out of magical // fairyland instead of over the network, so use "pagehide" as a proxy // for "unload" in these browsers. type = 'pagehide'; } if (!(type in this._targets)) { this._targets[type] = {}; } var type_target = this._targets[type]; for (var jj = 0; jj < paths.length; ++jj) { var path = paths[jj]; var id = this._handlers.length; this._handlers.push(func); this._need[id] = path.length; ids.push(id); for (var kk = 0; kk < path.length; ++kk) { if (__DEV__) { if (path[kk] == 'tag:#document') { throw new Error( 'JX.Stratcom.listen(..., "tag:#document", ...): ' + 'listen for all events using null, not "tag:#document"'); } if (path[kk] == 'tag:window') { throw new Error( 'JX.Stratcom.listen(..., "tag:window", ...): ' + 'listen for window events using null, not "tag:window"'); } } - if (!type_target[path[kk]]) { - type_target[path[kk]] = []; - } - type_target[path[kk]].push(id); + (type_target[path[kk]] || (type_target[path[kk]] = [])).push(id); } } } return { remove : function() { for (var ii = 0; ii < ids.length; ii++) { delete JX.Stratcom._handlers[ids[ii]]; } } }; }, /** * Dispatch a native Javascript event through the Stratcom control flow. * Generally, this is automatically called for you by the master dispatcher * installed by ##init.js##. When you want to dispatch an application event, * you should instead call invoke(). * * @param Event Native event for dispatch. * @return :JX.Event Dispatched :JX.Event. * @task internal */ dispatch : function(event) { var path = []; var nodes = {}; var push = function(key, node) { // we explicitly only store the first occurrence of each key if (!nodes.hasOwnProperty(key)) { nodes[key] = node; path.push(key); } }; var target = event.srcElement || event.target; // Touch events may originate from text nodes, but we want to start our // traversal from the nearest Element, so we grab the parentNode instead. if (target && target.nodeType === 3) { target = target.parentNode; } // Since you can only listen by tag, id, or sigil we unset the target if // it isn't an Element. Document and window are Nodes but not Elements. if (!target || !target.getAttribute) { target = null; } var cursor = target; while (cursor && cursor.getAttribute) { push('tag:' + cursor.nodeName.toLowerCase(), cursor); var id = cursor.id; if (id) { push('id:' + id, cursor); } var sigils = cursor.getAttribute('data-sigil'); if (sigils) { sigils = sigils.split(' '); for (var ii = 0; ii < sigils.length; ii++) { push(sigils[ii], cursor); } } cursor = cursor.parentNode; } var etype = event.type; if (etype in this._typeMap) { etype = this._typeMap[etype]; } var proxy = new JX.Event() .setRawEvent(event) .setType(etype) .setTarget(target) .setNodes(nodes) .setPath(path.reverse()); //JX.log('~> '+proxy.toString()); return this._dispatchProxy(proxy); }, /** * Dispatch a previously constructed proxy :JX.Event. * * @param :JX.Event Event to dispatch. * @return :JX.Event Returns the event argument. * @task internal */ _dispatchProxy : function(proxy) { var scope = this._targets[proxy.getType()]; if (!scope) { return proxy; } var path = proxy.getPath(); var len = path.length; var hits = {}; var matches; for (var root = -1; root < len; ++root) { if (root == -1) { matches = scope[this._auto]; } else { matches = scope[path[root]]; } if (!matches) { continue; } for (var ii = 0; ii < matches.length; ++ii) { hits[matches[ii]] = (hits[matches[ii]] || 0) + 1; } } var exec = []; for (var k in hits) { if (hits[k] == this._need[k]) { var handler = this._handlers[k]; if (handler) { exec.push(handler); } } } this._execContext.push({ handlers: exec, event: proxy, cursor: 0 }); this.pass(); this._execContext.pop(); return proxy; }, /** * Pass on an event, allowing other handlers to process it. The use case * here is generally something like: * * if (JX.Stratcom.pass()) { * // something else handled the event * return; * } * // handle the event * event.prevent(); * * This allows you to install event handlers that operate at a lower * effective priority, and provide a default behavior which is overridable * by listeners. * * @return bool True if the event was stopped or prevented by another * handler. * @task handle */ pass : function() { var context = this._execContext[this._execContext.length - 1]; while (context.cursor < context.handlers.length) { var cursor = context.cursor; ++context.cursor; (context.handlers[cursor] || JX.bag)(context.event); if (context.event.getStopped()) { break; } } return context.event.getStopped() || context.event.getPrevented(); }, /** * Retrieve the event (if any) which is currently being dispatched. * * @return :JX.Event|null Event which is currently being dispatched, or * null if there is no active dispatch. * @task handle */ context : function() { var len = this._execContext.length; if (!len) { return null; } return this._execContext[len - 1].event; }, /** * Merge metadata. You must call this (even if you have no metadata) to * start the Stratcom queue. * * @param int The datablock to merge data into. * @param dict Dictionary of metadata. * @return void * @task internal */ mergeData : function(block, data) { this._data[block] = data; if (block == 0) { JX.Stratcom.ready = true; JX.flushHoldingQueue('install-init', function(fn) { fn(); }); JX.__rawEventQueue({type: 'start-queue'}); } }, /** * Determine if a node has a specific sigil. * * @param Node Node to test. * @param string Sigil to check for. * @return bool True if the node has the sigil. * * @task sigil */ hasSigil : function(node, sigil) { if (__DEV__) { if (!node || !node.getAttribute) { throw new Error( 'JX.Stratcom.hasSigil(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } } var sigils = node.getAttribute('data-sigil') || false; return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1; }, /** * Add a sigil to a node. * * @param Node Node to add the sigil to. * @param string Sigil to name the node with. * @return void * @task sigil */ addSigil: function(node, sigil) { if (__DEV__) { if (!node || !node.getAttribute) { throw new Error( 'JX.Stratcom.addSigil(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } } var sigils = node.getAttribute('data-sigil'); if (sigils && !JX.Stratcom.hasSigil(node, sigil)) { sigil = sigils + ' ' + sigil; } node.setAttribute('data-sigil', sigil); }, /** * Retrieve a node's metadata. * * @param Node Node from which to retrieve data. * @return object Data attached to the node. If no data has been attached * to the node yet, an empty object will be returned, but * subsequent calls to this method will always retrieve the * same object. * @task meta */ getData : function(node) { if (__DEV__) { if (!node || !node.getAttribute) { throw new Error( 'JX.Stratcom.getData(): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have data.'); } } var meta_id = (node.getAttribute('data-meta') || '').split('_'); if (meta_id[0] && meta_id[1]) { var block = this._data[meta_id[0]]; var index = meta_id[1]; if (block && (index in block)) { return block[index]; } } var data = {}; if (!this._data[1]) { // data block 1 is reserved for JavaScript this._data[1] = {}; } this._data[1][this._dataIndex] = data; node.setAttribute('data-meta', '1_' + (this._dataIndex++)); return data; }, /** * Add data to a node's metadata. * * @param Node Node which data should be attached to. * @param object Data to add to the node's metadata. * @return object Data attached to the node that is returned by * JX.Stratcom.getData(). * @task meta */ addData : function(node, data) { if (__DEV__) { if (!node || !node.getAttribute) { throw new Error( 'JX.Stratcom.addData(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } if (!data || typeof data != 'object') { throw new Error( 'JX.Stratcom.addData(..., ): ' + 'data to attach to node is not an object. You must use ' + 'objects, not primitives, for metadata.'); } } return JX.copy(JX.Stratcom.getData(node), data); }, /** * @task internal */ allocateMetadataBlock : function() { return this._dataBlock++; } } }); diff --git a/src/core/install.js b/src/core/install.js index 8c1dbaa..3ed32e4 100644 --- a/src/core/install.js +++ b/src/core/install.js @@ -1,440 +1,434 @@ /** * @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 * * @author epriestley */ JX.install = function(new_name, new_junk) { - if (typeof JX.install._nextObjectID == 'undefined') { - JX.install._nextObjectID = 0; - } - // If we've already installed this, something is up. if (new_name in JX) { if (__DEV__) { throw new Error( 'JX.install("' + new_name + '", ...): ' + 'trying to reinstall something that has already been installed.'); } return; } if (__DEV__) { if ('name' in new_junk) { throw new Error( '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. - if (!JX.install._queue) { - JX.install._queue = []; - } - JX.install._queue.push([new_name, new_junk]); + (JX.install._queue || (JX.install._queue = [])).push([new_name, new_junk]); + var name; do { var junk; - var name = null; 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) { - if (typeof JX.install._nextObjectID == 'undefined') { - JX.install._nextObjectID = 0; - } var name = junk.name || ''; + var k; if (__DEV__) { var valid = { construct : 1, statics : 1, members : 1, extend : 1, properties : 1, events : 1, name : 1 }; - for (var k in junk) { + for (k in junk) { if (!(k in valid)) { throw new Error( 'JX.createClass("' + name + '", {"' + k + '": ...}): ' + 'trying to create unknown property `' + k + '`.'); } } if (junk.constructor !== {}.constructor) { throw new Error( '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) { throw new Error( '<' + 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 (var k in junk.statics) { + 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 (var k in (junk.properties || {})) { - var base = k.charAt(0).toUpperCase()+k.substr(1); + 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] = (function(prop) { - return function(v) { - this[prop] = v; - return this; - }; - })(prop); - - proto['get' + base] = (function(prop) { - return function() { - return this[prop]; - }; - })(prop); + 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] == '_') { throw new Error( '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) { throw new Error( '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 (var k in junk.members) { + for (k in junk.members) { proto[k] = junk.members[k]; } // 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 (var 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__)) { throw new Error( 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__)) { throw new Error( 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__)) { throw new Error( 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() { throw new Error( this.__readable__ + '.listen(...): ' + error_message); }; Class.invoke = Class.invoke || function() { throw new Error( this.__readable__ + '.invoke(...): ' + error_message); }; proto.listen = proto.listen || function() { throw new Error( this.__class__.__readable__ + '.listen(...): ' + error_message); }; proto.invoke = proto.invoke || function() { throw new Error( this.__class__.__readable__ + '.invoke(...): ' + error_message); }; } return Class; }; +JX.install._nextObjectID = 0; JX.flushHoldingQueue('install', JX.install);