Index: .gitmodules
===================================================================
--- .gitmodules
+++ /dev/null
@@ -1,3 +0,0 @@
-[submodule "externals/javelin"]
- path = externals/javelin
- url = git://github.com/facebook/javelin.git
Index: externals/javelin
===================================================================
--- externals/javelin
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 32c6e43f4b8b84df940bed8ed8d073e67f6c2b28
Index: externals/javelin/.gitignore
===================================================================
--- /dev/null
+++ externals/javelin/.gitignore
@@ -0,0 +1,25 @@
+.DS_Store
+._*
+*.o
+*.so
+*.a
+
+/externals/libfbjs/parser.lex.cpp
+/externals/libfbjs/parser.yacc.cpp
+/externals/libfbjs/parser.yacc.hpp
+/externals/libfbjs/parser.yacc.output
+
+/support/javelinsymbols/javelinsymbols
+/support/jsast/jsast
+/support/jsxmin/jsxmin
+
+# Diviner artifacts
+/docs/
+/.divinercache/
+
+/support/diviner/.phutil_module_cache
+
+# Mac OSX build artifacts
+/support/jsast/jsast.dSYM/
+/support/jsxmin/jsxmin.dSYM/
+/support/javelinsymbols/javelinsymbols.dSYM/
Index: externals/javelin/LICENSE
===================================================================
--- /dev/null
+++ externals/javelin/LICENSE
@@ -0,0 +1,25 @@
+Copyright (c) 2009, Evan Priestley and Facebook, inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of Facebook, inc. nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
Index: externals/javelin/README
===================================================================
--- /dev/null
+++ externals/javelin/README
@@ -0,0 +1,33 @@
+Javelin is a performance-oriented Javascript library originally developed at
+Facebook. Learn more at .
+
+GETTING STARTED
+
+Eat a hearty breakfast. Breakfast is the most important meal of the day!
+
+
+WHAT IS JAVELIN?
+
+Javelin is a compact Javascript library built around event delegation. Its
+primary design goal is performance; it is consequently well-suited to projects
+where performance is very important. It is not as good for smaller scale
+projects where other concerns (like features or ease of development) are more
+important.
+
+
+PACKAGES
+
+Packages come in two flavors: "dev" and "min". The "dev" packages are intended
+for development, and have comments and debugging code. The "min" packages have
+the same code, but with comments and debugging information stripped out and
+symbols crushed. They are intended for use in production -- ha ha ha!
+
+
+FILES
+
+ example/ Example code.
+ LICENSE A thrilling narrative.
+ pkg/ Ready-built Javelin packages.
+ README Who knows? Could be anything.
+ src/ Raw sources for Javelin.
+ support/ Support scripts and libraries.
Index: externals/javelin/src/core/Event.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/Event.js
@@ -0,0 +1,321 @@
+/**
+ * @requires javelin-install
+ * @provides javelin-event
+ * @javelin
+ */
+
+/**
+ * A generic event, routed by @{class:JX.Stratcom}. All events within Javelin
+ * are represented by a {@class:JX.Event}, regardless of whether they originate
+ * from a native DOM event (like a mouse click) or are custom application
+ * events.
+ *
+ * See @{article:Concepts: Event Delegation} for an introduction to Javelin's
+ * event delegation model.
+ *
+ * Events have a propagation model similar to native Javascript events, in that
+ * they can be stopped with stop() (which stops them from continuing to
+ * propagate to other handlers) or prevented with prevent() (which prevents them
+ * from taking their default action, like following a link). You can do both at
+ * once with kill().
+ *
+ * @task stop Stopping Event Behaviors
+ * @task info Getting Event Information
+ * @group event
+ */
+JX.install('Event', {
+ members : {
+
+ /**
+ * Stop an event from continuing to propagate. No other handler will
+ * receive this event, but its default behavior will still occur. See
+ * ""Using Events"" for more information on the distinction between
+ * 'stopping' and 'preventing' an event. See also prevent() (which prevents
+ * an event but does not stop it) and kill() (which stops and prevents an
+ * event).
+ *
+ * @return this
+ * @task stop
+ */
+ stop : function() {
+ var r = this.getRawEvent();
+ if (r) {
+ r.cancelBubble = true;
+ r.stopPropagation && r.stopPropagation();
+ }
+ this.setStopped(true);
+ return this;
+ },
+
+
+ /**
+ * Prevent an event's default action. This depends on the event type, but
+ * the common default actions are following links, submitting forms,
+ * and typing text. Event prevention is generally used when you have a link
+ * or form which work properly without Javascript but have a specialized
+ * Javascript behavior. When you intercept the event and make the behavior
+ * occur, you prevent it to keep the browser from following the link.
+ *
+ * Preventing an event does not stop it from propagating, so other handlers
+ * will still receive it. See ""Using Events"" for more information on the
+ * distinction between 'stopping' and 'preventing' an event. See also
+ * stop() (which stops an event but does not prevent it) and kill()
+ * (which stops and prevents an event).
+ *
+ * @return this
+ * @task stop
+ */
+ prevent : function() {
+ var r = this.getRawEvent();
+ if (r) {
+ r.returnValue = false;
+ r.preventDefault && r.preventDefault();
+ }
+ this.setPrevented(true);
+ return this;
+ },
+
+
+ /**
+ * Stop and prevent an event, which stops it from propagating and prevents
+ * its defualt behavior. This is a convenience function, see stop() and
+ * prevent() for information on what it means to stop or prevent an event.
+ *
+ * @return this
+ * @task stop
+ */
+ kill : function() {
+ this.prevent();
+ this.stop();
+ return this;
+ },
+
+
+ /**
+ * Get the special key (like tab or return), if any, associated with this
+ * event. Browsers report special keys differently; this method allows you
+ * to identify a keypress in a browser-agnostic way. Note that this detects
+ * only some special keys: delete, tab, return escape, left, up, right,
+ * down.
+ *
+ * For example, if you want to react to the escape key being pressed, you
+ * could install a listener like this:
+ *
+ * JX.Stratcom.listen('keydown', 'example', function(e) {
+ * if (e.getSpecialKey() == 'esc') {
+ * JX.log("You pressed 'Escape'! Well done! Bravo!");
+ * }
+ * });
+ *
+ * @return string|null ##null## if there is no associated special key,
+ * or one of the strings 'delete', 'tab', 'return',
+ * 'esc', 'left', 'up', 'right', or 'down'.
+ * @task info
+ */
+ getSpecialKey : function() {
+ var r = this.getRawEvent();
+ if (!r || r.shiftKey) {
+ return null;
+ }
+
+ return JX.Event._keymap[r.keyCode] || null;
+ },
+
+
+ /**
+ * Get whether the mouse button associated with the mouse event is the
+ * right-side button in a browser-agnostic way.
+ *
+ * @return bool
+ * @task info
+ */
+ isRightButton : function() {
+ var r = this.getRawEvent();
+ return r.which == 3 || r.button == 2;
+ },
+
+
+ /**
+ * Determine if a click event is a normal click (left mouse button, no
+ * modifier keys).
+ *
+ * @return bool
+ * @task info
+ */
+ isNormalClick : function() {
+ if (this.getType() != 'click') {
+ return false;
+ }
+
+ var r = this.getRawEvent();
+ if (r.metaKey || r.altKey || r.ctrlkey || r.shiftKey) {
+ return false;
+ }
+
+ if (('which' in r) && (r.which != 1)) {
+ return false;
+ }
+
+ if (('button' in r) && r.button) {
+ return false;
+ }
+
+ return true;
+ },
+
+
+ /**
+ * Get the node corresponding to the specified key in this event's node map.
+ * This is a simple helper method that makes the API for accessing nodes
+ * less ugly.
+ *
+ * JX.Stratcom.listen('click', 'tag:a', function(e) {
+ * var a = e.getNode('tag:a');
+ * // do something with the link that was clicked
+ * });
+ *
+ * @param string sigil or stratcom node key
+ * @return node|null Node mapped to the specified key, or null if it the
+ * key does not exist. The available keys include:
+ * - 'tag:'+tag - first node of each type
+ * - 'id:'+id - all nodes with an id
+ * - sigil - first node of each sigil
+ * @task info
+ */
+ getNode : function(key) {
+ return this.getNodes()[key] || null;
+ },
+
+
+ /**
+ * Get the metadata associated with the node that corresponds to the key
+ * in this event's node map. This is a simple helper method that makes
+ * the API for accessing metadata associated with specific nodes less ugly.
+ *
+ * JX.Stratcom.listen('click', 'tag:a', function(event) {
+ * var anchorData = event.getNodeData('tag:a');
+ * // do something with the metadata of the link that was clicked
+ * });
+ *
+ * @param string sigil or stratcom node key
+ * @return dict dictionary of the node's metadata
+ * @task info
+ */
+ getNodeData : function(key) {
+ // Evade static analysis - JX.Stratcom
+ return JX['Stratcom'].getData(this.getNode(key));
+ }
+ },
+
+ statics : {
+ _keymap : {
+ 8 : 'delete',
+ 9 : 'tab',
+ 13 : 'return',
+ 27 : 'esc',
+ 37 : 'left',
+ 38 : 'up',
+ 39 : 'right',
+ 40 : 'down',
+ 63232 : 'up',
+ 63233 : 'down',
+ 62234 : 'left',
+ 62235 : 'right'
+ }
+ },
+
+ properties : {
+
+ /**
+ * Native Javascript event which generated this @{class:JX.Event}. Not every
+ * event is generated by a native event, so there may be ##null## in
+ * this field.
+ *
+ * @type Event|null
+ * @task info
+ */
+ rawEvent : null,
+
+ /**
+ * String describing the event type, like 'click' or 'mousedown'. This
+ * may also be an application or object event.
+ *
+ * @type string
+ * @task info
+ */
+ type : null,
+
+ /**
+ * If available, the DOM node where this event occurred. For example, if
+ * this event is a click on a button, the target will be the button which
+ * was clicked. Application events will not have a target, so this property
+ * will return the value ##null##.
+ *
+ * @type DOMNode|null
+ * @task info
+ */
+ target : null,
+
+ /**
+ * Metadata attached to nodes associated with this event.
+ *
+ * For native events, the DOM is walked from the event target to the root
+ * element. Each sigil which is encountered while walking up the tree is
+ * added to the map as a key. If the node has associated metainformation,
+ * it is set as the value; otherwise, the value is null.
+ *
+ * @type dict
+ * @task info
+ */
+ data : null,
+
+ /**
+ * Sigil path this event was activated from. TODO: explain this
+ *
+ * @type list
+ * @task info
+ */
+ path : [],
+
+ /**
+ * True if propagation of the event has been stopped. See stop().
+ *
+ * @type bool
+ * @task stop
+ */
+ stopped : false,
+
+ /**
+ * True if default behavior of the event has been prevented. See prevent().
+ *
+ * @type bool
+ * @task stop
+ */
+ prevented : false,
+
+ /**
+ * @task info
+ */
+ nodes : {},
+
+ /**
+ * @task info
+ */
+ nodeDistances : {}
+ },
+
+ /**
+ * @{class:JX.Event} installs a toString() method in ##__DEV__## which allows
+ * you to log or print events and get a reasonable representation of them:
+ *
+ * Event<'click', ['path', 'stuff'], [object HTMLDivElement]>
+ */
+ initialize : function() {
+ if (__DEV__) {
+ JX.Event.prototype.toString = function() {
+ var path = '['+this.getPath().join(', ')+']';
+ return 'Event<'+this.getType()+', '+path+', '+this.getTarget()+'>';
+ }
+ }
+ }
+});
Index: externals/javelin/src/core/Stratcom.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/Stratcom.js
@@ -0,0 +1,646 @@
+/**
+ * @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 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(, ...): ' +
+ '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(, ...): ' +
+ '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(): ' +
+ '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(): 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(, ...): ' +
+ '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(..., ): ' +
+ '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++;
+ }
+ }
+});
Index: externals/javelin/src/core/__tests__/event-stop-and-kill.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/__tests__/event-stop-and-kill.js
@@ -0,0 +1,39 @@
+/**
+ * @requires javelin-event
+ */
+
+describe('Event Stop/Kill', function() {
+ var target;
+
+ beforeEach(function() {
+ target = new JX.Event();
+ });
+
+ it('should stop an event', function() {
+ expect(target.getStopped()).toBe(false);
+ target.prevent();
+ expect(target.getStopped()).toBe(false);
+ target.stop();
+ expect(target.getStopped()).toBe(true);
+ });
+
+ it('should prevent the default action of an event', function() {
+ expect(target.getPrevented()).toBe(false);
+ target.stop();
+ expect(target.getPrevented()).toBe(false);
+ target.prevent();
+ expect(target.getPrevented()).toBe(true);
+ });
+
+ it('should kill (stop and prevent) an event', function() {
+ expect(target.getPrevented()).toBe(false);
+ expect(target.getStopped()).toBe(false);
+ target.kill();
+ expect(target.getPrevented()).toBe(true);
+ expect(target.getStopped()).toBe(true);
+ });
+});
+
+
+
+
Index: externals/javelin/src/core/__tests__/install.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/__tests__/install.js
@@ -0,0 +1,152 @@
+/**
+ * @requires javelin-install
+ */
+
+describe('Javelin Install', function() {
+
+ it('should extend from an object', function() {
+ JX.install('Animal', {
+ properties: {
+ name: 'bob'
+ }
+ });
+
+ JX.install('Dog', {
+ extend: 'Animal',
+
+ members: {
+ bark: function() {
+ return 'bow wow';
+ }
+ }
+ });
+
+ var bob = new JX.Dog();
+ expect(bob.getName()).toEqual('bob');
+ expect(bob.bark()).toEqual('bow wow');
+ });
+
+ it('should create a class', function() {
+ var Animal = JX.createClass({
+ name: 'Animal',
+
+ properties: {
+ name: 'bob'
+ }
+ });
+
+ var Dog = JX.createClass({
+ name: 'Dog',
+
+ extend: Animal,
+
+ members: {
+ bark: function() {
+ return 'bow wow';
+ }
+ }
+ });
+
+ var bob = new Dog();
+ expect(bob.getName()).toEqual('bob');
+ expect(bob.bark()).toEqual('bow wow');
+ });
+
+ it('should call base constructor when construct is not provided', function() {
+ var Base = JX.createClass({
+ name: 'Base',
+
+ construct: function() {
+ this.baseCalled = true;
+ }
+ });
+
+ var Sub = JX.createClass({
+ name: 'Sub',
+ extend: Base
+ });
+
+ var obj = new Sub();
+ expect(obj.baseCalled).toBe(true);
+ });
+
+ it('should call intialize after install', function() {
+ var initialized = false;
+ JX.install('TestClass', {
+ properties: {
+ foo: 'bar'
+ },
+ initialize: function() {
+ initialized = true;
+ }
+ });
+
+ expect(initialized).toBe(true);
+ });
+
+ it('should call base ctor when construct is not provided in JX.install',
+ function() {
+
+ JX.install('Base', {
+ construct: function() {
+ this.baseCalled = true;
+ }
+ });
+
+ JX.install('Sub', {
+ extend: 'Base'
+ });
+
+ var obj = new JX.Sub();
+ expect(obj.baseCalled).toBe(true);
+ });
+
+ it('[DEV] should throw when calling install with name', function() {
+ ensure__DEV__(true, function() {
+ expect(function() {
+ JX.install('AngryAnimal', {
+ name: 'Kitty'
+ });
+ }).toThrow();
+ });
+ });
+
+ it('[DEV] should throw when calling createClass with initialize', function() {
+ ensure__DEV__(true, function() {
+ expect(function() {
+ JX.createClass({
+ initialize: function() {
+
+ }
+ });
+ }).toThrow();
+ });
+ });
+
+ it('initialize() should be able to access the installed class', function() {
+ JX.install('SomeClassWithInitialize', {
+ initialize : function() {
+ expect(!!JX.SomeClassWithInitialize).toBe(true);
+ }
+ });
+ });
+
+ it('should work with toString and its friends', function() {
+ JX.install('NiceAnimal', {
+ members: {
+ toString: function() {
+ return 'I am very nice.';
+ },
+
+ hasOwnProperty: function() {
+ return true;
+ }
+ }
+ });
+
+ expect(new JX.NiceAnimal().toString()).toEqual('I am very nice.');
+ expect(new JX.NiceAnimal().hasOwnProperty('dont-haz')).toEqual(true);
+ });
+
+});
+
Index: externals/javelin/src/core/__tests__/stratcom.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/__tests__/stratcom.js
@@ -0,0 +1,184 @@
+/**
+ * @requires javelin-stratcom
+ * javelin-dom
+ */
+describe('Stratcom Tests', function() {
+ node1 = document.createElement('div');
+ JX.Stratcom.addSigil(node1, 'what');
+ node2 = document;
+ node3 = document.createElement('div');
+ node3.className = 'what';
+
+ it('should disallow document', function() {
+ ensure__DEV__(true, function() {
+ expect(function() {
+ JX.Stratcom.listen('click', 'tag:#document', function() {});
+ }).toThrow();
+ });
+ });
+
+ it('should disallow window', function() {
+ ensure__DEV__(true, function() {
+ expect(function() {
+ JX.Stratcom.listen('click', 'tag:window', function() {});
+ }).toThrow();
+ });
+ });
+
+ it('should test nodes for hasSigil', function() {
+ expect(JX.Stratcom.hasSigil(node1, 'what')).toBe(true);
+ expect(JX.Stratcom.hasSigil(node3, 'what')).toBe(false);
+
+ ensure__DEV__(true, function() {
+ expect(function() {
+ JX.Stratcom.hasSigil(node2, 'what');
+ }).toThrow();
+ });
+ });
+
+ it('should be able to add sigils', function() {
+ var node = document.createElement('div');
+ JX.Stratcom.addSigil(node, 'my-sigil');
+ expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true);
+ expect(JX.Stratcom.hasSigil(node, 'i-dont-haz')).toBe(false);
+ JX.Stratcom.addSigil(node, 'javelin-rocks');
+ expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true);
+ expect(JX.Stratcom.hasSigil(node, 'javelin-rocks')).toBe(true);
+
+ // Should not arbitrarily take away other sigils
+ JX.Stratcom.addSigil(node, 'javelin-rocks');
+ expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true);
+ expect(JX.Stratcom.hasSigil(node, 'javelin-rocks')).toBe(true);
+ });
+
+ it('should test dataPersistence', function() {
+ var n, d;
+
+ n = JX.$N('div');
+ d = JX.Stratcom.getData(n);
+ expect(d).toEqual({});
+ d.noise = 'quack';
+ expect(JX.Stratcom.getData(n).noise).toEqual('quack');
+
+ n = JX.$N('div');
+ JX.Stratcom.addSigil(n, 'oink');
+ d = JX.Stratcom.getData(n);
+ expect(JX.Stratcom.getData(n)).toEqual({});
+ d.noise = 'quack';
+ expect(JX.Stratcom.getData(n).noise).toEqual('quack');
+
+ ensure__DEV__(true, function(){
+ var bad_values = [false, null, undefined, 'quack'];
+ for (var ii = 0; ii < bad_values.length; ii++) {
+ n = JX.$N('div');
+ expect(function() {
+ JX.Stratcom.addSigil(n, 'oink');
+ JX.Stratcom.addData(n, bad_values[ii]);
+ }).toThrow();
+ }
+ });
+
+ });
+
+ it('should allow the merge of additional data', function() {
+ ensure__DEV__(true, function() {
+ var clown = JX.$N('div');
+ clown.setAttribute('data-meta', '0_0');
+ JX.Stratcom.mergeData('0', {'0' : 'clown'});
+
+ expect(JX.Stratcom.getData(clown)).toEqual('clown');
+
+ var town = JX.$N('div');
+ town.setAttribute('data-meta', '0_1');
+ JX.Stratcom.mergeData('0', {'1' : 'town'});
+
+ expect(JX.Stratcom.getData(clown)).toEqual('clown');
+ expect(JX.Stratcom.getData(town)).toEqual('town');
+
+ expect(function() {
+ JX.Stratcom.mergeData('0', {'0' : 'oops'});
+ }).toThrow();
+ });
+ });
+
+ it('all listeners should be called', function() {
+ ensure__DEV__(true, function() {
+ var callback_count = 0;
+ JX.Stratcom.listen('custom:eventA', null, function() {
+ callback_count++;
+ });
+
+ JX.Stratcom.listen('custom:eventA', null, function() {
+ callback_count++;
+ });
+
+ expect(callback_count).toEqual(0);
+ JX.Stratcom.invoke('custom:eventA');
+ expect(callback_count).toEqual(2);
+ });
+ });
+
+ it('removed listeners should not be called', function() {
+ ensure__DEV__(true, function() {
+ var callback_count = 0;
+ var listeners = [];
+ var remove_listeners = function() {
+ while (listeners.length) {
+ listeners.pop().remove();
+ }
+ };
+
+ listeners.push(
+ JX.Stratcom.listen('custom:eventB', null, function() {
+ callback_count++;
+ remove_listeners();
+ })
+ );
+
+ listeners.push(
+ JX.Stratcom.listen('custom:eventB', null, function() {
+ callback_count++;
+ remove_listeners();
+ })
+ );
+
+ expect(callback_count).toEqual(0);
+ JX.Stratcom.invoke('custom:eventB');
+ expect(listeners.length).toEqual(0);
+ expect(callback_count).toEqual(1);
+ });
+ });
+
+ it('should throw when accessing data in an unloaded block', function() {
+ ensure__DEV__(true, function() {
+
+ var n = JX.$N('div');
+ n.setAttribute('data-meta', '9999999_9999999');
+
+ var caught;
+ try {
+ JX.Stratcom.getData(n);
+ } catch (error) {
+ caught = error;
+ }
+
+ expect(caught instanceof Error).toEqual(true);
+ });
+ });
+
+ // it('can set data serializer', function() {
+ // var uri = new JX.URI('http://www.facebook.com/home.php?key=value');
+ // uri.setQuerySerializer(JX.PHPQuerySerializer.serialize);
+ // uri.setQueryParam('obj', {
+ // num : 1,
+ // obj : {
+ // str : 'abc',
+ // i : 123
+ // }
+ // });
+ // expect(decodeURIComponent(uri.toString())).toEqual(
+ // 'http://www.facebook.com/home.php?key=value&' +
+ // 'obj[num]=1&obj[obj][str]=abc&obj[obj][i]=123');
+ // });
+
+});
Index: externals/javelin/src/core/__tests__/util.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/__tests__/util.js
@@ -0,0 +1,85 @@
+/**
+ * @requires javelin-util
+ */
+
+describe('JX.isArray', function() {
+
+ it('should correctly identify an array', function() {
+ expect(JX.isArray([1, 2, 3])).toBe(true);
+
+ expect(JX.isArray([])).toBe(true);
+ });
+
+ it('should return false on anything that is not an array', function() {
+ expect(JX.isArray(1)).toBe(false);
+ expect(JX.isArray('a string')).toBe(false);
+ expect(JX.isArray(true)).toBe(false);
+ expect(JX.isArray(/regex/)).toBe(false);
+
+ expect(JX.isArray(new String('a super string'))).toBe(false);
+ expect(JX.isArray(new Number(42))).toBe(false);
+ expect(JX.isArray(new Boolean(false))).toBe(false);
+
+ expect(JX.isArray({})).toBe(false);
+ expect(JX.isArray({'0': 1, '1': 2, length: 2})).toBe(false);
+ expect(JX.isArray((function(){
+ return arguments;
+ })('I', 'want', 'to', 'trick', 'you'))).toBe(false);
+ });
+
+ it('should identify an array from another context as an array', function() {
+ var iframe = document.createElement('iframe');
+ var name = iframe.name = 'javelin-iframe-test';
+ iframe.style.display = 'none';
+
+ document.body.insertBefore(iframe, document.body.firstChild);
+ var doc = iframe.contentWindow.document;
+ doc.write(
+ ''
+ );
+
+ var array = MaybeArray(1, 2, 3);
+ var array2 = new MaybeArray(1);
+ array2[0] = 5;
+
+ expect(JX.isArray(array)).toBe(true);
+ expect(JX.isArray(array2)).toBe(true);
+ });
+
+});
+
+describe('JX.bind', function() {
+
+ it('should bind a function to a context', function() {
+ var object = {a: 5, b: 3};
+ JX.bind(object, function() {
+ object.b = 1;
+ })();
+ expect(object).toEqual({a: 5, b: 1});
+ });
+
+ it('should bind a function without context', function() {
+ var called;
+ JX.bind(null, function() {
+ called = true;
+ })();
+ expect(called).toBe(true);
+ });
+
+ it('should bind with arguments', function() {
+ var list = [];
+ JX.bind(null, function() {
+ list.push.apply(list, JX.$A(arguments));
+ }, 'a', 2, 'c', 4)();
+ expect(list).toEqual(['a', 2, 'c', 4]);
+ });
+
+ it('should allow to pass additional arguments', function() {
+ var list = [];
+ JX.bind(null, function() {
+ list.push.apply(list, JX.$A(arguments));
+ }, 'a', 2)('c', 4);
+ expect(list).toEqual(['a', 2, 'c', 4]);
+ });
+
+});
Index: externals/javelin/src/core/init.js
===================================================================
--- /dev/null
+++ externals/javelin/src/core/init.js
@@ -0,0 +1,224 @@
+/**
+ * Javelin core; installs Javelin and Stratcom event delegation.
+ *
+ * @provides javelin-magical-init
+ *
+ * @javelin-installs JX.__rawEventQueue
+ * @javelin-installs JX.__simulate
+ * @javelin-installs JX.__allowedEvents
+ * @javelin-installs JX.enableDispatch
+ * @javelin-installs JX.onload
+ * @javelin-installs JX.flushHoldingQueue
+ *
+ * @javelin
+ */
+(function() {
+
+ if (window.JX) {
+ return;
+ }
+
+ window.JX = {};
+
+ // The holding queues hold calls to functions (JX.install() and JX.behavior())
+ // before they load, so if you're async-loading them later in the document
+ // the page will execute correctly regardless of the order resources arrive
+ // in.
+
+ var holding_queues = {};
+
+ function makeHoldingQueue(name) {
+ if (JX[name]) {
+ return;
+ }
+ holding_queues[name] = [];
+ JX[name] = function() { holding_queues[name].push(arguments); }
+ }
+
+ JX.flushHoldingQueue = function(name, fn) {
+ for (var ii = 0; ii < holding_queues[name].length; ii++) {
+ fn.apply(null, holding_queues[name][ii]);
+ }
+ holding_queues[name] = {};
+ }
+
+ makeHoldingQueue('install');
+ makeHoldingQueue('behavior');
+ makeHoldingQueue('install-init');
+
+ window['__DEV__'] = window['__DEV__'] || 0;
+
+ var loaded = false;
+ var onload = [];
+ var master_event_queue = [];
+ var root = document.documentElement;
+ var has_add_event_listener = !!root.addEventListener;
+
+ JX.__rawEventQueue = function(what) {
+ master_event_queue.push(what);
+
+ // Evade static analysis - JX.Stratcom
+ var Stratcom = JX['Stratcom'];
+ if (Stratcom && Stratcom.ready) {
+ // Empty the queue now so that exceptions don't cause us to repeatedly
+ // try to handle events.
+ var local_queue = master_event_queue;
+ master_event_queue = [];
+ for (var ii = 0; ii < local_queue.length; ++ii) {
+ var evt = local_queue[ii];
+
+ // Sometimes IE gives us events which throw when ".type" is accessed;
+ // just ignore them since we can't meaningfully dispatch them. TODO:
+ // figure out where these are coming from.
+ try { var test = evt.type; } catch (x) { continue; }
+
+ if (!loaded && evt.type == 'domready') {
+ document.body && (document.body.id = null);
+ loaded = true;
+ for (var jj = 0; jj < onload.length; jj++) {
+ onload[jj]();
+ }
+ }
+
+ Stratcom.dispatch(evt);
+ }
+ } else {
+ var target = what.srcElement || what.target;
+ if (target &&
+ (what.type in {click: 1, submit: 1}) &&
+ target.getAttribute &&
+ target.getAttribute('data-mustcapture') === '1') {
+ what.returnValue = false;
+ what.preventDefault && what.preventDefault();
+ document.body.id = 'event_capture';
+
+ // For versions of IE that use attachEvent, the event object is somehow
+ // stored globally by reference, and all the references we push to the
+ // master_event_queue will always refer to the most recent event. We
+ // work around this by popping the useless global event off the queue,
+ // and pushing a clone of the event that was just fired using the IE's
+ // proprietary createEventObject function.
+ // see: http://msdn.microsoft.com/en-us/library/ms536390(v=vs.85).aspx
+ if (!add_event_listener && document.createEventObject) {
+ master_event_queue.pop();
+ master_event_queue.push(document.createEventObject(what));
+ }
+
+ return false;
+ }
+ }
+ }
+
+ JX.enableDispatch = function(target, type) {
+ if (__DEV__) {
+ JX.__allowedEvents[type] = true;
+ }
+
+ if (target.addEventListener) {
+ target.addEventListener(type, JX.__rawEventQueue, true);
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + type, JX.__rawEventQueue);
+ }
+ };
+
+ var document_events = [
+ 'click',
+ 'dblclick',
+ 'change',
+ 'submit',
+ 'keypress',
+ 'mousedown',
+ 'mouseover',
+ 'mouseout',
+ 'mouseup',
+ 'keyup',
+ 'keydown',
+ 'input',
+ 'drop',
+ 'dragenter',
+ 'dragleave',
+ 'dragover',
+ 'paste',
+ 'touchstart',
+ 'touchmove',
+ 'touchend',
+ 'touchcancel'
+ ];
+
+ // Simulate focus and blur in old versions of IE using focusin and focusout
+ // TODO: Document the gigantic IE mess here with focus/blur.
+ // TODO: beforeactivate/beforedeactivate?
+ // http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ if (!has_add_event_listener) {
+ document_events.push('focusin', 'focusout');
+ }
+
+ // Opera is multilol: it propagates focus / blur oddly
+ if (window.opera) {
+ document_events.push('focus', 'blur');
+ }
+
+ if (__DEV__) {
+ JX.__allowedEvents = {};
+ if ('onpagehide' in window) {
+ JX.__allowedEvents.unload = true;
+ }
+ }
+
+ for (var ii = 0; ii < document_events.length; ++ii) {
+ JX.enableDispatch(root, document_events[ii]);
+ }
+
+ // In particular, we're interested in capturing window focus/blur here so
+ // long polls can abort when the window is not focused.
+ var window_events = [
+ ('onpagehide' in window) ? 'pagehide' : 'unload',
+ 'resize',
+ 'scroll',
+ 'focus',
+ 'blur',
+ 'popstate',
+ 'hashchange'
+ ];
+
+
+ for (var ii = 0; ii < window_events.length; ++ii) {
+ JX.enableDispatch(window, window_events[ii]);
+ }
+
+ JX.__simulate = function(node, event) {
+ if (!has_add_event_listener) {
+ var e = {target: node, type: event};
+ JX.__rawEventQueue(e);
+ if (e.returnValue === false) {
+ return false;
+ }
+ }
+ };
+
+ if (has_add_event_listener) {
+ document.addEventListener('DOMContentLoaded', function() {
+ JX.__rawEventQueue({type: 'domready'});
+ }, true);
+ } else {
+ var ready =
+ "if (this.readyState == 'complete') {" +
+ "JX.__rawEventQueue({type: 'domready'});" +
+ "}";
+
+ document.write(
+ '
+
+
+= Listening to Class Events =
+
+Beyond DOM events, you can also listen to class events. Every class installed
+by Javelin has static and instance methods called ##listen## (see
+@{method:JX.Base.listen}). The static method allows you to listen for all events
+emitted by every instance of a class and its descendants:
+
+ lang=js
+ JX.Animal.listen(
+ 'meow',
+ function(e) {
+ // Listen for ANY 'meow' from any JX.Animal instance or instance which
+ // extends JX.Animal.
+ });
+
+The instance method allows you to listen for all events emitted by that
+specific instance:
+
+ lang=js
+ var cat = new JX.Cat();
+ cat.listen(
+ 'meow',
+ function(e) {
+ // Listen for 'meow' from only this cat.
+ });
+
+= Magic Sigils =
+
+Javelin implements general delegation by building and comparing sigil sets. Some
+of these sigils are not DOM sigils, but derived from other things:
+
+ - ##id:*## ID sigils are generated when an examined node has an "id" property.
+ - ##obj:*## Object sigils are generated when an event affects a class
+ instance.
+ - ##class:*## Class sigils are generated while walking an affected instance's
+ class chain.
+ - ##tag:*## Tag sigils are generated by examining the tag names of DOM nodes.
+
+For instance, you can listen to all clicks on #### tags in a document like
+this:
+
+ lang=js
+ JX.Stratcom.listen(
+ 'click',
+ 'tag:a',
+ function(e) {
+ // ...
+ });
Index: externals/javelin/src/docs/concepts/sigils_metadata.diviner
===================================================================
--- /dev/null
+++ externals/javelin/src/docs/concepts/sigils_metadata.diviner
@@ -0,0 +1,129 @@
+@title Concepts: Sigils and Metadata
+@group concepts
+
+Explains Javelin's sigils and metadata.
+
+= Overview =
+
+Javelin introduces two major concepts, "sigils" and "metadata", which are core
+parts of the library but don't generally exist in other Javascript libraries.
+Both sigils and metadata are extra information you add to the DOM which Javelin
+can access. This document explains what they are, why they exist, and how you
+use them.
+
+= Sigils =
+
+Sigils are names attached to nodes in the DOM. They behave almost exactly like
+CSS class names: sigils are strings, each node may have zero or more sigils, and
+sigils are not unique within a document. Sigils convey semantic information
+about the structure of the document.
+
+It is reasonable to think of sigils as being CSS class names in a different,
+semantic namespace.
+
+If you're emitting raw tags, you specify sigils by adding a ##data-sigil##
+attribute to a node:
+
+ lang=html
+
+
+However, this should be considered an implementation detail and you should not
+rely on it excessively. In Javelin, use @{method:JX.Stratcom.hasSigil} to test
+if a node has a given sigil, and @{method:JX.Stratcom.addSigil} to add a sigil
+to a node.
+
+Javelin uses sigils instead of CSS classes to rigidly enforce the difference
+between semantic information and style information in the document. While CSS
+classes can theoretically handle both, the conflation between semantic and style
+information in a realistic engineering environment caused a number of problems
+at Facebook, including a few silly, preventable, and unfortunately severe bugs.
+
+Javelin separates this information into different namespaces, so developers and
+designers can be confident that changing CSS classes and presentation of a
+document will never change its semantic meaning. This is also why Javelin does
+not have a method to test whether a node has a CSS class, and does not have CSS
+selectors. Unless you cheat, Javelin makes it very difficult to use CSS class
+names semantically.
+
+This is an unusual decision for a library, and quite possibly the wrong tradeoff
+in many environments. But this was a continual source of problems at Facebook's
+scale and in its culture, such that it seemed to justify the measures Javelin
+takes to prevent accidents with style information having inadvertent or
+unrealized semantic value.
+
+= Metadata =
+
+Metadata is arbitrary pieces of data attached to nodes in the DOM. Metadata can
+be (and generally is) specified on the server, when the document is constructed.
+The value of metadata is that it allows handlers which use event delegation to
+distinguish between events which occur on similar nodes. For instance, if you
+have newsfeed with several "Like" links in it, your document might look like
+this:
+
+ lang=html
+
+
+You can install a listener using Javelin event delegation (see @{article:
+Concepts: Event Delegation} for more information) like this:
+
+ lang=js
+ JX.Stratcom.listen(
+ 'click',
+ ['newsfeed', 'story', 'like'],
+ function(e) {
+ // ...
+ });
+
+This calls the function you provide when the user clicks on a "like" link, but
+you need to be able to distinguish between the different links so you can know
+which story the user is trying to like. Javelin allows you to do this by
+attaching **metadata** to each node. Metadata is attached to a node by adding a
+##data-meta## attribute which has an index into data later provided to
+@{method:JX.Stratcom.mergeData}:
+
+ lang=html
+
+ ...
+
+
+This data can now be accessed with @{method:JX.Stratcom.getData}, or with
+@{method:JX.Event.getNodeData} in an event handler:
+
+ lang=js
+ JX.Stratcom.listen(
+ 'click',
+ ['newsfeed', 'story', 'like'],
+ function(e) {
+ var id = e.getNodeData('story').storyID;
+ // ...
+ });
+
+You can also add data to a node programmatically in Javascript with
+@{method:JX.Stratcom.addData}.
Index: externals/javelin/src/docs/facebook.diviner
===================================================================
--- /dev/null
+++ externals/javelin/src/docs/facebook.diviner
@@ -0,0 +1,82 @@
+@title Javelin at Facebook
+@group facebook
+
+Information specific to Javelin at Facebook.
+
+= Building Support Scripts =
+
+Javelin now ships with the source to build several libfbjs-based binaries, which
+serve to completely sever its dependencies on trunk:
+
+ - ##javelinsymbols##: used for lint
+ - ##jsast##: used for documentation generation
+ - ##jsxmin##: used to crush packages
+
+To build these, first build libfbjs:
+
+ javelin/ $ cd externals/libfbjs
+ javelin/externals/libfbjs/ $ CXX=/usr/bin/g++ make
+
+Note that **you must specify CXX explicitly because the default CXX is broken**.
+
+Now you should be able to build the individual binaries:
+
+ javelin/ $ cd support/javelinsymbols
+ javelin/support/javelinsymbols $ CXX=/usr/bin/g++ make
+
+ javelin/ $ cd support/jsast
+ javelin/support/jsast $ CXX=/usr/bin/g++ make
+
+ javelin/ $ cd support/jsxmin
+ javelin/support/jsxmin $ CXX=/usr/bin/g++ make
+
+= Synchronizing Javelin =
+
+To synchronize Javelin **from** Facebook trunk, run the synchronize script:
+
+ javelin/ $ ./scripts/sync-from-facebook.php ~/www
+
+...where ##~/www## is the root you want to pull Javelin files from. The script
+will copy files out of ##html/js/javelin## and build packages, and leave the
+results in your working copy. From there you can review changes and commit, and
+then push, diff, or send a pull request.
+
+To synchronize Javelin **to** Facebook trunk, run the, uh, reverse-synchronize
+script:
+
+ javelin/ $ ./scripts/sync-to-facebook.php ~/www
+
+...where ##~/www## is the root you want to push Javelin files to. The script
+will copy files out of the working copy into your ##www## and leave you with a
+dirty ##www##. From there you can review changes.
+
+Once Facebook moves to pure git for ##www## we can probably just submodule
+Javelin into it and get rid of all this nonsense, but the mixed SVN/git
+environment makes that difficult until then.
+
+= Building Documentation =
+
+Check out ##diviner## and ##libphutil## from Facebook github, and put them in a
+directory with ##javelin##:
+
+ somewhere/ $ ls
+ diviner/
+ javelin/
+ libphutil/
+ somewhere/ $
+
+Now run ##diviner## on ##javelin##:
+
+ somewhere/ $ cd javelin
+ somewhere/javelin/ $ ../diviner/bin/diviner .
+ [DivinerArticleEngine] Generating documentation for 48 files...
+ [JavelinDivinerEngine] Generating documentation for 74 files...
+ somewhere/javelin/ $
+
+Documentation is now available in ##javelin/docs/##.
+
+= Editing javelinjs.com =
+
+The source for javelinjs.com lives in ##javelin/support/webroot/##. The site
+itself is served off the phabricator.com host. You need access to that host to
+push it.
Index: externals/javelin/src/docs/onload.js
===================================================================
--- /dev/null
+++ externals/javelin/src/docs/onload.js
@@ -0,0 +1,22 @@
+/**
+ * @javelin
+ */
+
+/**
+ * Register a callback for invocation after DOMContentReady.
+ *
+ * NOTE: Although it isn't private, use of this function is heavily discouraged.
+ * See @{article:Concepts: Behaviors} for information on using behaviors to
+ * structure and invoke glue code.
+ *
+ * This function is defined as a side effect of init.js.
+ *
+ * @param function Callback function to invoke after DOMContentReady.
+ * @return void
+ * @group util
+ */
+JX.onload = function(callback) {
+ // This isn't the real function definition, it's only defined here to let the
+ // documentation generator find it. The actual definition is in init.js.
+};
+
Index: externals/javelin/src/ext/fx/Color.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/fx/Color.js
@@ -0,0 +1,33 @@
+/**
+ * @provides javelin-color
+ * @requires javelin-install
+ * @javelin
+ */
+
+JX.install('Color', {
+
+ statics: {
+
+ rgbRegex: new RegExp('([\\d]{1,3})', 'g'),
+
+ rgbToHex: function(str, as_array) {
+ var rgb = str.match(JX.Color.rgbRegex);
+ var hex = [0, 1, 2].map(function(index) {
+ return ('0' + (rgb[index] - 0).toString(16)).substr(-2, 2);
+ });
+ return as_array ? hex : '#' + hex.join('');
+ },
+
+ hexRegex: new RegExp('^[#]{0,1}([\\w]{1,2})([\\w]{1,2})([\\w]{1,2})$'),
+
+ hexToRgb: function(str, as_array) {
+ var hex = str.match(JX.Color.hexRegex);
+ var rgb = hex.slice(1).map(function(bit) {
+ return parseInt(bit.length == 1 ? bit + bit : bit, 16);
+ });
+ return as_array ? rgb : 'rgb(' + rgb + ')';
+ }
+
+ }
+
+});
Index: externals/javelin/src/ext/fx/FX.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/fx/FX.js
@@ -0,0 +1,214 @@
+/**
+ * @provides javelin-fx
+ * @requires javelin-color javelin-install javelin-util
+ * @javelin
+ *
+ * Based on moo.fx (moofx.mad4milk.net).
+ */
+
+JX.install('FX', {
+
+ events: ['start', 'complete'],
+
+ construct: function(element) {
+ this._config = {};
+ this.setElement(element);
+ this.setTransition(JX.FX.Transitions.sine);
+ },
+
+ properties: {
+ fps: 50,
+ wait: true,
+ duration: 500,
+ element: null,
+ property: null,
+ transition: null
+ },
+
+ members: {
+ _to: null,
+ _now: null,
+ _from: null,
+ _start: null,
+ _config: null,
+ _interval: null,
+
+ start: function(config) {
+ if (__DEV__) {
+ if (!config) {
+ throw new Error('What styles do you want to animate?');
+ }
+ if (!this.getElement()) {
+ throw new Error('What element do you want to animate?');
+ }
+ }
+ if (this._interval && this.getWait()) {
+ return;
+ }
+ var from = {};
+ var to = {};
+ for (var prop in config) {
+ from[prop] = config[prop][0];
+ to[prop] = config[prop][1];
+ if (/color/i.test(prop)) {
+ from[prop] = JX.Color.hexToRgb(from[prop], true);
+ to[prop] = JX.Color.hexToRgb(to[prop], true);
+ }
+ }
+ this._animate(from, to);
+ return this;
+ },
+
+ stop: function() {
+ clearInterval(this._interval);
+ this._interval = null;
+ return this;
+ },
+
+ then: function(func) {
+ var token = this.listen('complete', function() {
+ token.remove();
+ func();
+ });
+ return this;
+ },
+
+ _animate: function(from, to) {
+ if (!this.getWait()) {
+ this.stop();
+ }
+ if (this._interval) {
+ return;
+ }
+ setTimeout(JX.bind(this, this.invoke, 'start'), 10);
+ this._from = from;
+ this._to = to;
+ this._start = JX.now();
+ this._interval = setInterval(
+ JX.bind(this, this._tween),
+ Math.round(1000 / this.getFps()));
+ },
+
+ _tween: function() {
+ var now = JX.now();
+ var prop;
+ if (now < this._start + this.getDuration()) {
+ this._now = now - this._start;
+ for (prop in this._from) {
+ this._config[prop] = this._compute(this._from[prop], this._to[prop]);
+ }
+ } else {
+ setTimeout(JX.bind(this, this.invoke, 'complete'), 10);
+
+ // Compute the final position using the transition function, in case
+ // the function applies transformations.
+ this._now = this.getDuration();
+ for (prop in this._from) {
+ this._config[prop] = this._compute(this._from[prop], this._to[prop]);
+ }
+ this.stop();
+ }
+ this._render();
+ },
+
+ _compute: function(from, to) {
+ if (JX.isArray(from)) {
+ return from.map(function(value, ii) {
+ return Math.round(this._compute(value, to[ii]));
+ }, this);
+ }
+ var delta = to - from;
+ return this.getTransition()(this._now, from, delta, this.getDuration());
+ },
+
+ _render: function() {
+ var style = this.getElement().style;
+ for (var prop in this._config) {
+ var value = this._config[prop];
+ if (prop == 'opacity') {
+ value = parseInt(100 * value, 10);
+ if (window.ActiveXObject) {
+ style.filter = 'alpha(opacity=' + value + ')';
+ } else {
+ style.opacity = value / 100;
+ }
+ } else if (/color/i.test(prop)) {
+ style[prop] = 'rgb(' + value + ')';
+ } else {
+ style[prop] = value + 'px';
+ }
+ }
+ }
+ },
+
+ statics: {
+ fade: function(element, visible) {
+ return new JX.FX(element).setDuration(250).start({
+ opacity: visible ? [0, 1] : [1, 0]
+ });
+ },
+
+ highlight: function(element, color) {
+ color = color || '#fff8dd';
+ return new JX.FX(element).setDuration(1000).start({
+ backgroundColor: [color, '#fff']
+ });
+ },
+
+ /**
+ * Easing equations based on work by Robert Penner
+ * http://www.robertpenner.com/easing/
+ */
+ Transitions: {
+ linear: function(t, b, c, d) {
+ return c * t / d + b;
+ },
+
+ sine: function(t, b, c, d) {
+ return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
+ },
+
+ sineIn: function(t, b, c, d) {
+ if (t == d) {
+ return c + b;
+ }
+ return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
+ },
+
+ sineOut: function(t, b, c, d) {
+ if (t == d) {
+ return c + b;
+ }
+ return c * Math.sin(t / d * (Math.PI / 2)) + b;
+ },
+
+ elastic: function(t, b, c, d, a, p) {
+ if (t === 0) { return b; }
+ if ((t /= d) == 1) { return b + c; }
+ if (!p) { p = d * 0.3; }
+ if (!a) { a = 1; }
+ var s;
+ if (a < Math.abs(c)) {
+ a = c;
+ s = p / 4;
+ } else {
+ s = p / (2 * Math.PI) * Math.asin(c / a);
+ }
+ return a * Math.pow(2, -10 * t) *
+ Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b;
+ },
+
+ bounce: function(t, b, c, d) {
+ if ((t /= d) < (1 / 2.75)) {
+ return c * (7.5625 * t * t) + b;
+ } else if (t < (2 / 2.75)) {
+ return c * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75) + b;
+ } else if (t < (2.5 / 2.75)) {
+ return c * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375) + b;
+ } else {
+ return c * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375) + b;
+ }
+ }
+ }
+ }
+});
Index: externals/javelin/src/ext/reactor/core/DynVal.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/reactor/core/DynVal.js
@@ -0,0 +1,48 @@
+/**
+ * @provides javelin-dynval
+ * @requires javelin-install
+ * javelin-reactornode
+ * javelin-util
+ * javelin-reactor
+ * @javelin
+ */
+
+JX.install('DynVal', {
+ members : {
+ _lastPulseVal : null,
+ _reactorNode : null,
+ getValueNow : function() {
+ return this._lastPulseVal;
+ },
+ getChanges : function() {
+ return this._reactorNode;
+ },
+ forceValueNow : function(value) {
+ this.getChanges().forceSendValue(value);
+ },
+ transform : function(fn) {
+ return new JX.DynVal(
+ this.getChanges().transform(fn),
+ fn(this.getValueNow())
+ );
+ },
+ calm : function(min_interval) {
+ return new JX.DynVal(
+ this.getChanges().calm(min_interval),
+ this.getValueNow()
+ );
+ }
+ },
+ construct : function(stream, init) {
+ this._lastPulseVal = init;
+ this._reactorNode =
+ new JX.ReactorNode([stream], JX.bind(this, function(pulse) {
+ if (this._lastPulseVal == pulse) {
+ return JX.Reactor.DoNotPropagate;
+ }
+ this._lastPulseVal = pulse;
+ return pulse;
+ }));
+ }
+});
+
Index: externals/javelin/src/ext/reactor/core/Reactor.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/reactor/core/Reactor.js
@@ -0,0 +1,90 @@
+/**
+ * @provides javelin-reactor
+ * @requires javelin-install
+ * javelin-util
+ * @javelin
+ */
+
+JX.install('Reactor', {
+ statics : {
+ /**
+ * Return this value from a ReactorNode transformer to indicate that
+ * its listeners should not be activated.
+ */
+ DoNotPropagate : {},
+ /**
+ * For internal use by the Reactor system.
+ */
+ propagatePulse : function(start_pulse, start_node) {
+ var reverse_post_order =
+ JX.Reactor._postOrder(start_node).reverse();
+ start_node.primeValue(start_pulse);
+
+ for (var ix = 0; ix < reverse_post_order.length; ix++) {
+ var node = reverse_post_order[ix];
+ var pulse = node.getNextPulse();
+ if (pulse === JX.Reactor.DoNotPropagate) {
+ continue;
+ }
+
+ var next_pulse = node.getTransformer()(pulse);
+ var sends_to = node.getListeners();
+ for (var jx = 0; jx < sends_to.length; jx++) {
+ sends_to[jx].primeValue(next_pulse);
+ }
+ }
+ },
+ /**
+ * For internal use by the Reactor system.
+ */
+ _postOrder : function(node, result, pending) {
+ if (typeof result === "undefined") {
+ result = [];
+ pending = {};
+ }
+ pending[node.getGraphID()] = true;
+
+ var nexts = node.getListeners();
+ for (var ix = 0; ix < nexts.length; ix++) {
+ var next = nexts[ix];
+ if (pending[next.getGraphID()]) {
+ continue;
+ }
+ JX.Reactor._postOrder(next, result, pending);
+ }
+
+ result.push(node);
+ return result;
+ },
+
+ // Helper for lift.
+ _valueNow : function(fn, dynvals) {
+ var values = [];
+ for (var ix = 0; ix < dynvals.length; ix++) {
+ values.push(dynvals[ix].getValueNow());
+ }
+ return fn.apply(null, values);
+ },
+
+ /**
+ * Lift a function over normal values to be a function over dynvals.
+ * @param fn A function expecting normal values
+ * @param dynvals Array of DynVals whose instaneous values will be passed
+ * to fn.
+ * @return A DynVal representing the changing value of fn applies to dynvals
+ * over time.
+ */
+ lift : function(fn, dynvals) {
+ var valueNow = JX.bind(null, JX.Reactor._valueNow, fn, dynvals);
+
+ var streams = [];
+ for (var ix = 0; ix < dynvals.length; ix++) {
+ streams.push(dynvals[ix].getChanges());
+ }
+
+ var result = new JX['ReactorNode'](streams, valueNow);
+ return new JX['DynVal'](result, valueNow());
+ }
+ }
+});
+
Index: externals/javelin/src/ext/reactor/core/ReactorNode.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/reactor/core/ReactorNode.js
@@ -0,0 +1,97 @@
+/**
+ * @provides javelin-reactornode
+ * @requires javelin-install
+ * javelin-reactor
+ * javelin-util
+ * javelin-reactor-node-calmer
+ * @javelin
+ */
+
+JX.install('ReactorNode', {
+ members : {
+ _transformer : null,
+ _sendsTo : null,
+ _nextPulse : null,
+ _graphID : null,
+
+ getGraphID : function() {
+ return this._graphID || this.__id__;
+ },
+
+ setGraphID : function(id) {
+ this._graphID = id;
+ return this;
+ },
+
+ setTransformer : function(fn) {
+ this._transformer = fn;
+ return this;
+ },
+
+ /**
+ * Set up dest as a listener to this.
+ */
+ listen : function(dest) {
+ this._sendsTo[dest.__id__] = dest;
+ return { remove : JX.bind(null, this._removeListener, dest) };
+ },
+ /**
+ * Helper for listen.
+ */
+ _removeListener : function(dest) {
+ delete this._sendsTo[dest.__id__];
+ },
+ /**
+ * For internal use by the Reactor system
+ */
+ primeValue : function(value) {
+ this._nextPulse = value;
+ },
+ getListeners : function() {
+ var result = [];
+ for (var k in this._sendsTo) {
+ result.push(this._sendsTo[k]);
+ }
+ return result;
+ },
+ /**
+ * For internal use by the Reactor system
+ */
+ getNextPulse : function(pulse) {
+ return this._nextPulse;
+ },
+ getTransformer : function() {
+ return this._transformer;
+ },
+ forceSendValue : function(pulse) {
+ JX.Reactor.propagatePulse(pulse, this);
+ },
+ // fn should return JX.Reactor.DoNotPropagate to indicate a value that
+ // should not be retransmitted.
+ transform : function(fn) {
+ return new JX.ReactorNode([this], fn);
+ },
+
+ /**
+ * Suppress events to happen at most once per min_interval.
+ * The last event that fires within an interval will fire at the end
+ * of the interval. Events that are sandwiched between other events
+ * within an interval are dropped.
+ */
+ calm : function(min_interval) {
+ var result = new JX.ReactorNode([this], JX.id);
+ var transformer = new JX.ReactorNodeCalmer(result, min_interval);
+ result.setTransformer(JX.bind(transformer, transformer.onPulse));
+ return result;
+ }
+ },
+ construct : function(source_streams, transformer) {
+ this._nextPulse = JX.Reactor.DoNotPropagate;
+ this._transformer = transformer;
+ this._sendsTo = {};
+ for (var ix = 0; ix < source_streams.length; ix++) {
+ source_streams[ix].listen(this);
+ }
+ }
+});
+
Index: externals/javelin/src/ext/reactor/core/ReactorNodeCalmer.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/reactor/core/ReactorNodeCalmer.js
@@ -0,0 +1,48 @@
+/**
+ * @provides javelin-reactor-node-calmer
+ * @requires javelin-install
+ * javelin-reactor
+ * javelin-util
+ * @javelin
+ */
+
+JX.install('ReactorNodeCalmer', {
+ properties : {
+ lastTime : 0,
+ timeout : null,
+ minInterval : 0,
+ reactorNode : null,
+ isEnabled : true
+ },
+ construct : function(node, min_interval) {
+ this.setLastTime(-min_interval);
+ this.setMinInterval(min_interval);
+ this.setReactorNode(node);
+ },
+ members: {
+ onPulse : function(pulse) {
+ if (!this.getIsEnabled()) {
+ return pulse;
+ }
+ var current_time = JX.now();
+ if (current_time - this.getLastTime() > this.getMinInterval()) {
+ this.setLastTime(current_time);
+ return pulse;
+ } else {
+ clearTimeout(this.getTimeout());
+ this.setTimeout(setTimeout(
+ JX.bind(this, this.send, pulse),
+ this.getLastTime() + this.getMinInterval() - current_time
+ ));
+ return JX.Reactor.DoNotPropagate;
+ }
+ },
+ send : function(pulse) {
+ this.setLastTime(JX.now());
+ this.setIsEnabled(false);
+ this.getReactorNode().forceSendValue(pulse);
+ this.setIsEnabled(true);
+ }
+ }
+});
+
Index: externals/javelin/src/ext/reactor/dom/RDOM.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/reactor/dom/RDOM.js
@@ -0,0 +1,406 @@
+/**
+ * Javelin Reactive functions to work with the DOM.
+ * @provides javelin-reactor-dom
+ * @requires javelin-dom
+ * javelin-dynval
+ * javelin-reactornode
+ * javelin-install
+ * javelin-util
+ * @javelin
+ */
+JX.install('RDOM', {
+ statics : {
+ _time : null,
+ /**
+ * DynVal of the current time in milliseconds.
+ */
+ time : function() {
+ if (JX.RDOM._time === null) {
+ var time = new JX.ReactorNode([], JX.id);
+ window.setInterval(function() {
+ time.forceSendValue(JX.now());
+ }, 100);
+ JX.RDOM._time = new JX.DynVal(time, JX.now());
+ }
+ return JX.RDOM._time;
+ },
+
+ /**
+ * Given a DynVal[String], return a DOM text node whose value tracks it.
+ */
+ $DT : function(dyn_string) {
+ var node = document.createTextNode(dyn_string.getValueNow());
+ dyn_string.transform(function(s) { node.data = s; });
+ return node;
+ },
+
+ _recvEventPulses : function(node, event) {
+ var reactor_node = new JX.ReactorNode([], JX.id);
+ var no_path = null;
+ JX.DOM.listen(
+ node,
+ event,
+ no_path,
+ JX.bind(reactor_node, reactor_node.forceSendValue)
+ );
+
+ reactor_node.setGraphID(JX.DOM.uniqID(node));
+ return reactor_node;
+ },
+
+ _recvChangePulses : function(node) {
+ return JX.RDOM._recvEventPulses(node, 'change').transform(function() {
+ return node.value;
+ });
+ },
+
+
+ /**
+ * Sets up a bidirectional DynVal for a node.
+ * @param node :: DOM Node
+ * @param inPulsesFn :: DOM Node -> ReactorNode
+ * @param inDynValFn :: DOM Node -> ReactorNode -> DynVal
+ * @param outFn :: ReactorNode -> DOM Node
+ */
+ _bidi : function(node, inPulsesFn, inDynValFn, outFn) {
+ var inPulses = inPulsesFn(node);
+ var inDynVal = inDynValFn(node, inPulses);
+ outFn(inDynVal.getChanges(), node);
+ inDynVal.getChanges().listen(inPulses);
+ return inDynVal;
+ },
+
+ /**
+ * ReactorNode[String] of the incoming values of a radio group.
+ * @param Array of DOM elements, all the radio buttons in a group.
+ */
+ _recvRadioPulses : function(buttons) {
+ var ins = [];
+ for (var ii = 0; ii < buttons.length; ii++) {
+ ins.push(JX.RDOM._recvChangePulses(buttons[ii]));
+ }
+ return new JX.ReactorNode(ins, JX.id);
+ },
+
+ /**
+ * DynVal[String] of the incoming values of a radio group.
+ * pulses is a ReactorNode[String] of the incoming values of the group
+ */
+ _recvRadio : function(buttons, pulses) {
+ var init = '';
+ for (var ii = 0; ii < buttons.length; ii++) {
+ if (buttons[ii].checked) {
+ init = buttons[ii].value;
+ break;
+ }
+ }
+
+ return new JX.DynVal(pulses, init);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the radio group.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ _sendRadioPulses : function(rnode, buttons) {
+ return rnode.transform(function(val) {
+ var found;
+ if (__DEV__) {
+ found = false;
+ }
+
+ for (var ii = 0; ii < buttons.length; ii++) {
+ if (buttons[ii].value == val) {
+ buttons[ii].checked = true;
+ if (__DEV__) {
+ found = true;
+ }
+ }
+ }
+
+ if (__DEV__) {
+ if (!found) {
+ throw new Error("Mismatched radio button value");
+ }
+ }
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[String] for a radio group.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ radio : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvRadioPulses,
+ JX.RDOM._recvRadio,
+ JX.RDOM._sendRadioPulses
+ );
+ },
+
+ /**
+ * ReactorNode[Boolean] of the values of the checkbox when it changes.
+ */
+ _recvCheckboxPulses : function(checkbox) {
+ return JX.RDOM._recvChangePulses(checkbox).transform(function(val) {
+ return Boolean(val);
+ });
+ },
+
+ /**
+ * DynVal[Boolean] of the value of a checkbox.
+ */
+ _recvCheckbox : function(checkbox, pulses) {
+ return new JX.DynVal(pulses, Boolean(checkbox.checked));
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[Boolean] to the checkbox
+ */
+ _sendCheckboxPulses : function(rnode, checkbox) {
+ return rnode.transform(function(val) {
+ if (__DEV__) {
+ if (!(val === true || val === false)) {
+ throw new Error("Send boolean values to checkboxes.");
+ }
+ }
+
+ checkbox.checked = val;
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[Boolean] for a checkbox.
+ */
+ checkbox : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvCheckboxPulses,
+ JX.RDOM._recvCheckbox,
+ JX.RDOM._sendCheckboxPulses
+ );
+ },
+
+ /**
+ * ReactorNode[String] of the changing values of a text input.
+ */
+ _recvInputPulses : function(input) {
+ // This misses advanced changes like paste events.
+ var live_changes = [
+ JX.RDOM._recvChangePulses(input),
+ JX.RDOM._recvEventPulses(input, 'keyup'),
+ JX.RDOM._recvEventPulses(input, 'keypress'),
+ JX.RDOM._recvEventPulses(input, 'keydown')
+ ];
+
+ return new JX.ReactorNode(live_changes, function() {
+ return input.value;
+ });
+ },
+
+ /**
+ * DynVal[String] of the value of a text input.
+ */
+ _recvInput : function(input, pulses) {
+ return new JX.DynVal(pulses, input.value);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the input
+ */
+ _sendInputPulses : function(rnode, input) {
+ var result = rnode.transform(function(val) {
+ input.value = val;
+ });
+ result.setGraphID(JX.DOM.uniqID(input));
+ return result;
+ },
+
+
+ /**
+ * Bidirectional DynVal[String] for a text input.
+ */
+ input : function(input) {
+ return JX.RDOM._bidi(
+ input,
+ JX.RDOM._recvInputPulses,
+ JX.RDOM._recvInput,
+ JX.RDOM._sendInputPulses
+ );
+ },
+
+ /**
+ * ReactorNode[String] of the incoming changes in value of a select element.
+ */
+ _recvSelectPulses : function(select) {
+ return JX.RDOM._recvChangePulses(select);
+ },
+
+ /**
+ * DynVal[String] of the value of a select element.
+ */
+ _recvSelect : function(select, pulses) {
+ return new JX.DynVal(pulses, select.value);
+ },
+
+ /**
+ * Send the pulses from the ReactorNode[String] to the select.
+ * Sending an invalid value will result in a log message in __DEV__.
+ */
+ _sendSelectPulses : function(rnode, select) {
+ return rnode.transform(function(val) {
+ select.value = val;
+
+ if (__DEV__) {
+ if (select.value !== val) {
+ throw new Error("Mismatched select value");
+ }
+ }
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[String] for the value of a select.
+ */
+ select : function(select) {
+ return JX.RDOM._bidi(
+ select,
+ JX.RDOM._recvSelectPulses,
+ JX.RDOM._recvSelect,
+ JX.RDOM._sendSelectPulses
+ );
+ },
+
+ /**
+ * ReactorNode[undefined] that fires when a button is clicked.
+ */
+ clickPulses : function(button) {
+ return JX.RDOM._recvEventPulses(button, 'click').transform(function() {
+ return null;
+ });
+ },
+
+ /**
+ * ReactorNode[Boolean] of whether the mouse is over a target.
+ */
+ _recvIsMouseOverPulses : function(target) {
+ var mouseovers = JX.RDOM._recvEventPulses(target, 'mouseover').transform(
+ function() {
+ return true;
+ });
+ var mouseouts = JX.RDOM._recvEventPulses(target, 'mouseout').transform(
+ function() {
+ return false;
+ });
+
+ return new JX.ReactorNode([mouseovers, mouseouts], JX.id);
+ },
+
+ /**
+ * DynVal[Boolean] of whether the mouse is over a target.
+ */
+ isMouseOver : function(target) {
+ // Not worth it to initialize this properly.
+ return new JX.DynVal(JX.RDOM._recvIsMouseOverPulses(target), false);
+ },
+
+ /**
+ * ReactorNode[Boolean] of whether an element has the focus.
+ */
+ _recvHasFocusPulses : function(target) {
+ var focuses = JX.RDOM._recvEventPulses(target, 'focus').transform(
+ function() {
+ return true;
+ });
+ var blurs = JX.RDOM._recvEventPulses(target, 'blur').transform(
+ function() {
+ return false;
+ });
+
+ return new JX.ReactorNode([focuses, blurs], JX.id);
+ },
+
+ /**
+ * DynVal[Boolean] of whether an element has the focus.
+ */
+ _recvHasFocus : function(target) {
+ var is_focused_now = (target === document.activeElement);
+ return new JX.DynVal(JX.RDOM._recvHasFocusPulses(target), is_focused_now);
+ },
+
+ _sendHasFocusPulses : function(rnode, target) {
+ rnode.transform(function(should_focus) {
+ if (should_focus) {
+ target.focus();
+ } else {
+ target.blur();
+ }
+ return should_focus;
+ });
+ },
+
+ /**
+ * Bidirectional DynVal[Boolean] of whether an element has the focus.
+ */
+ hasFocus : function(target) {
+ return JX.RDOM._bidi(
+ target,
+ JX.RDOM._recvHasFocusPulses,
+ JX.RDOM._recvHasFocus,
+ JX.RDOM._sendHasFocusPulses
+ );
+ },
+
+ /**
+ * Send a CSS class from a DynVal to a node
+ */
+ sendClass : function(dynval, node, className) {
+ return dynval.transform(function(add) {
+ JX.DOM.alterClass(node, className, add);
+ });
+ },
+
+ /**
+ * Dynamically attach a set of DynVals to a DOM node's properties as
+ * specified by props.
+ * props: {left: someDynVal, style: {backgroundColor: someOtherDynVal}}
+ */
+ sendProps : function(node, props) {
+ var dynvals = [];
+ var keys = [];
+ var style_keys = [];
+ for (var key in props) {
+ keys.push(key);
+ if (key === 'style') {
+ for (var style_key in props[key]) {
+ style_keys.push(style_key);
+ dynvals.push(props[key][style_key]);
+ node.style[style_key] = props[key][style_key].getValueNow();
+ }
+ } else {
+ dynvals.push(props[key]);
+ node[key] = props[key].getValueNow();
+ }
+ }
+
+ return JX.Reactor.lift(JX.bind(null, function(keys, style_keys, node) {
+ var args = JX.$A(arguments).slice(3);
+
+ for (var ii = 0; ii < args.length; ii++) {
+ if (keys[ii] === 'style') {
+ for (var jj = 0; jj < style_keys.length; jj++) {
+ node.style[style_keys[jj]] = args[ii];
+ ii++;
+ }
+ ii--;
+ } else {
+ node[keys[ii]] = args[ii];
+ }
+ }
+ }, keys, style_keys, node), dynvals);
+ }
+ }
+});
+
+
Index: externals/javelin/src/ext/view/HTMLView.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/HTMLView.js
@@ -0,0 +1,136 @@
+/**
+ * Dumb HTML views. Mostly to demonstrate how the visitor pattern over these
+ * views works, as driven by validation. I'm not convinced it's actually a good
+ * idea to do validation.
+ *
+ * @provides javelin-view-html
+ * @requires javelin-install
+ * javelin-view
+ */
+
+JX.install('HTMLView', {
+ extend: 'View',
+ members : {
+ render: function(rendered_children) {
+ return JX.$N(this.getName(), this.getAllAttributes(), rendered_children);
+ },
+ validate: function() {
+ this.accept(JX.HTMLView.getValidatingVisitor());
+ }
+ },
+
+ statics: {
+ getValidatingVisitor: function() {
+ return new JX.ViewVisitor(JX.HTMLView.validate);
+ },
+
+ validate: function(view, children) {
+ var spec = this._getHTMLSpec();
+ if (!view.getName() in spec) {
+ throw new Error("invalid tag");
+ }
+
+ var tag_spec = spec[view.getName()];
+
+ var attrs = view.getAllAttributes();
+ for (var attr in attrs) {
+ if (!(attr in tag_spec)) {
+ throw new Error("invalid attr");
+ }
+
+ var validator = tag_spec[attr];
+ if (typeof validator === "function") {
+ return validator(attrs[attr]);
+ }
+ }
+
+ return true;
+ },
+
+ _validateRel: function(target) {
+ return target in {
+ "_blank": 1,
+ "_self": 1,
+ "_parent": 1,
+ "_top": 1
+ };
+ },
+ _getHTMLSpec: function() {
+ var attrs_any_can_have = {
+ className: 1,
+ id: 1,
+ sigil: 1
+ };
+
+ var form_elem_attrs = {
+ name: 1,
+ value: 1
+ };
+
+ var spec = {
+ a: { href: 1, target: JX.HTMLView._validateRel },
+ b: {},
+ blockquote: {},
+ br: {},
+ button: JX.copy({}, form_elem_attrs),
+ canvas: {},
+ code: {},
+ dd: {},
+ div: {},
+ dl: {},
+ dt: {},
+ em: {},
+ embed: {},
+ fieldset: {},
+ form: { type: 1 },
+ h1: {},
+ h2: {},
+ h3: {},
+ h4: {},
+ h5: {},
+ h6: {},
+ hr: {},
+ i: {},
+ iframe: { src: 1 },
+ img: { src: 1, alt: 1 },
+ input: JX.copy({}, form_elem_attrs),
+ label: {'for': 1},
+ li: {},
+ ol: {},
+ optgroup: {},
+ option: JX.copy({}, form_elem_attrs),
+ p: {},
+ pre: {},
+ q: {},
+ select: {},
+ span: {},
+ strong: {},
+ sub: {},
+ sup: {},
+ table: {},
+ tbody: {},
+ td: {},
+ textarea: {},
+ tfoot: {},
+ th: {},
+ thead: {},
+ tr: {},
+ ul: {}
+ };
+
+ for (var k in spec) {
+ JX.copy(spec[k], attrs_any_can_have);
+ }
+
+ return spec;
+ },
+ registerToInterpreter: function(view_interpreter) {
+ var spec = this._getHTMLSpec();
+ for (var tag in spec) {
+ view_interpreter.register(tag, JX.HTMLView);
+ }
+ return view_interpreter;
+ }
+ }
+});
+
Index: externals/javelin/src/ext/view/View.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/View.js
@@ -0,0 +1,189 @@
+/**
+ * A View is a composable wrapper on JX.$N, allowing abstraction of higher-order
+ * views and a consistent pattern of parameterization. It is intended
+ * to be used either directly or as a building block for a syntactic sugar layer
+ * for concise expression of markup patterns.
+ *
+ * @provides javelin-view
+ * @requires javelin-install
+ * javelin-util
+ */
+JX.install('View', {
+ construct : function(attrs, children) {
+ this._attributes = JX.copy({}, this.getDefaultAttributeValues());
+ JX.copy(this._attributes, attrs);
+
+ this._rawChildren = {};
+ this._childKeys = [];
+
+ if (children) {
+ this.addChildren(JX.$AX(children));
+ }
+
+ this.setName(this.__class__.__readable__);
+ },
+ events: [
+ 'change'
+ ],
+
+ properties: {
+ 'name': null
+ },
+
+ members : {
+ _attributes : null,
+ _rawChildren : null,
+ _childKeys: null, // keys of rawChildren, kept ordered.
+ _nextChildKey: 0, // next key to use for a new child
+
+ /*
+ * Don't override.
+ * TODO: Strongly typed attribute access (getIntAttr, getStringAttr...)?
+ */
+ getAttr : function(attrName) {
+ return this._attributes[attrName];
+ },
+
+ /*
+ * Don't override.
+ */
+ multisetAttr : function(attrs) {
+ JX.copy(this._attributes, attrs);
+ this.invoke('change');
+ return this;
+ },
+
+ /*
+ * Don't override.
+ */
+ setAttr : function(attrName, value) {
+ this._attributes[attrName] = value;
+ this.invoke('change');
+ return this;
+ },
+ /*
+ * Child views can override to specify default values for attributes.
+ */
+ getDefaultAttributeValues : function() {
+ return {};
+ },
+
+ /**
+ * Don't override.
+ */
+ getAllAttributes: function() {
+ return JX.copy({}, this._attributes);
+ },
+
+ /**
+ * Get the children. Don't override.
+ */
+ getChildren : function() {
+ var result = [];
+ var should_repack = false;
+
+ for(var ii = 0; ii < this._childKeys.length; ii++) {
+ var key = this._childKeys[ii];
+ if (this._rawChildren[key] === undefined) {
+ should_repack = true;
+ } else {
+ result.push(this._rawChildren[key]);
+ }
+ }
+
+ if (should_repack) {
+ var new_child_keys = [];
+ for(var ii = 0; ii < this._childKeys.length; ii++) {
+ var key = this._childKeys[ii];
+ if (this._rawChildren[key] !== undefined) {
+ new_child_keys.push(key);
+ }
+ }
+
+ this._childKeys = new_child_keys;
+ }
+
+ return result;
+ },
+
+ /**
+ * Add children to the view. Returns array of removal handles.
+ * Don't override.
+ */
+ addChildren : function(children) {
+ var result = [];
+ for (var ii = 0; ii < children.length; ii++) {
+ result.push(this._addChild(children[ii]));
+ }
+ this.invoke('change');
+ return result;
+ },
+
+ /**
+ * Add a single child view to the view.
+ * Returns a removal handle, i.e. an object that has a method remove(),
+ * that removes the added child from the view.
+ *
+ * Don't override.
+ */
+ addChild: function(child) {
+ var result = this._addChild(child);
+ this.invoke('change');
+ return result;
+ },
+
+ _addChild: function(child) {
+ var key = this._nextChildKey++;
+ this._rawChildren[key] = child;
+ this._childKeys.push(key);
+
+ return {
+ remove: JX.bind(this, this._removeChild, key)
+ };
+ },
+
+ _removeChild: function(child_key) {
+ delete this._rawChildren[child_key];
+ this.invoke('change');
+ },
+
+ /**
+ * Accept visitors. This allows adding new behaviors to Views without
+ * having to change View classes themselves.
+ *
+ * This implements a post-order traversal over the tree of views. Children
+ * are processed before parents, and for convenience the results of the
+ * visitor on the children are passed to it when processing the parent.
+ *
+ * The visitor parameter is a callable which receives two parameters.
+ * The first parameter is the view to visit. The second parameter is an
+ * array of the results of visiting the view's children.
+ *
+ * Don't override.
+ */
+ accept: function(visitor) {
+ var results = [];
+ var children = this.getChildren();
+ for(var ii = 0; ii < children.length; ii++) {
+ var result;
+ if (children[ii].accept) {
+ result = children[ii].accept(visitor);
+ } else {
+ result = children[ii];
+ }
+ results.push(result);
+ }
+ return visitor(this, results);
+ },
+
+ /**
+ * Given the already-rendered children, return the rendered result of
+ * this view.
+ * By default, just pass the children through.
+ */
+ render: function(rendered_children) {
+ return rendered_children;
+ }
+ }
+});
+
Index: externals/javelin/src/ext/view/ViewInterpreter.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/ViewInterpreter.js
@@ -0,0 +1,71 @@
+/**
+ * Experimental interpreter for nice views.
+ * This is CoffeeScript:
+ *
+ * d = declare
+ * selectable: false
+ * boxOrientation: Orientation.HORIZONTAL
+ * additionalClasses: ['some-css-class']
+ * MultiAvatar ref: 'avatars'
+ * div
+ * flex: 1
+ * div(
+ * span className: 'some-css-class', ref: 'actorTargetLine'
+ * span className: 'message-css', ref: 'message'
+ * )
+ *
+ * div
+ * boxOrientation: Orientation.HORIZONTAL
+ * className: 'attachment-css-class'
+ * div
+ * className: 'attachment-image-css-class'
+ * ref: 'attachmentImageContainer'
+ * boxOrientation: Orientation.HORIZONTAL
+ * div className: 'inline attachment-text', ref: 'attachmentText',
+ * div
+ * className: 'attachment-title'
+ * ref: 'attachmentTitle'
+ * flex: 1
+ * div
+ * className: 'attachment-subtitle'
+ * ref: 'attachmentSubtitle'
+ * flex: 1
+ * div className: 'clear'
+ * MiniUfi ref: 'miniUfi'
+ * FeedbackFlyout ref: 'feedbackFlyout'
+ *
+ * It renders to nested function calls of the form:
+ * view({....options...}, child1, child2, ...);
+ *
+ * This view interpreter is meant to make it work.
+ *
+ * @provides javelin-view-interpreter
+ * @requires javelin-view
+ * javelin-install
+ *
+ */
+
+JX.install('ViewInterpreter', {
+ members : {
+ register : function(name, view_cls) {
+ this[name] = function(/* [properties, ]children... */) {
+ var properties = arguments[0] || {};
+ var children = Array.prototype.slice.call(arguments, 1);
+
+ // Passing properties is optional
+ if (properties instanceof JX.View ||
+ properties instanceof JX.HTML ||
+ properties.nodeType ||
+ typeof properties === "string") {
+ children.unshift(properties);
+ properties = {};
+ }
+
+ var result = new view_cls(properties).setName(name);
+ result.addChildren(children);
+
+ return result;
+ }
+ }
+ }
+});
Index: externals/javelin/src/ext/view/ViewPlaceholder.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/ViewPlaceholder.js
@@ -0,0 +1,103 @@
+/**
+ * Initialize a client-side view from the server. The main idea here is to
+ * give server-side views a way to integrate with client-side views.
+ *
+ * The idea is that a client-side view will have an accompanying
+ * thin server-side component. The server-side component renders a placeholder
+ * element in the document, and then it will invoke this behavior to initialize
+ * the view into the placeholder.
+ *
+ * Because server-side views may be nested, we need to add complexity to
+ * handle nesting properly.
+ *
+ * Assuming a server-side view design that looks like hierarchical views,
+ * we have to handle structures like
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * This leads to a problem: Client component 1 needs to initialize the behavior
+ * with its children, which includes client component 2. So client component
+ * 2 must be rendered first. When client component 2 is rendered, it will also
+ * initialize a copy of this behavior. If behaviors are run in the order they
+ * are initialized, the child component will run before the parent, and its
+ * placeholder won't exist yet.
+ *
+ * To solve this problem, placeholder behaviors are initialized with the token
+ * of a containing view that must be rendered first (if any) and a token
+ * representing it for its own children to depend on. This means the server code
+ * is free to call initBehavior in any order.
+ *
+ * In Phabricator, AphrontJavelinView demonstrates how to handle this correctly.
+ *
+ * config: {
+ * id: Node id to replace.
+ * view: class of view, without the 'JX.' prefix.
+ * params: view parameters
+ * children: messy and loud, cute when drunk
+ * trigger_id: id of containing view that must be rendered first
+ * }
+ *
+ * @provides javelin-behavior-view-placeholder
+ * @requires javelin-behavior
+ * javelin-dom
+ * javelin-view-renderer
+ */
+
+
+
+JX.behavior('view-placeholder', function(config, statics) {
+ JX.ViewPlaceholder.register(config.trigger_id, config.id, function() {
+ var replace = JX.$(config.id);
+
+ var children = config.children;
+ if (typeof children === "string") {
+ children = JX.$H(children);
+ }
+
+ var view = new JX[config.view](config.params, children);
+ var rendered = JX.ViewRenderer.render(view);
+
+ JX.DOM.replace(replace, rendered);
+ });
+});
+
+JX.install('ViewPlaceholder', {
+ statics: {
+ register: function(wait_on_token, token, cb) {
+ var ready_q = [];
+
+ if (!wait_on_token || wait_on_token in JX.ViewPlaceholder.ready) {
+ ready_q.push({token: token, cb: cb});
+ } else {
+ var waiting = JX.ViewPlaceholder.waiting;
+ waiting[wait_on_token] = waiting[wait_on_token] || [];
+ waiting[wait_on_token].push({token: token, cb: cb});
+ }
+
+ while(ready_q.length) {
+ var ready = ready_q.shift();
+
+ var waiting = JX.ViewPlaceholder.waiting[ready.token];
+ if (waiting) {
+ for (var ii = 0; ii < waiting.length; ii++) {
+ ready_q.push(waiting[ii]);
+ }
+ delete JX.ViewPlaceholder.waiting[ready.token];
+ }
+ ready.cb();
+
+ JX.ViewPlaceholder.ready[token] = true;
+ }
+
+ },
+ ready: {},
+ waiting: {}
+ }
+});
Index: externals/javelin/src/ext/view/ViewRenderer.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/ViewRenderer.js
@@ -0,0 +1,19 @@
+/**
+ * @provides javelin-view-renderer
+ * @requires javelin-install
+ */
+
+JX.install('ViewRenderer', {
+ members: {
+ visit: function(view, children) {
+ return view.render(children);
+ }
+ },
+ statics: {
+ render: function(view) {
+ var renderer = new JX.ViewRenderer();
+ return view.accept(JX.bind(renderer, renderer.visit));
+ }
+ }
+});
+
Index: externals/javelin/src/ext/view/ViewVisitor.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/ViewVisitor.js
@@ -0,0 +1,36 @@
+/**
+ * @provides javelin-view-visitor
+ * @requires javelin-install
+ * javelin-util
+ *
+ * Add new behaviors to views without changing the view classes themselves.
+ *
+ * Allows you to register specific visitor functions for certain view classes.
+ * If no visitor is registered for a view class, the default_visitor is used.
+ * If no default_visitor is invoked, a no-op visitor is used.
+ *
+ * Registered visitors should be functions with signature
+ * function(view, results_of_visiting_children) {}
+ * Children are visited before their containing parents, and the return values
+ * of the visitor on the children are passed to the parent.
+ *
+ */
+
+JX.install('ViewVisitor', {
+ construct: function(default_visitor) {
+ this._visitors = {};
+ this._default = default_visitor || JX.bag;
+ },
+ members: {
+ _visitors: null,
+ _default: null,
+ register: function(cls, visitor) {
+ this._visitors[cls] = visitor;
+ },
+ visit: function(view, children) {
+ var visitor = this._visitors[cls] || this._default;
+ return visitor(view, children);
+ }
+ }
+});
+
Index: externals/javelin/src/ext/view/__tests__/HTMLView.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/__tests__/HTMLView.js
@@ -0,0 +1,25 @@
+/**
+ * @requires javelin-view-html
+ * javelin-view-interpreter
+ */
+
+
+describe('JX.HTMLView', function() {
+ var html = new JX.ViewInterpreter();
+
+ JX.HTMLView.registerToInterpreter(html);
+
+ it('should fail validation for a little view', function() {
+ var little_view =
+ html.div({className: 'pretty'},
+ html.p({},
+ html.span({sigil: 'hook', invalid: 'foo'},
+ 'Check out ',
+ html.a({href: 'https://fb.com/', target: '_blank' }, 'Facebook'))));
+
+
+ expect(function() {
+ little_view.validate();
+ }).toThrow();
+ });
+});
Index: externals/javelin/src/ext/view/__tests__/View.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/__tests__/View.js
@@ -0,0 +1,61 @@
+/**
+ * @requires javelin-view
+ * javelin-util
+ */
+
+describe('JX.View', function() {
+ JX.install('TestView', {
+ extend : 'View',
+ construct : function(name, attrs, children) {
+ JX.View.call(this, attrs, children);
+ this.setName(name);
+ },
+
+ members : {
+ getDefaultAttributeValues : function() {
+ return {id: 'test'};
+ },
+ render : function(rendered_children) {
+ return JX.$N(
+ 'span',
+ {id : this.getAttr('id')},
+ [this.getName()].concat(rendered_children)
+ );
+ }
+ }
+ });
+
+ it('should by default render children that are passed in', function() {
+ var t = new JX.TestView(
+ '',
+ {},
+ [new JX.TestView('Hey', {id: "child"}, [])]
+ );
+ var result = JX.ViewRenderer.render(t);
+ expect(JX.DOM.scry(result, 'span').length).toBe(1);
+ });
+
+ it('should fail sanely with a bad getAttr call', function() {
+ expect(new JX.TestView('', {}, []).getAttr('foo')).toBeUndefined();
+ });
+
+ it('should allow attribute setting with multiset', function() {
+ var test_val = 'something else';
+ expect(new JX.TestView('', {}, []).multisetAttr({
+ id: 'some_id',
+ other: test_val
+ }).getAttr('other')).toBe(test_val);
+ });
+
+ it('should allow attribute setting with setAttr', function() {
+ var test_val = 'something else';
+ expect(new JX.TestView('', {}, [])
+ .setAttr('other', test_val)
+ .getAttr('other')).toBe(test_val);
+ });
+
+ it('should set default attributes per getDefaultAttributeValues', function() {
+ // Also the test for getAttr
+ expect(new JX.TestView('', {}, []).getAttr('id')).toBe('test');
+ });
+});
Index: externals/javelin/src/ext/view/__tests__/ViewInterpreter.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/__tests__/ViewInterpreter.js
@@ -0,0 +1,47 @@
+/**
+ * @requires javelin-view
+ * javelin-view-interpreter
+ * javelin-view-html
+ * javelin-util
+ */
+
+describe('JX.ViewInterpreter', function() {
+ var html = new JX.ViewInterpreter();
+
+ JX.HTMLView.registerToInterpreter(html);
+
+ it('should allow purty syntax to make a view', function() {
+ var little_view =
+ html.div({},
+ html.p({className: 'pretty'},
+ html.span({sigil: 'hook'},
+ 'Check out ',
+ html.a({href: 'https://fb.com/', rel: '_blank' }, 'Facebook'))));
+
+ var rendered = JX.ViewRenderer.render(little_view);
+
+ expect(rendered.tagName).toBe('DIV');
+ expect(JX.DOM.scry(rendered, 'span', 'hook').length).toBe(1);
+ });
+
+ it('should handle no-attr case', function() {
+ /* Coffeescript:
+ * div(
+ * span className: 'some-css-class', ref: 'actorTargetLine'
+ * span className: 'message-css', ref: 'message'
+ * )
+ *
+ * = javascript:
+ * div(span({
+ * className: 'some-css-class',
+ * ref: 'actorTargetLine'
+ * }), span({
+ * className: 'message-css',
+ * ref: 'message'
+ * }));
+ */
+ var little_view = html.div(html.span({sigil: 'hook'}));
+ var rendered = JX.ViewRenderer.render(little_view);
+ expect(JX.DOM.scry(rendered, 'span', 'hook').length).toBe(1);
+ });
+});
Index: externals/javelin/src/ext/view/__tests__/ViewRenderer.js
===================================================================
--- /dev/null
+++ externals/javelin/src/ext/view/__tests__/ViewRenderer.js
@@ -0,0 +1,25 @@
+/**
+ * @requires javelin-view-renderer
+ * javelin-view
+ */
+
+describe('JX.ViewRenderer', function() {
+ it('should render children then parent', function() {
+ var child_rendered = false;
+ var child_rendered_first = false;
+
+ var child = new JX.View({});
+ var parent = new JX.View({});
+ parent.addChild(child);
+ child.render = function(_) {
+ child_rendered = true;
+ }
+
+ parent.render = function(rendered_children) {
+ child_rendered_first = child_rendered;
+ }
+
+ JX.ViewRenderer.render(parent);
+ expect(child_rendered_first).toBe(true);
+ });
+});
Index: externals/javelin/src/lib/Cookie.js
===================================================================
--- /dev/null
+++ externals/javelin/src/lib/Cookie.js
@@ -0,0 +1,102 @@
+/**
+ * @provides javelin-cookie
+ * @requires javelin-install
+ * javelin-util
+ * @javelin
+ */
+
+/*
+ * API/Wrapper for document cookie access and manipulation based heavily on the
+ * MooTools Cookie.js
+ *
+ * github.com/mootools/mootools-core/blob/master/Source/Utilities/Cookie.js
+ *
+ * Thx again, Moo.
+ */
+JX.install('Cookie', {
+
+ /**
+ * Setting cookies involves setting up a cookie object which is eventually
+ * written.
+ *
+ * var prefs = new JX.Cookie('prefs');
+ * prefs.setDaysToLive(5);
+ * prefs.setValue('1,0,10,1350');
+ * prefs.setSecure();
+ * prefs.write();
+ *
+ * Retrieving a cookie from the browser requires only a read() call on the
+ * cookie object. However, because cookies have such a complex API you may
+ * not be able to get your value this way if a path or domain was set when the
+ * cookie was written. Good luck with that.
+ *
+ * var prefs_string = new JX.Cookie('prefs').read();
+ *
+ * There is no real API in HTTP for deleting a cookie aside from setting the
+ * cookie to expire immediately. This dispose method will null out the value
+ * and expire the cookie as well.
+ *
+ * new JX.Cookie('prefs').dispose();
+ */
+ construct : function(key) {
+ if (__DEV__ &&
+ (!key.length ||
+ key.match(/^(?:expires|domain|path|secure)$/i) ||
+ key.match(/[\s,;]/) ||
+ key.indexOf('$') === 0)) {
+ JX.$E('JX.Cookie(): Invalid cookie name. "' + key + '" provided.');
+ }
+ this.setKey(key);
+ this.setTarget(document);
+ },
+
+ properties : {
+ key : null,
+ value : null,
+ domain : null,
+ path : null,
+ daysToLive : 0,
+ secure : true,
+ target : null
+ },
+
+ members : {
+ write : function() {
+ this.setValue(encodeURIComponent(this.getValue()));
+
+ var cookie_bits = [];
+ cookie_bits.push(this.getValue());
+
+ if (this.getDomain()) {
+ cookie_bits.push('Domain=' + this.getDomain());
+ }
+
+ if (this.getPath()) {
+ cookie_bits.push('Path=' + this.getPath());
+ }
+
+ var exp = new Date(JX.now() + this.getDaysToLive() * 1000 * 60 * 60 * 24);
+ cookie_bits.push('Expires=' + exp.toGMTString());
+
+ if (this.getSecure()) {
+ cookie_bits.push('Secure');
+ }
+
+ cookie_str = cookie_bits.join('; ') + ';';
+ var cookie_str = this.getKey() + '=' + cookie_str;
+ this.getTarget().cookie = cookie_str;
+ },
+
+ read : function() {
+ var key = this.getKey().replace(/([-.*+?^${}()|[\]\/\\])/g, '\\$1');
+ var val = this.getTarget().cookie.match('(?:^|;)\\s*' + key + '=([^;]*)');
+ return (val) ? decodeURIComponent(val[1]) : null;
+ },
+
+ dispose : function() {
+ this.setValue(null);
+ this.setDaysToLive(-1);
+ this.write();
+ }
+ }
+});
Index: externals/javelin/src/lib/DOM.js
===================================================================
--- /dev/null
+++ externals/javelin/src/lib/DOM.js
@@ -0,0 +1,883 @@
+/**
+ * @requires javelin-magical-init
+ * javelin-install
+ * javelin-util
+ * javelin-vector
+ * javelin-stratcom
+ * @provides javelin-dom
+ *
+ * @javelin-installs JX.$
+ * @javelin-installs JX.$N
+ * @javelin-installs JX.$H
+ *
+ * @javelin
+ */
+
+
+/**
+ * Select an element by its "id" attribute, like ##document.getElementById()##.
+ * For example:
+ *
+ * var node = JX.$('some_id');
+ *
+ * This will select the node with the specified "id" attribute:
+ *
+ * LANG=HTML
+ * ...
+ *
+ * If the specified node does not exist, @{JX.$()} will throw an exception.
+ *
+ * For other ways to select nodes from the document, see @{JX.DOM.scry()} and
+ * @{JX.DOM.find()}.
+ *
+ * @param string "id" attribute to select from the document.
+ * @return Node Node with the specified "id" attribute.
+ *
+ * @group dom
+ */
+JX.$ = function(id) {
+
+ if (__DEV__) {
+ if (!id) {
+ JX.$E('Empty ID passed to JX.$()!');
+ }
+ }
+
+ var node = document.getElementById(id);
+ if (!node || (node.id != id)) {
+ if (__DEV__) {
+ if (node && (node.id != id)) {
+ JX.$E(
+ 'JX.$("'+id+'"): '+
+ 'document.getElementById() returned an element without the '+
+ 'correct ID. This usually means that the element you are trying '+
+ 'to select is being masked by a form with the same value in its '+
+ '"name" attribute.');
+ }
+ }
+ JX.$E("JX.$('" + id + "') call matched no nodes.");
+ }
+
+ return node;
+};
+
+/**
+ * Upcast a string into an HTML object so it is treated as markup instead of
+ * plain text. See @{JX.$N} for discussion of Javelin's security model. Every
+ * time you call this function you potentially open up a security hole. Avoid
+ * its use wherever possible.
+ *
+ * This class intentionally supports only a subset of HTML because many browsers
+ * named "Internet Explorer" have awkward restrictions around what they'll
+ * accept for conversion to document fragments. Alter your datasource to emit
+ * valid HTML within this subset if you run into an unsupported edge case. All
+ * the edge cases are crazy and you should always be reasonably able to emit
+ * a cohesive tag instead of an unappendable fragment.
+ *
+ * You may use @{JX.$H} as a shortcut for creating new JX.HTML instances:
+ *
+ * JX.$N('div', {}, some_html_blob); // Treat as string (safe)
+ * JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!)
+ *
+ * @task build String into HTML
+ * @task nodes HTML into Nodes
+ *
+ * @group dom
+ */
+JX.install('HTML', {
+
+ construct : function(str) {
+ if (__DEV__) {
+ var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup',
+ 'caption', 'tr', 'th', 'td', 'option'];
+ var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i');
+ var match = null;
+ if (match = str.match(evil_stuff)) {
+ JX.$E(
+ 'new JX.HTML("<' + match[1] + '>..."): ' +
+ 'call initializes an HTML object with an invalid partial fragment ' +
+ 'and can not be converted into DOM nodes. The enclosing tag of an ' +
+ 'HTML content string must be appendable to a document fragment. ' +
+ 'For example, is allowed but or
are not.');
+ }
+
+ var really_evil = /