Changeset View
Changeset View
Standalone View
Standalone View
externals/javelin/src/core/Stratcom.js
- This file was added.
| /** | |||||
| * @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. | |||||
| * | |||||
| * @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 | |||||
| * @group event | |||||
| */ | |||||
| JX.install('Stratcom', { | |||||
| statics : { | |||||
| ready : false, | |||||
| _targets : {}, | |||||
| _handlers : [], | |||||
| _need : {}, | |||||
| _auto : '*', | |||||
| _data : {}, | |||||
| _execContext : [], | |||||
| /** | |||||
| * 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 string|list? Optionally, a sigil path to attach to the event. | |||||
| * This is rarely meaningful 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) { | |||||
| if (__DEV__) { | |||||
| if (path && typeof path !== 'string' && !JX.isArray(path)) { | |||||
| throw new Error( | |||||
| 'JX.Stratcom.invoke(...): path must be a string or an array.'); | |||||
| } | |||||
| } | |||||
| path = JX.$AX(path); | |||||
| return this._dispatchProxy( | |||||
| new JX.Event() | |||||
| .setType(type) | |||||
| .setData(data || {}) | |||||
| .setPath(path || []) | |||||
| ); | |||||
| }, | |||||
| /** | |||||
| * 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<string> 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. | |||||
| * @task listen | |||||
| */ | |||||
| listen : function(types, paths, func) { | |||||
| if (__DEV__) { | |||||
| if (arguments.length != 3) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.listen(...): '+ | |||||
| 'requires exactly 3 arguments. Did you mean JX.DOM.listen?'); | |||||
| } | |||||
| if (typeof func != 'function') { | |||||
| JX.$E( | |||||
| '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]; | |||||
| } | |||||
| var listener = { _callback : func }; | |||||
| // 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(listener); | |||||
| this._need[id] = path.length; | |||||
| ids.push(id); | |||||
| for (var kk = 0; kk < path.length; ++kk) { | |||||
| if (__DEV__) { | |||||
| if (path[kk] == 'tag:#document') { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.listen(..., "tag:#document", ...): ' + | |||||
| 'listen for all events using null, not "tag:#document"'); | |||||
| } | |||||
| if (path[kk] == 'tag:window') { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.listen(..., "tag:window", ...): ' + | |||||
| 'listen for window events using null, not "tag:window"'); | |||||
| } | |||||
| } | |||||
| (type_target[path[kk]] || (type_target[path[kk]] = [])).push(id); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Add a remove function to the listener | |||||
| listener['remove'] = function() { | |||||
| if (listener._callback) { | |||||
| delete listener._callback; | |||||
| for (var ii = 0; ii < ids.length; ii++) { | |||||
| delete JX.Stratcom._handlers[ids[ii]]; | |||||
| } | |||||
| } | |||||
| }; | |||||
| return listener; | |||||
| }, | |||||
| /** | |||||
| * Sometimes you may be interested in removing a listener directly from it's | |||||
| * handler. This is possible by calling JX.Stratcom.removeCurrentListener() | |||||
| * | |||||
| * // Listen to only the first click on the page | |||||
| * JX.Stratcom.listen('click', null, function() { | |||||
| * // do interesting things | |||||
| * JX.Stratcom.removeCurrentListener(); | |||||
| * }); | |||||
| * | |||||
| * @task remove | |||||
| */ | |||||
| removeCurrentListener : function() { | |||||
| var context = this._execContext[this._execContext.length - 1]; | |||||
| var listeners = context.listeners; | |||||
| // JX.Stratcom.pass will have incremented cursor by now | |||||
| var cursor = context.cursor - 1; | |||||
| if (listeners[cursor]) { | |||||
| listeners[cursor].handler.remove(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * 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 distances = {}; | |||||
| var push = function(key, node, distance) { | |||||
| // we explicitly only store the first occurrence of each key | |||||
| if (!nodes.hasOwnProperty(key)) { | |||||
| nodes[key] = node; | |||||
| distances[key] = distance; | |||||
| 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 distance = 1; | |||||
| var cursor = target; | |||||
| while (cursor && cursor.getAttribute) { | |||||
| push('tag:' + cursor.nodeName.toLowerCase(), cursor, distance); | |||||
| var id = cursor.id; | |||||
| if (id) { | |||||
| push('id:' + id, cursor, distance); | |||||
| } | |||||
| var sigils = cursor.getAttribute('data-sigil'); | |||||
| if (sigils) { | |||||
| sigils = sigils.split(' '); | |||||
| for (var ii = 0; ii < sigils.length; ii++) { | |||||
| push(sigils[ii], cursor, distance); | |||||
| } | |||||
| } | |||||
| var auto_id = cursor.getAttribute('data-autoid'); | |||||
| if (auto_id) { | |||||
| push('autoid:' + auto_id, cursor, distance); | |||||
| } | |||||
| ++distance; | |||||
| cursor = cursor.parentNode; | |||||
| } | |||||
| var etype = event.type; | |||||
| if (etype == 'focusin') { | |||||
| etype = 'focus'; | |||||
| } else if (etype == 'focusout') { | |||||
| etype = 'blur'; | |||||
| } | |||||
| var proxy = new JX.Event() | |||||
| .setRawEvent(event) | |||||
| .setData(event.customData) | |||||
| .setType(etype) | |||||
| .setTarget(target) | |||||
| .setNodes(nodes) | |||||
| .setNodeDistances(distances) | |||||
| .setPath(path.reverse()); | |||||
| // Don't touch this for debugging purposes | |||||
| //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 distances = proxy.getNodeDistances(); | |||||
| var len = path.length; | |||||
| var hits = {}; | |||||
| var hit_distances = {}; | |||||
| var matches; | |||||
| // A large number (larger than any distance we will ever encounter), but | |||||
| // we need to do math on it in the sort function so we can't use | |||||
| // Number.POSITIVE_INFINITY. | |||||
| var far_away = 1000000; | |||||
| for (var root = -1; root < len; ++root) { | |||||
| matches = scope[(root == -1) ? this._auto : path[root]]; | |||||
| if (matches) { | |||||
| var distance = distances[path[root]] || far_away; | |||||
| for (var ii = 0; ii < matches.length; ++ii) { | |||||
| var match = matches[ii]; | |||||
| hits[match] = (hits[match] || 0) + 1; | |||||
| hit_distances[match] = Math.min( | |||||
| hit_distances[match] || distance, | |||||
| distance | |||||
| ); | |||||
| } | |||||
| } | |||||
| } | |||||
| var listeners = []; | |||||
| for (var k in hits) { | |||||
| if (hits[k] == this._need[k]) { | |||||
| var handler = this._handlers[k]; | |||||
| if (handler) { | |||||
| listeners.push({ | |||||
| distance: hit_distances[k], | |||||
| handler: handler | |||||
| }); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Sort listeners by matched sigil closest to the target node | |||||
| // Listeners with the same closest sigil are called in an undefined order | |||||
| listeners.sort(function(a, b) { | |||||
| if (__DEV__) { | |||||
| // Make sure people play by the rules. >:) | |||||
| return (a.distance - b.distance) || (Math.random() - 0.5); | |||||
| } | |||||
| return a.distance - b.distance; | |||||
| }); | |||||
| this._execContext.push({ | |||||
| listeners: listeners, | |||||
| 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]; | |||||
| var event = context.event; | |||||
| var listeners = context.listeners; | |||||
| while (context.cursor < listeners.length) { | |||||
| var cursor = context.cursor++; | |||||
| if (listeners[cursor]) { | |||||
| var handler = listeners[cursor].handler; | |||||
| handler._callback && handler._callback(event); | |||||
| } | |||||
| if (event.getStopped()) { | |||||
| break; | |||||
| } | |||||
| } | |||||
| return event.getStopped() || 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; | |||||
| return len ? this._execContext[len - 1].event : null; | |||||
| }, | |||||
| /** | |||||
| * 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) { | |||||
| if (this._data[block]) { | |||||
| if (__DEV__) { | |||||
| for (var key in data) { | |||||
| if (key in this._data[block]) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.mergeData(' + block + ', ...); is overwriting ' + | |||||
| 'existing data.'); | |||||
| } | |||||
| } | |||||
| } | |||||
| JX.copy(this._data[block], data); | |||||
| } else { | |||||
| 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) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.hasSigil(<non-element>, ...): ' + | |||||
| '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) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.addSigil(<non-element>, ...): ' + | |||||
| '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 (!JX.Stratcom.hasSigil(node, sigil)) { | |||||
| sigils += ' ' + sigil; | |||||
| } | |||||
| node.setAttribute('data-sigil', sigils); | |||||
| }, | |||||
| /** | |||||
| * 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) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.getData(<non-element>): ' + | |||||
| '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]; | |||||
| } else if (__DEV__) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.getData(<node>): Tried to access data (block ' + | |||||
| meta_id[0] + ', index ' + index + ') that was not present. This ' + | |||||
| 'probably means you are calling getData() before the block ' + | |||||
| 'is provided by mergeData().'); | |||||
| } | |||||
| } | |||||
| 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) { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.addData(<non-element>, ...): ' + | |||||
| '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') { | |||||
| JX.$E( | |||||
| 'JX.Stratcom.addData(..., <nonobject>): ' + | |||||
| '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++; | |||||
| } | |||||
| } | |||||
| }); | |||||