1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-09 16:32:39 +01:00

Use direct inclusion, not submodules, to bring Javelin into Phabricator

Summary:
Submoduling is slightly convenient for developers but hellishly difficult for many users. Since we make about a dozen updates to Javelin per year, just include the source directly.

Even if we run `git submodule status` more often, this creates additional problems for users with PATH misconfigured.

Fixes T2062 by nuking it from orbit.

Test Plan: Loaded site, browsed around. Grepped for references to submodules.

Reviewers: btrahan, vrana

CC: aran

Maniphest Tasks: T2062

Differential Revision: https://secure.phabricator.com/D4581
This commit is contained in:
epriestley 2013-01-22 10:32:26 -08:00
parent 22c64c67ff
commit 07767fda00
70 changed files with 10264 additions and 117 deletions

3
.gitmodules vendored
View file

@ -1,3 +0,0 @@
[submodule "externals/javelin"]
path = externals/javelin
url = git://github.com/facebook/javelin.git

1
externals/javelin vendored

@ -1 +0,0 @@
Subproject commit 32c6e43f4b8b84df940bed8ed8d073e67f6c2b28

25
externals/javelinjs/.gitignore vendored Normal file
View file

@ -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/

25
externals/javelinjs/LICENSE vendored Normal file
View file

@ -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.

33
externals/javelinjs/README vendored Normal file
View file

@ -0,0 +1,33 @@
Javelin is a performance-oriented Javascript library originally developed at
Facebook. Learn more at <http://www.javelinjs.com/>.
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.

321
externals/javelinjs/src/core/Event.js vendored Normal file
View file

@ -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<string, *>
* @task info
*/
data : null,
/**
* Sigil path this event was activated from. TODO: explain this
*
* @type list<string>
* @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()+'>';
}
}
}
});

646
externals/javelinjs/src/core/Stratcom.js vendored Normal file
View file

@ -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<string> Event type (or list of event names) to
* listen for. For example, ##'click'## or
* ##['keydown', 'keyup']##.
*
* @param wild Sigil paths to listen for this event on. See discussion
* in method documentation.
*
* @param function Callback to invoke when this event is triggered. It
* should have the signature ##f(:JX.Event e)##.
*
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
* @task listen
*/
listen : function(types, paths, func) {
if (__DEV__) {
if (arguments.length != 3) {
JX.$E(
'JX.Stratcom.listen(...): '+
'requires exactly 3 arguments. Did you mean JX.DOM.listen?');
}
if (typeof func != 'function') {
JX.$E(
'JX.Stratcom.listen(...): '+
'callback is not a function.');
}
}
var ids = [];
types = JX.$AX(types);
if (!paths) {
paths = this._auto;
}
if (!JX.isArray(paths)) {
paths = [[paths]];
} else if (!JX.isArray(paths[0])) {
paths = [paths];
}
var listener = { _callback : func };
// To listen to multiple event types on multiple paths, we just install
// the same listener a whole bunch of times: if we install for two
// event types on three paths, we'll end up with six references to the
// listener.
//
// TODO: we'll call your listener twice if you install on two paths where
// one path is a subset of another. The solution is "don't do that", but
// it would be nice to verify that the caller isn't doing so, in __DEV__.
for (var ii = 0; ii < types.length; ++ii) {
var type = types[ii];
if (('onpagehide' in window) && type == 'unload') {
// If we use "unload", we break the bfcache ("Back-Forward Cache") in
// Safari and Firefox. The BFCache makes using the back/forward
// buttons really fast since the pages can come out of magical
// fairyland instead of over the network, so use "pagehide" as a proxy
// for "unload" in these browsers.
type = 'pagehide';
}
if (!(type in this._targets)) {
this._targets[type] = {};
}
var type_target = this._targets[type];
for (var jj = 0; jj < paths.length; ++jj) {
var path = paths[jj];
var id = this._handlers.length;
this._handlers.push(listener);
this._need[id] = path.length;
ids.push(id);
for (var kk = 0; kk < path.length; ++kk) {
if (__DEV__) {
if (path[kk] == 'tag:#document') {
JX.$E(
'JX.Stratcom.listen(..., "tag:#document", ...): ' +
'listen for all events using null, not "tag:#document"');
}
if (path[kk] == 'tag:window') {
JX.$E(
'JX.Stratcom.listen(..., "tag:window", ...): ' +
'listen for window events using null, not "tag:window"');
}
}
(type_target[path[kk]] || (type_target[path[kk]] = [])).push(id);
}
}
}
// Add a remove function to the listener
listener['remove'] = function() {
if (listener._callback) {
delete listener._callback;
for (var ii = 0; ii < ids.length; ii++) {
delete JX.Stratcom._handlers[ids[ii]];
}
}
};
return listener;
},
/**
* Sometimes you may be interested in removing a listener directly from it's
* handler. This is possible by calling JX.Stratcom.removeCurrentListener()
*
* // Listen to only the first click on the page
* JX.Stratcom.listen('click', null, function() {
* // do interesting things
* JX.Stratcom.removeCurrentListener();
* });
*
* @task remove
*/
removeCurrentListener : function() {
var context = this._execContext[this._execContext.length - 1];
var listeners = context.listeners;
// JX.Stratcom.pass will have incremented cursor by now
var cursor = context.cursor - 1;
if (listeners[cursor]) {
listeners[cursor].handler.remove();
}
},
/**
* Dispatch a native Javascript event through the Stratcom control flow.
* Generally, this is automatically called for you by the master dispatcher
* installed by ##init.js##. When you want to dispatch an application event,
* you should instead call invoke().
*
* @param Event Native event for dispatch.
* @return :JX.Event Dispatched :JX.Event.
* @task internal
*/
dispatch : function(event) {
var path = [];
var nodes = {};
var distances = {};
var push = function(key, node, distance) {
// we explicitly only store the first occurrence of each key
if (!nodes.hasOwnProperty(key)) {
nodes[key] = node;
distances[key] = distance;
path.push(key);
}
};
var target = event.srcElement || event.target;
// Touch events may originate from text nodes, but we want to start our
// traversal from the nearest Element, so we grab the parentNode instead.
if (target && target.nodeType === 3) {
target = target.parentNode;
}
// Since you can only listen by tag, id, or sigil we unset the target if
// it isn't an Element. Document and window are Nodes but not Elements.
if (!target || !target.getAttribute) {
target = null;
}
var distance = 1;
var cursor = target;
while (cursor && cursor.getAttribute) {
push('tag:' + cursor.nodeName.toLowerCase(), cursor, distance);
var id = cursor.id;
if (id) {
push('id:' + id, cursor, distance);
}
var sigils = cursor.getAttribute('data-sigil');
if (sigils) {
sigils = sigils.split(' ');
for (var ii = 0; ii < sigils.length; ii++) {
push(sigils[ii], cursor, distance);
}
}
var auto_id = cursor.getAttribute('data-autoid');
if (auto_id) {
push('autoid:' + auto_id, cursor, distance);
}
++distance;
cursor = cursor.parentNode;
}
var etype = event.type;
if (etype == 'focusin') {
etype = 'focus';
} else if (etype == 'focusout') {
etype = 'blur';
}
var proxy = new JX.Event()
.setRawEvent(event)
.setData(event.customData)
.setType(etype)
.setTarget(target)
.setNodes(nodes)
.setNodeDistances(distances)
.setPath(path.reverse());
// Don't touch this for debugging purposes
//JX.log('~> '+proxy.toString());
return this._dispatchProxy(proxy);
},
/**
* Dispatch a previously constructed proxy :JX.Event.
*
* @param :JX.Event Event to dispatch.
* @return :JX.Event Returns the event argument.
* @task internal
*/
_dispatchProxy : function(proxy) {
var scope = this._targets[proxy.getType()];
if (!scope) {
return proxy;
}
var path = proxy.getPath();
var distances = proxy.getNodeDistances();
var len = path.length;
var hits = {};
var hit_distances = {};
var matches;
// A large number (larger than any distance we will ever encounter), but
// we need to do math on it in the sort function so we can't use
// Number.POSITIVE_INFINITY.
var far_away = 1000000;
for (var root = -1; root < len; ++root) {
matches = scope[(root == -1) ? this._auto : path[root]];
if (matches) {
var distance = distances[path[root]] || far_away;
for (var ii = 0; ii < matches.length; ++ii) {
var match = matches[ii];
hits[match] = (hits[match] || 0) + 1;
hit_distances[match] = Math.min(
hit_distances[match] || distance,
distance
);
}
}
}
var listeners = [];
for (var k in hits) {
if (hits[k] == this._need[k]) {
var handler = this._handlers[k];
if (handler) {
listeners.push({
distance: hit_distances[k],
handler: handler
});
}
}
}
// Sort listeners by matched sigil closest to the target node
// Listeners with the same closest sigil are called in an undefined order
listeners.sort(function(a, b) {
if (__DEV__) {
// Make sure people play by the rules. >:)
return (a.distance - b.distance) || (Math.random() - 0.5);
}
return a.distance - b.distance;
});
this._execContext.push({
listeners: listeners,
event: proxy,
cursor: 0
});
this.pass();
this._execContext.pop();
return proxy;
},
/**
* Pass on an event, allowing other handlers to process it. The use case
* here is generally something like:
*
* if (JX.Stratcom.pass()) {
* // something else handled the event
* return;
* }
* // handle the event
* event.prevent();
*
* This allows you to install event handlers that operate at a lower
* effective priority, and provide a default behavior which is overridable
* by listeners.
*
* @return bool True if the event was stopped or prevented by another
* handler.
* @task handle
*/
pass : function() {
var context = this._execContext[this._execContext.length - 1];
var event = context.event;
var listeners = context.listeners;
while (context.cursor < listeners.length) {
var cursor = context.cursor++;
if (listeners[cursor]) {
var handler = listeners[cursor].handler;
handler._callback && handler._callback(event);
}
if (event.getStopped()) {
break;
}
}
return event.getStopped() || event.getPrevented();
},
/**
* Retrieve the event (if any) which is currently being dispatched.
*
* @return :JX.Event|null Event which is currently being dispatched, or
* null if there is no active dispatch.
* @task handle
*/
context : function() {
var len = this._execContext.length;
return len ? this._execContext[len - 1].event : null;
},
/**
* Merge metadata. You must call this (even if you have no metadata) to
* start the Stratcom queue.
*
* @param int The datablock to merge data into.
* @param dict Dictionary of metadata.
* @return void
* @task internal
*/
mergeData : function(block, data) {
if (this._data[block]) {
if (__DEV__) {
for (var key in data) {
if (key in this._data[block]) {
JX.$E(
'JX.Stratcom.mergeData(' + block + ', ...); is overwriting ' +
'existing data.');
}
}
}
JX.copy(this._data[block], data);
} else {
this._data[block] = data;
if (block === 0) {
JX.Stratcom.ready = true;
JX.flushHoldingQueue('install-init', function(fn) {
fn();
});
JX.__rawEventQueue({type: 'start-queue'});
}
}
},
/**
* Determine if a node has a specific sigil.
*
* @param Node Node to test.
* @param string Sigil to check for.
* @return bool True if the node has the sigil.
*
* @task sigil
*/
hasSigil : function(node, sigil) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.hasSigil(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
}
var sigils = node.getAttribute('data-sigil') || false;
return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1;
},
/**
* Add a sigil to a node.
*
* @param Node Node to add the sigil to.
* @param string Sigil to name the node with.
* @return void
* @task sigil
*/
addSigil: function(node, sigil) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.addSigil(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
}
var sigils = node.getAttribute('data-sigil') || '';
if (!JX.Stratcom.hasSigil(node, sigil)) {
sigils += ' ' + sigil;
}
node.setAttribute('data-sigil', sigils);
},
/**
* Retrieve a node's metadata.
*
* @param Node Node from which to retrieve data.
* @return object Data attached to the node. If no data has been attached
* to the node yet, an empty object will be returned, but
* subsequent calls to this method will always retrieve the
* same object.
* @task meta
*/
getData : function(node) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.getData(<non-element>): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have data.');
}
}
var meta_id = (node.getAttribute('data-meta') || '').split('_');
if (meta_id[0] && meta_id[1]) {
var block = this._data[meta_id[0]];
var index = meta_id[1];
if (block && (index in block)) {
return block[index];
} else if (__DEV__) {
JX.$E(
'JX.Stratcom.getData(<node>): Tried to access data (block ' +
meta_id[0] + ', index ' + index + ') that was not present. This ' +
'probably means you are calling getData() before the block ' +
'is provided by mergeData().');
}
}
var data = {};
if (!this._data[1]) { // data block 1 is reserved for JavaScript
this._data[1] = {};
}
this._data[1][this._dataIndex] = data;
node.setAttribute('data-meta', '1_' + (this._dataIndex++));
return data;
},
/**
* Add data to a node's metadata.
*
* @param Node Node which data should be attached to.
* @param object Data to add to the node's metadata.
* @return object Data attached to the node that is returned by
* JX.Stratcom.getData().
* @task meta
*/
addData : function(node, data) {
if (__DEV__) {
if (!node || !node.getAttribute) {
JX.$E(
'JX.Stratcom.addData(<non-element>, ...): ' +
'node is not an element. Most likely, you\'re passing window or ' +
'document, which are not elements and can\'t have sigils.');
}
if (!data || typeof data != 'object') {
JX.$E(
'JX.Stratcom.addData(..., <nonobject>): ' +
'data to attach to node is not an object. You must use ' +
'objects, not primitives, for metadata.');
}
}
return JX.copy(JX.Stratcom.getData(node), data);
},
/**
* @task internal
*/
allocateMetadataBlock : function() {
return this._dataBlock++;
}
}
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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');
// });
});

View file

@ -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(
'<script>parent.MaybeArray = Array;</script>'
);
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]);
});
});

224
externals/javelinjs/src/core/init.js vendored Normal file
View file

@ -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(
'<script' +
' defer="defer"' +
' src="javascript:void(0)"' +
' onreadystatechange="' + ready + '"' +
'><\/sc' + 'ript\>');
}
JX.onload = function(func) {
if (loaded) {
func();
} else {
onload.push(func);
}
}
})();

459
externals/javelinjs/src/core/install.js vendored Normal file
View file

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

367
externals/javelinjs/src/core/util.js vendored Normal file
View file

@ -0,0 +1,367 @@
/**
* Javelin utility functions.
*
* @provides javelin-util
*
* @javelin-installs JX.$E
* @javelin-installs JX.$A
* @javelin-installs JX.$AX
* @javelin-installs JX.isArray
* @javelin-installs JX.copy
* @javelin-installs JX.bind
* @javelin-installs JX.bag
* @javelin-installs JX.keys
* @javelin-installs JX.log
* @javelin-installs JX.id
* @javelin-installs JX.now
*
* @javelin
*/
/**
* Throw an exception and attach the caller data in the exception.
*
* @param string Exception message.
*
* @group util
*/
JX.$E = function(message) {
var e = new Error(message);
var caller_fn = JX.$E.caller;
if (caller_fn) {
e.caller_fn = caller_fn.caller;
}
throw e;
};
/**
* Convert an array-like object (usually ##arguments##) into a real Array. An
* "array-like object" is something with a ##length## property and numerical
* keys. The most common use for this is to let you call Array functions on the
* magical ##arguments## object.
*
* JX.$A(arguments).slice(1);
*
* @param obj Array, or array-like object.
* @return Array Actual array.
*
* @group util
*/
JX.$A = function(mysterious_arraylike_object) {
// NOTE: This avoids the Array.slice() trick because some bizarre COM object
// I dug up somewhere was freaking out when I tried to do it and it made me
// very upset, so do not replace this with Array.slice() cleverness.
var r = [];
for (var ii = 0; ii < mysterious_arraylike_object.length; ii++) {
r.push(mysterious_arraylike_object[ii]);
}
return r;
};
/**
* Cast a value into an array, by wrapping scalars into singletons. If the
* argument is an array, it is returned unmodified. If it is a scalar, an array
* with a single element is returned. For example:
*
* JX.$AX([3]); // Returns [3].
* JX.$AX(3); // Returns [3].
*
* Note that this function uses a @{function:JX.isArray} check whether or not
* the argument is an array, so you may need to convert array-like objects (such
* as ##arguments##) into real arrays with @{function:JX.$A}.
*
* This function is mostly useful to create methods which accept either a
* value or a list of values.
*
* @param wild Scalar or Array.
* @return Array If the argument was a scalar, an Array with the argument as
* its only element. Otherwise, the original Array.
*
* @group util
*/
JX.$AX = function(maybe_scalar) {
return JX.isArray(maybe_scalar) ? maybe_scalar : [maybe_scalar];
};
/**
* Checks whether a value is an array.
*
* JX.isArray(['an', 'array']); // Returns true.
* JX.isArray('Not an Array'); // Returns false.
*
* @param wild Any value.
* @return bool true if the argument is an array, false otherwise.
*
* @group util
*/
JX.isArray = Array.isArray || function(maybe_array) {
return Object.prototype.toString.call(maybe_array) == '[object Array]';
};
/**
* Copy properties from one object to another. If properties already exist, they
* are overwritten.
*
* var cat = {
* ears: 'clean',
* paws: 'clean',
* nose: 'DIRTY OH NOES'
* };
* var more = {
* nose: 'clean',
* tail: 'clean'
* };
*
* JX.copy(cat, more);
*
* // cat is now:
* // {
* // ears: 'clean',
* // paws: 'clean',
* // nose: 'clean',
* // tail: 'clean'
* // }
*
* NOTE: This function does not copy the ##toString## property or anything else
* which isn't enumerable or is somehow magic or just doesn't work. But it's
* usually what you want.
*
* @param obj Destination object, which properties should be copied to.
* @param obj Source object, which properties should be copied from.
* @return obj Modified destination object.
*
* @group util
*/
JX.copy = function(copy_dst, copy_src) {
for (var k in copy_src) {
copy_dst[k] = copy_src[k];
}
return copy_dst;
};
/**
* Create a function which invokes another function with a bound context and
* arguments (i.e., partial function application) when called; king of all
* functions.
*
* Bind performs context binding (letting you select what the value of ##this##
* will be when a function is invoked) and partial function application (letting
* you create some function which calls another one with bound arguments).
*
* = Context Binding =
*
* Normally, when you call ##obj.method()##, the magic ##this## object will be
* the ##obj## you invoked the method from. This can be undesirable when you
* need to pass a callback to another function. For instance:
*
* COUNTEREXAMPLE
* var dog = new JX.Dog();
* dog.barkNow(); // Makes the dog bark.
*
* JX.Stratcom.listen('click', 'bark', dog.barkNow); // Does not work!
*
* This doesn't work because ##this## is ##window## when the function is
* later invoked; @{method:JX.Stratcom.listen} does not know about the context
* object ##dog##. The solution is to pass a function with a bound context
* object:
*
* var dog = new JX.Dog();
* var bound_function = JX.bind(dog, dog.barkNow);
*
* JX.Stratcom.listen('click', 'bark', bound_function);
*
* ##bound_function## is a function with ##dog## bound as ##this##; ##this##
* will always be ##dog## when the function is called, no matter what
* property chain it is invoked from.
*
* You can also pass ##null## as the context argument to implicitly bind
* ##window##.
*
* = Partial Function Application =
*
* @{function:JX.bind} also performs partial function application, which allows
* you to bind one or more arguments to a function. For instance, if we have a
* simple function which adds two numbers:
*
* function add(a, b) { return a + b; }
* add(3, 4); // 7
*
* Suppose we want a new function, like this:
*
* function add3(b) { return 3 + b; }
* add3(4); // 7
*
* Instead of doing this, we can define ##add3()## in terms of ##add()## by
* binding the value ##3## to the ##a## argument:
*
* var add3_bound = JX.bind(null, add, 3);
* add3_bound(4); // 7
*
* Zero or more arguments may be bound in this way. This is particularly useful
* when using closures in a loop:
*
* COUNTEREXAMPLE
* for (var ii = 0; ii < button_list.length; ii++) {
* button_list[ii].onclick = function() {
* JX.log('You clicked button number '+ii+'!'); // Fails!
* };
* }
*
* This doesn't work; all the buttons report the highest number when clicked.
* This is because the local ##ii## is captured by the closure. Instead, bind
* the current value of ##ii##:
*
* var func = function(button_num) {
* JX.log('You clicked button number '+button_num+'!');
* }
* for (var ii = 0; ii < button_list.length; ii++) {
* button_list[ii].onclick = JX.bind(null, func, ii);
* }
*
* @param obj|null Context object to bind as ##this##.
* @param function Function to bind context and arguments to.
* @param ... Zero or more arguments to bind.
* @return function New function which invokes the original function with
* bound context and arguments when called.
*
* @group util
*/
JX.bind = function(context, func, more) {
if (__DEV__) {
if (typeof func != 'function') {
JX.$E(
'JX.bind(context, <yuck>, ...): '+
'Attempting to bind something that is not a function.');
}
}
var bound = JX.$A(arguments).slice(2);
if (func.bind) {
return func.bind.apply(func, [context].concat(bound));
}
return function() {
return func.apply(context || window, bound.concat(JX.$A(arguments)));
}
};
/**
* "Bag of holding"; function that does nothing. Primarily, it's used as a
* placeholder when you want something to be callable but don't want it to
* actually have an effect.
*
* @return void
*
* @group util
*/
JX.bag = function() {
// \o\ \o/ /o/ woo dance party
};
/**
* Convert an object's keys into a list. For example:
*
* JX.keys({sun: 1, moon: 1, stars: 1}); // Returns: ['sun', 'moon', 'stars']
*
* @param obj Object to retrieve keys from.
* @return list List of keys.
*
* @group util
*/
JX.keys = Object.keys || function(obj) {
var r = [];
for (var k in obj) {
r.push(k);
}
return r;
};
/**
* Identity function; returns the argument unmodified. This is primarily useful
* as a placeholder for some callback which may transform its argument.
*
* @param wild Any value.
* @return wild The passed argument.
*
* @group util
*/
JX.id = function(any) {
return any;
};
JX.log = JX.bag;
if (__DEV__) {
if (!window.console || !window.console.log) {
if (window.opera && window.opera.postError) {
window.console = {log: function(m) { window.opera.postError(m); }};
} else {
window.console = {log: function(m) { }};
}
}
/**
* Print a message to the browser debugging console (like Firebug). This
* method exists only in ##__DEV__##.
*
* @param string Message to print to the browser debugging console.
* @return void
*
* @group util
*/
JX.log = function(message) {
window.console.log(message);
}
window.alert = (function(native_alert) {
var recent_alerts = [];
var in_alert = false;
return function(msg) {
if (in_alert) {
JX.log(
'alert(...): '+
'discarded reentrant alert.');
return;
}
in_alert = true;
recent_alerts.push(JX.now());
if (recent_alerts.length > 3) {
recent_alerts.splice(0, recent_alerts.length - 3);
}
if (recent_alerts.length >= 3 &&
(recent_alerts[recent_alerts.length - 1] - recent_alerts[0]) < 5000) {
if (confirm(msg + "\n\nLots of alert()s recently. Kill them?")) {
window.alert = JX.bag;
}
} else {
// Note that we can't .apply() the IE6 version of this "function".
native_alert(msg);
}
in_alert = false;
}
})(window.alert);
}
/**
* Date.now is the fastest timestamp function, but isn't supported by every
* browser. This gives the fastest version the environment can support.
* The wrapper function makes the getTime call even slower, but benchmarking
* shows it to be a marginal perf loss. Considering how small of a perf
* difference this makes overall, it's not really a big deal. The primary
* reason for this is to avoid hacky "just think of the byte savings" JS
* like +new Date() that has an unclear outcome for the unexposed.
*
* @return Int A Unix timestamp of the current time on the local machine
*/
JX.now = (Date.now || function() { return new Date().getTime(); });

75
externals/javelinjs/src/docs/Base.js vendored Normal file
View file

@ -0,0 +1,75 @@
/**
* @requires javelin-install
* @javelin
*/
/**
* This is not a real class, but @{function:JX.install} provides several methods
* which exist on all Javelin classes. This class documents those methods.
*
* @task events Builtin Events
* @group install
*/
JX.install('Base', {
members : {
/**
* Invoke a class event, notifying all listeners. You must declare the
* events your class invokes when you install it; see @{function:JX.install}
* for documentation. Any arguments you provide will be passed to listener
* callbacks.
*
* @param string Event type, must be declared when class is
* installed.
* @param ... Zero or more arguments.
*
* @return @{JX.Event} Event object which was dispatched.
* @task events
*/
invoke : function(type, more) {
// <docstub only, see JX.install()> //
},
/**
* Listen for events emitted by this object instance. You can also use
* the static flavor of this method to listen to events emitted by any
* instance of this object.
*
* See also @{method:JX.Stratcom.listen}.
*
* @param string Type of event to listen for.
* @param function Function to call when this event occurs.
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
* @task events
*/
listen : function(type, callback) {
// <docstub only, see JX.install()> //
}
},
statics : {
/**
* Static listen interface for listening to events produced by any instance
* of this class. See @{method:listen} for documentation.
*
* @param string Type of event to listen for.
* @param function Function to call when this event occurs.
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
* @task events
*/
listen : function(type, callback) {
// <docstub only, see JX.install()> //
}
}
});

View file

@ -0,0 +1,182 @@
@title Concepts: Behaviors
@group concepts
Javelin behaviors help you glue pieces of code together.
= Overview =
Javelin behaviors provide a place for you to put glue code. For instance, when
a page loads, you often need to instantiate objects, or set up event listeners,
or alert the user that they've won a hog, or create a dependency between two
objects, or modify the DOM, etc.
Sometimes there's enough code involved here or a particular setup step happens
often enough that it makes sense to write a class, but sometimes it's just a
few lines of one-off glue. Behaviors give you a structured place to put this
glue so that it's consistently organized and can benefit from Javelin
infrastructure.
= Behavior Basics =
Behaviors are defined with @{function:JX.behavior}:
lang=js
JX.behavior('win-a-hog', function(config, statics) {
alert("YOU WON A HOG NAMED " + config.hogName + "!");
});
They are called with @{function:JX.initBehaviors}:
lang=js
JX.initBehaviors({
"win-a-hog" : [{hogName : "Ethel"}]
});
Normally, you don't construct the @{function:JX.initBehaviors} call yourself,
but instead use a server-side library which manages behavior initialization for
you. For example, using the PHP library:
lang=php
$config = array('hogName' => 'Ethel');
JavelinHelper::initBehaviors('win-a-hog', $config);
Regardless, this will alert the user that they've won a hog (named Ethel, which
is a good name for a hog) when they load the page.
The callback you pass to @{function:JX.behavior} should have this signature:
lang=js
function(config, statics) {
// ...
}
The function will be invoked once for each configuration dictionary passed to
@{function:JX.initBehaviors}, and the dictionary will be passed as the
##config## parameter. For example, to alert the user that they've won two hogs:
lang=js
JX.initBehaviors({
"win-a-hog" : [{hogName : "Ethel"}, {hogName: "Beatrice"}]
});
This will invoke the function twice, once for each ##config## dictionary.
Usually, you invoke a behavior multiple times if you have several similar
controls on a page, like multiple @{class:JX.Tokenizer}s.
An initially empty object will be passed in the ##statics## parameter, but
changes to this object will persist across invocations of the behavior. For
example:
lang=js
JX.initBehaviors('win-a-hog', function(config, statics) {
statics.hogsWon = (statics.hogsWon || 0) + 1;
if (statics.hogsWon == 1) {
alert("YOU WON A HOG! YOUR HOG IS NAMED " + config.hogName + "!");
} else {
alert("YOU WON ANOTHER HOG!!! THIS ONE IS NAMED " + config.hogName + "!");
}
}
One way to think about behaviors are that they take the anonymous function
passed to @{function:JX.behavior} and put it in a private Javelin namespace,
which you access with @{function:JX.initBehavior}.
Another way to think about them is that you are defining methods which represent
the entirety of the API exposed by the document. The recommended approach to
glue code is that the server interact with Javascript on the client //only// by
invoking behaviors, so the set of available behaviors represent the complete set
of legal interactions available to the server.
= History and Rationale =
This section explains why behaviors exist and how they came about. You can
understand and use them without knowing any of this, but it may be useful or
interesting.
In early 2007, Facebook often solved the "glue code" problem through the use
of global functions and DOM Level 0 event handlers, by manually building HTML
tags in PHP:
lang=php
echo '<a href="#" '.
'onclick="win_a_hog('.escape_js_string($hog_name).'); return false;">'.
'Click here to win!'.
'</a>';
(This example produces a link which the user can click to be alerted they have
won a hog, which is slightly different from the automatic alert in the other
examples in this document. Some subtle distinctions are ignored or glossed
over here because they are not important to understanding behaviors.)
This has a wide array of technical and architectural problems:
- Correctly escaping parameters is cumbersome and difficult.
- It resists static analysis, and is difficult to even grep for. You can't
easily package, minify, or determine dependencies for the piece of JS in
the result string.
- DOM Level 0 events have a host of issues in a complex application
environment.
- The JS global namespace becomes polluted with application glue functions.
- The server and client are tightly and relatively arbitrarily coupled, since
many of these handlers called multiple functions or had logic in the
strings. There is no structure to the coupling, so many callers relied on
the full power of arbitrary JS execution.
- It's utterly hideous.
In 2007/2008, we introduced @{function@libphutil:jsprintf} and a function called
onloadRegister() to solve some of the obvious problems:
lang=php
onloadRegister('win_a_hog(%s);', $hog_name);
This registers the snippet for invocation after DOMContentReady fires. This API
makes escaping manageable, and was combined with recommendations to structure
code like this in order to address some of the other problems:
lang=php
$id = uniq_id();
echo '<a href="#" id="'.$id.'">Click here to win!</a>';
onloadRegister('new WinAHogController(%s, %s);', $id, $hog_name);
By 2010 (particularly with the introduction of XHP) the API had become more
sophisticated, but this is basically how most of Facebook's glue code still
works as of mid-2011. If you view the source of any page, you'll see a bunch
of ##onloadRegister()## calls in the markup which are generated like this.
This mitigates many of the problems but is still fairly awkward. Escaping is
easier, but still possible to get wrong. Stuff is a bit easier to grep for, but
not much. You can't get very far with static analysis unless you get very
complex. Coupling between the languages has been reduced but not eliminated. And
now you have a bunch of classes which only really have glue code in them.
Javelin behaviors provide a more structured solution to some of these problems:
- All your Javascript code is in Javascript files, not embedded in strings in
in some host language on the server side.
- You can use static analysis and minification tools normally.
- Provided you use a reasonable server-side library, you can't get escaping
wrong.
- Coupling is reduced because server only passes data to the client, never
code.
- The server declares client dependencies explicitly, not implicitly inside
a string literal. Behaviors are also relatively easy to grep for.
- Behaviors exist in a private, structured namespace instead of the global
namespace.
- Separation between the document's layout and behavior is a consequence of
the structure of behaviors.
- The entire interface the server may invoke against can be readily inferred.
Note that Javelin does provide @{function:JX.onload}, which behaves like
##onloadRegister()##. However, its use is discouraged.
The two major downsides to the behavior design appear to be:
- They have a higher setup cost than the ad-hoc methods, but Javelin
philosophically places a very low value on this.
- Because there's a further setup cost to migrate an existing behavior into a
class, behaviors sometimes grow little by little until they are too big,
have more than just glue code, and should have been refactored into a
real class some time ago. This is a pretty high-level drawback and is
manageable through awareness of the risk and code review.

View file

@ -0,0 +1,191 @@
@title Concepts: Event Delegation
@group concepts
Explains Javelin event delegation with @{class:JX.Stratcom}.
= Overview =
Javelin provides event delegation as a core feature of the library, orchestrated
with @{class:JX.Stratcom}. Event delegation means that the library listens to
every event in the document and then delegates them to handlers you install,
as opposed to you installing handlers on specific nodes for specific events you
are interested in.
Event delegation can greatly simplify event handling for many types of user
interactions, and can also be used to do more traditional event listening for
specific events on specific nodes. The goal is to provide a strictly more
powerful event model, which gives you a very general delegation toolkit for
interactions where delegation makes sense but refines into a very specific
toolkit when you need less generality.
Beyond DOM events, Stratcom provides a general event delegation framework which
Javelin classes integrate with.
= Event Delegation Basics =
Javelin routes events based on the **event type** and **sigil set**.
The **event type** is a string with a general description of the event, and
includes the DOM event types like 'click' and 'keydown'. It may also be a
class-specific event (JX.Duck might emit 'quack').
The **sigil set** is a list of sigils (see @{article:Concepts: Sigils and
Metadata}) for the event. If the event is a DOM event, Javelin builds the
sigil set by walking up the DOM tree from the event target and collecting all
the sigils on nodes (it also collects some other data into the sigil set,
see "Magic Sigils" below). If the event is a class event, Javelin walks up
the class hierarchy collecting class names. If the event is a raw event invoked
with @{method:JX.Stratcom.invoke}, the caller must provide the sigil set.
When you install an event handler, you specify the event type and the (possibly
empty) sigil set you want to listen for.
When an event is invoked, Javelin finds all the listeners for that event type
and compares their sigil sets with the event's sigil set. If all of the sigils
in a callback's sigil set appear in the event's sigil set, the callback is
invoked. Generally, this mechanism allows you to ignore events you are not
interested in.
= Listening to General DOM Events =
You can listen to general DOM events with @{method:JX.Stratcom.listen}. This
method allows you to select which types of events you want to receive, and
which elements must be involved in the event:
lang=js
JX.Stratcom.listen(
'click', // Node
null, // Sigil set
function(e) { // Callback
// ...
});
This listens to all clicks on all elements in the document. More likely, you
want to listen only to some clicks. You accomplish this by annotating your
document with Javelin sigils (see @{article:Concepts: Sigils and Metadata})
and specifying a set of sigils which must be present between the target node
and the document root. For instance, your document might look like this:
lang=html
<a href="#" data-sigil="important">Important!</a>
<a href="#">Some Other Link</a>
If you install a handler like the one above, you'll get callbacks for every
click, no matter which link it is or even if it's on the document itself. If
you just want clicks on the "Important!" link, you can install a more specific
handler:
lang=js
JX.Stratcom.listen(
'click',
'important', // Listen only to this sigil set
function(e) {
// ...
});
Now you will receive a callback only when the event target or some ancestor of
it has the "important" sigil, i.e., only when the user clicks on the
"Important!" link. You can also specify multiple sigils; ancestors must have
all of the sigils for you to get a callback:
lang=js
JX.Stratcom.listen(
'click',
['menu', 'item'], // Listen only for clicks on menu items.
function(e) {
// ...
});
= Listening to Specific DOM Events =
You can listen to specific DOM events with @{method:JX.DOM.listen}. This method
works like @{method:JX.Stratcom.listen} but takes a DOM node as the first
parameter and listens only for events within that node:
lang=js
JX.DOM.listen(
node, // Node
'click', // Event type(s)
null, // Sigil set
function(e) { // Callback
// ...
});
This is similar to setting ##node.onclick## or ##node.addEventListener##, but
uses the Javelin delegation core. You can also provide a sigil set, which works
just like @{method:JX.Stratcom.listen} general events. This is useful if your
node is a container, like a ##<div />##, with a lot of stuff in it.
= DOM Event Flow =
Events dispatched within the DOM propagate using a bubbling method, as detailed
by http://www.w3.org/TR/DOM-Level-3-Events/#event-flow
Listeners assigned using @{method:JX.Stratcom.listen} or @{method:JX.DOM.listen}
are called in order of the deepest node in the DOM of the nodes which match the
sigil set listened to.
In this example the second listener, subscribed to 'inner', will be called
before the listener subscribed to 'outer'
lang=html
<div data-sigil="outer">
<div data-sigil="inner">
Click Me
</div>
</div>
<script type="text/javascript">
JX.Stratcom.listen('click', ['outer'], function(e) { ... });
JX.Stratcom.listen('click', ['inner'], function(e) { ... });
</script>
= 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 ##<a />## tags in a document like
this:
lang=js
JX.Stratcom.listen(
'click',
'tag:a',
function(e) {
// ...
});

View file

@ -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
<div data-sigil="newsfeed">
<div data-sigil="story">
...
</div>
<div data-sigil="story">
...
</div>
</div>
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
<div data-sigil="newsfeed">
<div data-sigil="story">
...
<a href="..." data-sigil="like">Like</a>
</div>
<div data-sigil="story">
...
<a href="..." data-sigil="like">Like</a>
</div>
</div>
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
<div data-sigil="newsfeed">
<div data-sigil="story" data-meta="0_0">
...
<a href="..." data-sigil="like">Like</a>
</div>
<div data-sigil="story" data-meta="0_1">
...
<a href="..." data-sigil="like">Like</a>
</div>
</div>
...
<script type="text/javascript">
JX.Stratcom.mergeData(0, [{"storyID" : 12345}, {"storyID" : 23456}]);
</script>
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}.

View file

@ -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.

22
externals/javelinjs/src/docs/onload.js vendored Normal file
View file

@ -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.
};

33
externals/javelinjs/src/ext/fx/Color.js vendored Normal file
View file

@ -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 + ')';
}
}
});

214
externals/javelinjs/src/ext/fx/FX.js vendored Normal file
View file

@ -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;
}
}
}
}
});

View file

@ -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;
}));
}
});

View file

@ -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());
}
}
});

View file

@ -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);
}
}
});

View file

@ -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);
}
}
});

View file

@ -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);
}
}
});

View file

@ -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;
}
}
});

189
externals/javelinjs/src/ext/view/View.js vendored Normal file
View file

@ -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;
}
}
});

View file

@ -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;
}
}
}
});

View file

@ -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
*
* <server:component>
* <client:component id="1">
* <server:component>
* <client:component id="2">
* </client:component>
* </server:component>
* </client:component>
* </server:component>
*
* 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: {}
}
});

View file

@ -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));
}
}
});

View file

@ -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);
}
}
});

View file

@ -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();
});
});

View file

@ -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');
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

102
externals/javelinjs/src/lib/Cookie.js vendored Normal file
View file

@ -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();
}
}
});

883
externals/javelinjs/src/lib/DOM.js vendored Normal file
View file

@ -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
* <div id="some_id">...</div>
*
* 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, <table> is allowed but <tr> or <tfoot> are not.');
}
var really_evil = /<script\b/;
if (str.match(really_evil)) {
JX.$E(
'new JX.HTML("...<script>..."): ' +
'call initializes an HTML object with an embedded script tag! ' +
'Are you crazy?! Do NOT do this!!!');
}
var wont_work = /<object\b/;
if (str.match(wont_work)) {
JX.$E(
'new JX.HTML("...<object>..."): ' +
'call initializes an HTML object with an embedded <object> tag. IE ' +
'will not do the right thing with this.');
}
// TODO(epriestley): May need to deny <option> more broadly, see
// http://support.microsoft.com/kb/829907 and the whole mess in the
// heavy stack. But I seem to have gotten away without cloning into the
// documentFragment below, so this may be a nonissue.
}
this._content = str;
},
members : {
_content : null,
/**
* Convert the raw HTML string into a DOM node tree.
*
* @task nodes
* @return DocumentFragment A document fragment which contains the nodes
* corresponding to the HTML string you provided.
*/
getFragment : function() {
var wrapper = JX.$N('div');
wrapper.innerHTML = this._content;
var fragment = document.createDocumentFragment();
while (wrapper.firstChild) {
// TODO(epriestley): Do we need to do a bunch of cloning junk here?
// See heavy stack. I'm disconnecting the nodes instead; this seems
// to work but maybe my test case just isn't extensive enough.
fragment.appendChild(wrapper.removeChild(wrapper.firstChild));
}
return fragment;
}
}
});
/**
* Build a new HTML object from a trustworthy string. JX.$H is a shortcut for
* creating new JX.HTML instances.
*
* @task build
* @param string A string which you want to be treated as HTML, because you
* know it is from a trusted source and any data in it has been
* properly escaped.
* @return JX.HTML HTML object, suitable for use with @{JX.$N}.
*
* @group dom
*/
JX.$H = function(str) {
return new JX.HTML(str);
};
/**
* Create a new DOM node with attributes and content.
*
* var link = JX.$N('a');
*
* This creates a new, empty anchor tag without any attributes. The equivalent
* markup would be:
*
* LANG=HTML
* <a />
*
* You can also specify attributes by passing a dictionary:
*
* JX.$N('a', {name: 'anchor'});
*
* This is equivalent to:
*
* LANG=HTML
* <a name="anchor" />
*
* Additionally, you can specify content:
*
* JX.$N(
* 'a',
* {href: 'http://www.javelinjs.com'},
* 'Visit the Javelin Homepage');
*
* This is equivalent to:
*
* LANG=HTML
* <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a>
*
* If you only want to specify content, you can omit the attribute parameter.
* That is, these calls are equivalent:
*
* JX.$N('div', {}, 'Lorem ipsum...'); // No attributes.
* JX.$N('div', 'Lorem ipsum...') // Same as above.
*
* Both are equivalent to:
*
* LANG=HTML
* <div>Lorem ipsum...</div>
*
* Note that the content is treated as plain text, not HTML. This means it is
* safe to use untrusted strings:
*
* JX.$N('div', '<script src="evil.com" />');
*
* This is equivalent to:
*
* LANG=HTML
* <div>&lt;script src="evil.com" /&gt;</div>
*
* That is, the content will be properly escaped and will not create a
* vulnerability. If you want to set HTML content, you can use @{JX.HTML}:
*
* JX.$N('div', JX.$H(some_html));
*
* **This is potentially unsafe**, so make sure you understand what you're
* doing. You should usually avoid passing HTML around in string form. See
* @{JX.HTML} for discussion.
*
* You can create new nodes with a Javelin sigil (and, optionally, metadata) by
* providing "sigil" and "meta" keys in the attribute dictionary.
*
* @param string Tag name, like 'a' or 'div'.
* @param dict|string|@{JX.HTML}? Property dictionary, or content if you don't
* want to specify any properties.
* @param string|@{JX.HTML}? Content string (interpreted as plain text)
* or @{JX.HTML} object (interpreted as HTML,
* which may be dangerous).
* @return Node New node with whatever attributes and
* content were specified.
*
* @group dom
*/
JX.$N = function(tag, attr, content) {
if (typeof content == 'undefined' &&
(typeof attr != 'object' || attr instanceof JX.HTML)) {
content = attr;
attr = {};
}
if (__DEV__) {
if (tag.toLowerCase() != tag) {
JX.$E(
'$N("'+tag+'", ...): '+
'tag name must be in lower case; '+
'use "'+tag.toLowerCase()+'", not "'+tag+'".');
}
}
var node = document.createElement(tag);
if (attr.style) {
JX.copy(node.style, attr.style);
delete attr.style;
}
if (attr.sigil) {
JX.Stratcom.addSigil(node, attr.sigil);
delete attr.sigil;
}
if (attr.meta) {
JX.Stratcom.addData(node, attr.meta);
delete attr.meta;
}
if (__DEV__) {
if (('metadata' in attr) || ('data' in attr)) {
JX.$E(
'$N(' + tag + ', ...): ' +
'use the key "meta" to specify metadata, not "data" or "metadata".');
}
}
JX.copy(node, attr);
if (content) {
JX.DOM.setContent(node, content);
}
return node;
};
/**
* Query and update the DOM. Everything here is static, this is essentially
* a collection of common utility functions.
*
* @task stratcom Attaching Event Listeners
* @task content Changing DOM Content
* @task nodes Updating Nodes
* @task serialize Serializing Forms
* @task test Testing DOM Properties
* @task convenience Convenience Methods
* @task query Finding Nodes in the DOM
* @task view Changing View State
*
* @group dom
*/
JX.install('DOM', {
statics : {
_autoid : 0,
_uniqid : 0,
_metrics : {},
/* -( Changing DOM Content )----------------------------------------------- */
/**
* Set the content of some node. This uses the same content semantics as
* other Javelin content methods, see @{function:JX.$N} for a detailed
* explanation. Previous content will be replaced: you can also
* @{method:prependContent} or @{method:appendContent}.
*
* @param Node Node to set content of.
* @param mixed Content to set.
* @return void
* @task content
*/
setContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.setContent(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
while (node.firstChild) {
JX.DOM.remove(node.firstChild);
}
JX.DOM.appendContent(node, content);
},
/**
* Prepend content to some node. This method uses the same content semantics
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
* can also @{method:setContent} or @{method:appendContent}.
*
* @param Node Node to prepend content to.
* @param mixed Content to prepend.
* @return void
* @task content
*/
prependContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.prependContent(<junk>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismPrepend, true);
},
/**
* Append content to some node. This method uses the same content semantics
* as other Javelin methods, see @{function:JX.$N} for an explanation. You
* can also @{method:setContent} or @{method:prependContent}.
*
* @param Node Node to append the content of.
* @param mixed Content to append.
* @return void
* @task content
*/
appendContent : function(node, content) {
if (__DEV__) {
if (!JX.DOM.isNode(node)) {
JX.$E(
'JX.DOM.appendContent(<bleh>, ...): '+
'first argument must be a DOM node.');
}
}
this._insertContent(node, content, this._mechanismAppend);
},
/**
* Internal, add content to a node by prepending.
*
* @param Node Node to prepend content to.
* @param Node Node to prepend.
* @return void
* @task content
*/
_mechanismPrepend : function(node, content) {
node.insertBefore(content, node.firstChild);
},
/**
* Internal, add content to a node by appending.
*
* @param Node Node to append content to.
* @param Node Node to append.
* @task content
*/
_mechanismAppend : function(node, content) {
node.appendChild(content);
},
/**
* Internal, add content to a node using some specified mechanism.
*
* @param Node Node to add content to.
* @param mixed Content to add.
* @param function Callback for actually adding the nodes.
* @param bool True if array elements should be passed to the mechanism
* in reverse order, i.e. the mechanism prepends nodes.
* @return void
* @task content
*/
_insertContent : function(parent, content, mechanism, reverse) {
if (JX.isArray(content)) {
if (reverse) {
content = [].concat(content).reverse();
}
for (var ii = 0; ii < content.length; ii++) {
JX.DOM._insertContent(parent, content[ii], mechanism, reverse);
}
} else {
var type = typeof content;
if (content instanceof JX.HTML) {
content = content.getFragment();
} else if (type == 'string' || type == 'number') {
content = document.createTextNode(content);
}
if (__DEV__) {
if (content && !content.nodeType) {
JX.$E(
'JX.DOM._insertContent(<node>, ...): '+
'second argument must be a string, a number, ' +
'a DOM node or a JX.HTML instance');
}
}
content && mechanism(parent, content);
}
},
/* -( Updating Nodes )----------------------------------------------------- */
/**
* Remove a node from its parent, so it is no longer a child of any other
* node.
*
* @param Node Node to remove.
* @return Node The node.
* @task nodes
*/
remove : function(node) {
node.parentNode && JX.DOM.replace(node, null);
return node;
},
/**
* Replace a node with some other piece of content. This method obeys
* Javelin content semantics, see @{function:JX.$N} for an explanation.
* You can also @{method:setContent}, @{method:prependContent}, or
* @{method:appendContent}.
*
* @param Node Node to replace.
* @param mixed Content to replace it with.
* @return Node the original node.
* @task nodes
*/
replace : function(node, replacement) {
if (__DEV__) {
if (!node.parentNode) {
JX.$E(
'JX.DOM.replace(<node>, ...): '+
'node has no parent node, so it can not be replaced.');
}
}
var mechanism;
if (node.nextSibling) {
mechanism = JX.bind(node.nextSibling, function(parent, content) {
parent.insertBefore(content, this);
});
} else {
mechanism = this._mechanismAppend;
}
var parent = node.parentNode;
parent.removeChild(node);
this._insertContent(parent, replacement, mechanism);
return node;
},
/* -( Serializing Froms )-------------------------------------------------- */
/**
* Converts a form into a list of <name, value> pairs.
*
* Note: This function explicity does not match for submit inputs as there
* could be multiple in a form. It's the caller's obligation to add the
* submit input value if desired.
*
* @param Node The form element to convert into a list of pairs.
* @return List A list of <name, value> pairs.
* @task serialize
*/
convertFormToListOfPairs : function(form) {
var elements = form.getElementsByTagName('*');
var data = [];
for (var ii = 0; ii < elements.length; ++ii) {
if (!elements[ii].name) {
continue;
}
if (elements[ii].disabled) {
continue;
}
var type = elements[ii].type;
var tag = elements[ii].tagName;
if ((type in {radio: 1, checkbox: 1} && elements[ii].checked) ||
type in {text: 1, hidden: 1, password: 1, email: 1, tel: 1,
number: 1} ||
tag in {TEXTAREA: 1, SELECT: 1}) {
data.push([elements[ii].name, elements[ii].value]);
}
}
return data;
},
/**
* Converts a form into a dictionary mapping input names to values. This
* will overwrite duplicate inputs in an undefined way.
*
* @param Node The form element to convert into a dictionary.
* @return Dict A dictionary of form values.
* @task serialize
*/
convertFormToDictionary : function(form) {
var data = {};
var pairs = JX.DOM.convertFormToListOfPairs(form);
for (var ii = 0; ii < pairs.length; ii++) {
data[pairs[ii][0]] = pairs[ii][1];
}
return data;
},
/* -( Testing DOM Properties )--------------------------------------------- */
/**
* Test if an object is a valid Node.
*
* @param wild Something which might be a Node.
* @return bool True if the parameter is a DOM node.
* @task test
*/
isNode : function(node) {
return !!(node && node.nodeName && (node !== window));
},
/**
* Test if an object is a node of some specific (or one of several) types.
* For example, this tests if the node is an ##<input />##, ##<select />##,
* or ##<textarea />##.
*
* JX.DOM.isType(node, ['input', 'select', 'textarea']);
*
* @param wild Something which might be a Node.
* @param string|list One or more tags which you want to test for.
* @return bool True if the object is a node, and it's a node of one
* of the provided types.
* @task test
*/
isType : function(node, of_type) {
node = ('' + (node.nodeName || '')).toUpperCase();
of_type = JX.$AX(of_type);
for (var ii = 0; ii < of_type.length; ++ii) {
if (of_type[ii].toUpperCase() == node) {
return true;
}
}
return false;
},
/**
* Listen for events occuring beneath a specific node in the DOM. This is
* similar to @{JX.Stratcom.listen()}, but allows you to specify some node
* which serves as a scope instead of the default scope (the whole document)
* which you get if you install using @{JX.Stratcom.listen()} directly. For
* example, to listen for clicks on nodes with the sigil 'menu-item' below
* the root menu node:
*
* var the_menu = getReferenceToTheMenuNodeSomehow();
* JX.DOM.listen(the_menu, 'click', 'menu-item', function(e) { ... });
*
* @task stratcom
* @param Node The node to listen for events underneath.
* @param string|list One or more event types to listen for.
* @param list? A path to listen on, or a list of paths.
* @param function Callback to invoke when a matching event occurs.
* @return object A reference to the installed listener. You can later
* remove the listener by calling this object's remove()
* method.
*/
listen : function(node, type, path, callback) {
var auto_id = ['autoid:' + JX.DOM._getAutoID(node)];
path = JX.$AX(path || []);
if (!path.length) {
path = auto_id;
} else {
for (var ii = 0; ii < path.length; ii++) {
path[ii] = auto_id.concat(JX.$AX(path[ii]));
}
}
return JX.Stratcom.listen(type, path, callback);
},
/**
* Invoke a custom event on a node. This method is a companion to
* @{method:JX.DOM.listen} and parallels @{method:JX.Stratcom.invoke} in
* the same way that method parallels @{method:JX.Stratcom.listen}.
*
* This method can not be used to invoke native events (like 'click').
*
* @param Node The node to invoke an event on.
* @param string Custom event type.
* @param dict Event data.
* @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.
*/
invoke : function(node, type, data) {
if (__DEV__) {
if (type in JX.__allowedEvents) {
throw new Error(
'JX.DOM.invoke(..., "' + type + '", ...): ' +
'you cannot invoke with the same type as a native event.');
}
}
return JX.Stratcom.dispatch({
target: node,
type: type,
customData: data
});
},
uniqID : function(node) {
if (!node.getAttribute('id')) {
node.setAttribute('id', 'uniqid_'+(++JX.DOM._uniqid));
}
return node.getAttribute('id');
},
alterClass : function(node, className, add) {
if (__DEV__) {
if (add !== false && add !== true) {
JX.$E(
'JX.DOM.alterClass(...): ' +
'expects the third parameter to be Boolean: ' +
add + ' was provided');
}
}
var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1);
if (add && !has) {
node.className += ' '+className;
} else if (has && !add) {
node.className = node.className.replace(
new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' ');
}
},
htmlize : function(str) {
return (''+str)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
},
/**
* Show one or more elements, by removing their "display" style. This
* assumes you have hidden them with @{method:hide}, or explicitly set
* the style to `display: none;`.
*
* @task convenience
* @param ... One or more nodes to remove "display" styles from.
* @return void
*/
show : function() {
if (__DEV__) {
for (var ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
JX.$E(
'JX.DOM.show(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (var ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = '';
}
},
/**
* Hide one or more elements, by setting `display: none;` on them. This is
* a convenience method. See also @{method:show}.
*
* @task convenience
* @param ... One or more nodes to set "display: none" on.
* @return void
*/
hide : function() {
if (__DEV__) {
for (var ii = 0; ii < arguments.length; ++ii) {
if (!arguments[ii]) {
JX.$E(
'JX.DOM.hide(...): ' +
'one or more arguments were null or empty.');
}
}
}
for (var ii = 0; ii < arguments.length; ++ii) {
arguments[ii].style.display = 'none';
}
},
textMetrics : function(node, pseudoclass, x) {
if (!this._metrics[pseudoclass]) {
var n = JX.$N(
'var',
{className: pseudoclass});
this._metrics[pseudoclass] = n;
}
var proxy = this._metrics[pseudoclass];
document.body.appendChild(proxy);
proxy.style.width = x ? (x+'px') : '';
JX.DOM.setContent(
proxy,
JX.$H(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />')));
var metrics = JX.Vector.getDim(proxy);
document.body.removeChild(proxy);
return metrics;
},
/**
* Search the document for DOM nodes by providing a root node to look
* beneath, a tag name, and (optionally) a sigil. Nodes which match all
* specified conditions are returned.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, a sigil which nodes are required to have.
*
* @return list List of matching nodes, which may be empty.
*/
scry : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
JX.$E(
'JX.DOM.scry(<yuck>, ...): '+
'first argument must be a DOM node.');
}
}
var nodes = root.getElementsByTagName(tagname);
if (!sigil) {
return JX.$A(nodes);
}
var result = [];
for (var ii = 0; ii < nodes.length; ii++) {
if (JX.Stratcom.hasSigil(nodes[ii], sigil)) {
result.push(nodes[ii]);
}
}
return result;
},
/**
* Select a node uniquely identified by a root, tagname and sigil. This
* is similar to JX.DOM.scry() but expects exactly one result.
*
* @task query
*
* @param Node Root node to search beneath.
* @param string Tag name, like 'a' or 'textarea'.
* @param string Optionally, sigil which selected node must have.
*
* @return Node Node uniquely identified by the criteria.
*/
find : function(root, tagname, sigil) {
if (__DEV__) {
if (!JX.DOM.isNode(root)) {
JX.$E(
'JX.DOM.find(<glop>, "'+tagname+'", "'+sigil+'"): '+
'first argument must be a DOM node.');
}
}
var result = JX.DOM.scry(root, tagname, sigil);
if (__DEV__) {
if (result.length > 1) {
JX.$E(
'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+
'matched more than one node.');
}
}
if (!result.length) {
JX.$E('JX.DOM.find(<node>, "' +
tagname + '", "' + sigil + '"): '+ 'matched no nodes.');
}
return result[0];
},
/**
* Focus a node safely. This is just a convenience wrapper that allows you
* to avoid IE's habit of throwing when nearly any focus operation is
* invoked.
*
* @task convenience
* @param Node Node to move cursor focus to, if possible.
* @return void
*/
focus : function(node) {
try { node.focus(); } catch (lol_ie) {}
},
/**
* Scroll to the position of an element in the document.
* @task view
* @param Node Node to move document scroll position to, if possible.
* @return void
*/
scrollTo : function(node) {
window.scrollTo(0, JX.$V(node).y);
},
_getAutoID : function(node) {
if (!node.getAttribute('data-autoid')) {
node.setAttribute('data-autoid', 'autoid_'+(++JX.DOM._autoid));
}
return node.getAttribute('data-autoid');
}
}
});

222
externals/javelinjs/src/lib/History.js vendored Executable file
View file

@ -0,0 +1,222 @@
/**
* @requires javelin-stratcom
* javelin-install
* javelin-uri
* javelin-util
* @provides javelin-history
* @javelin
*/
/**
* JX.History provides a stable interface for managing the browser's history
* stack. Whenever the history stack mutates, the "history:change" event is
* invoked via JX.Stratcom.
*
* Inspired by History Manager implemented by Christoph Pojer (@cpojer)
* @see https://github.com/cpojer/mootools-history
*/
JX.install('History', {
statics : {
// Mechanisms to @{JX.History.install} with (in preferred support order).
// The default behavior is to use the best supported mechanism.
DEFAULT : Infinity,
PUSHSTATE : 3,
HASHCHANGE : 2,
POLLING : 1,
// Last path parsed from the URL fragment.
_hash : null,
// Some browsers fire an extra "popstate" on initial page load, so we keep
// track of the initial path to normalize behavior (and not fire the extra
// event).
_initialPath : null,
// Mechanism used to interface with the browser history stack.
_mechanism : null,
/**
* Starts history management. This method must be invoked first before any
* other JX.History method can be used.
*
* @param int An optional mechanism used to interface with the browser
* history stack. If it is not supported, the next supported
* mechanism will be used.
*/
install : function(mechanism) {
if (__DEV__) {
if (JX.History._installed) {
JX.$E('JX.History.install(): can only install once.');
}
JX.History._installed = true;
}
mechanism = mechanism || JX.History.DEFAULT;
if (mechanism >= JX.History.PUSHSTATE && 'pushState' in history) {
JX.History._mechanism = JX.History.PUSHSTATE;
JX.History._initialPath = JX.History._getBasePath(location.href);
JX.Stratcom.listen('popstate', null, JX.History._handleChange);
} else if (mechanism >= JX.History.HASHCHANGE &&
'onhashchange' in window) {
JX.History._mechanism = JX.History.HASHCHANGE;
JX.Stratcom.listen('hashchange', null, JX.History._handleChange);
} else {
JX.History._mechanism = JX.History.POLLING;
setInterval(JX.History._handleChange, 200);
}
},
/**
* Get the name of the mechanism used to interface with the browser
* history stack.
*
* @return string Mechanism, either pushstate, hashchange, or polling.
*/
getMechanism : function() {
if (__DEV__) {
if (!JX.History._installed) {
JX.$E(
'JX.History.getMechanism(): ' +
'must call JX.History.install() first.');
}
}
return JX.History._mechanism;
},
/**
* Returns the path on top of the history stack.
*
* If the HTML5 History API is unavailable and an eligible path exists in
* the current URL fragment, the fragment is parsed for a path. Otherwise,
* the current URL path is returned.
*
* @return string Path on top of the history stack.
*/
getPath : function() {
if (__DEV__) {
if (!JX.History._installed) {
JX.$E(
'JX.History.getPath(): ' +
'must call JX.History.install() first.');
}
}
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
return JX.History._getBasePath(location.href);
} else {
var parsed = JX.History._parseFragment(location.hash);
return parsed || JX.History._getBasePath(location.href);
}
},
/**
* Pushes a path onto the history stack.
*
* @param string Path.
* @return void
*/
push : function(path) {
if (__DEV__) {
if (!JX.History._installed) {
JX.$E(
'JX.History.push(): ' +
'must call JX.History.install() first.');
}
}
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
if (JX.History._initialPath && JX.History._initialPath !== path) {
JX.History._initialPath = null;
}
history.pushState(null, null, path);
JX.History._fire(path);
} else {
location.hash = JX.History._composeFragment(path);
}
},
/**
* Modifies the path on top of the history stack.
*
* @param string Path.
* @return void
*/
replace : function(path) {
if (__DEV__) {
if (!JX.History._installed) {
JX.$E(
'JX.History.replace(): ' +
'must call JX.History.install() first.');
}
}
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
history.replaceState(null, null, path);
JX.History._fire(path);
} else {
var uri = JX.$U(location.href);
uri.setFragment(JX.History._composeFragment(path));
// Safari bug: "location.replace" does not respect changes made via
// setting "location.hash", so use "history.replaceState" if possible.
if ('replaceState' in history) {
history.replaceState(null, null, uri.toString());
JX.History._handleChange();
} else {
location.replace(uri.toString());
}
}
},
_handleChange : function() {
var path = JX.History.getPath();
if (JX.History.getMechanism() === JX.History.PUSHSTATE) {
if (path === JX.History._initialPath) {
JX.History._initialPath = null;
} else {
JX.History._fire(path);
}
} else {
if (path !== JX.History._hash) {
JX.History._hash = path;
JX.History._fire(path);
}
}
},
_fire : function(path) {
JX.Stratcom.invoke('history:change', null, {
path: JX.History._getBasePath(path)
});
},
_getBasePath : function(href) {
return JX.$U(href).setProtocol(null).setDomain(null).toString();
},
_composeFragment : function(path) {
path = JX.History._getBasePath(path);
// If the URL fragment does not change, the new path will not get pushed
// onto the stack. So we alternate the hash prefix to force a new state.
if (JX.History.getPath() === path) {
var hash = location.hash;
if (hash && hash.charAt(1) === '!') {
return '~!' + path;
}
}
return '!' + path;
},
_parseFragment : function(fragment) {
if (fragment) {
if (fragment.charAt(1) === '!') {
return fragment.substr(2);
} else if (fragment.substr(1, 2) === '~!') {
return fragment.substr(3);
}
}
return null;
}
}
});

155
externals/javelinjs/src/lib/JSON.js vendored Normal file
View file

@ -0,0 +1,155 @@
/**
* Simple JSON serializer.
*
* @requires javelin-install
* @provides javelin-json
* @javelin
*/
/**
* JSON serializer and parser. This class uses the native JSON parser if it is
* available; if not, it provides an eval-based parser and a simple serializer.
*
* NOTE: This class uses eval() on some systems, without sanitizing input. It is
* not safe to use with untrusted data. Javelin does not provide a library
* suitable for parsing untrusted JSON.
*
* Usage is straightforward:
*
* JX.JSON.stringify({"bees":"knees"}); // Returns string: '{"bees":"knees"}'
* JX.JSON.parse('{"bees":"knees"}'); // Returns object: {"bees":"knees"}
*
* @task json JSON Manipulation
* @task internal Internal
* @group util
*/
JX.install('JSON', {
statics : {
/* -( JSON Manipulation )-------------------------------------------------- */
/**
* Parse a **trusted** JSON string into an object. Accepts a valid JSON
* string and returns the object it encodes.
*
* NOTE: This method does not sanitize input and uses an eval-based parser
* on some systems. It is **NOT SAFE** to use with untrusted inputs.
*
* @param string A valid, trusted JSON string.
* @return object The object encoded by the JSON string.
* @task json
*/
parse : function(data) {
if (typeof data != 'string') {
return null;
}
if (window.JSON && JSON.parse) {
var obj;
try {
obj = JSON.parse(data);
} catch (e) {}
return obj || null;
}
return eval('(' + data + ')');
},
/**
* Serialize an object into a JSON string. Accepts an object comprised of
* maps, lists and scalars and transforms it into a JSON representation.
* This method has undefined behavior if you pass in other complicated
* things, e.g. object graphs containing cycles, document.body, or Date
* objects.
*
* @param object An object comprised of maps, lists and scalars.
* @return string JSON representation of the object.
* @task json
*/
stringify : function(val) {
if (window.JSON && JSON.stringify) {
return JSON.stringify(val);
}
var out = [];
if (
val === null || val === true || val === false || typeof val == 'number'
) {
return '' + val;
}
if (val.push && val.pop) {
var v;
for (var ii = 0; ii < val.length; ii++) {
// For consistency with JSON.stringify(), encode undefined array
// indices as null.
v = (typeof val[ii] == 'undefined') ? null : val[ii];
out.push(JX.JSON.stringify(v));
}
return '[' + out.join(',') + ']';
}
if (typeof val == 'string') {
return JX.JSON._esc(val);
}
for (var k in val) {
out.push(JX.JSON._esc(k) + ':' + JX.JSON.stringify(val[k]));
}
return '{' + out.join(',') + '}';
},
/* -( Internal )----------------------------------------------------------- */
// Lifted more or less directly from Crockford's JSON2.
_escexp : /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
// List of control character escape codes.
_meta : {
'\b' : '\\b',
'\t' : '\\t',
'\n' : '\\n',
'\f' : '\\f',
'\r' : '\\r',
'"' : '\\"',
'\\' : '\\\\'
},
/**
* Quote and escape a string for inclusion in serialized JSON. Finds
* characters in the string which need to be escaped and uses
* @{method:_replace} to escape them.
*
* @param string Unescaped string.
* @return string Escaped string.
* @task internal
*/
_esc : function(str) {
JX.JSON._escexp.lastIndex = 0;
return JX.JSON._escexp.test(str) ?
'"' + str.replace(JX.JSON._escexp, JX.JSON._replace) + '"' :
'"' + str + '"';
},
/**
* Helper callback for @{method:_esc}, escapes characters which can't be
* represented normally in serialized JSON.
*
* @param string Unescaped character.
* @return string Escaped character.
* @task internal
*/
_replace : function(m) {
if (m in JX.JSON._meta) {
return JX.JSON._meta[m];
}
return '\\u' + (('0000' + m.charCodeAt(0).toString(16)).slice(-4));
}
}
});

109
externals/javelinjs/src/lib/Mask.js vendored Normal file
View file

@ -0,0 +1,109 @@
/**
* @requires javelin-install
* javelin-dom
* @provides javelin-mask
* @javelin
*/
/**
* Show a "mask" over the page for lightboxes or dialogs. This is used by
* Workflow to draw visual attention to modal dialogs.
*
* JX.Mask.show();
* // Show a dialog, lightbox, or other modal UI.
* JX.Mask.hide();
*
* Masks are stackable, if modal UIs need to spawn other modal UIs.
*
* The mask has class `jx-mask`, which you should apply styles to. For example:
*
* .jx-mask {
* opacity: 0.8;
* background: #000000;
* position: fixed;
* top: 0;
* bottom: 0;
* left: 0;
* right: 0;
* z-index: 2;
* }
*
* You can create multiple mask styles and select them with the `mask_type`
* parameter to `show()` (for instance, a light mask for dialogs and a dark
* mask for lightboxing):
*
* JX.Mask.show('jx-light-mask');
* // ...
* JX.Mask.hide();
*
* This will be applied as a class name to the mask element, which you can
* customize in CSS:
*
* .jx-light-mask {
* background: #ffffff;
* }
*
* The mask has sigil `jx-mask`, which can be used to intercept events
* targeting it, like clicks on the mask.
*
* @group control
*/
JX.install('Mask', {
statics : {
_stack : [],
_mask : null,
_currentType : null,
/**
* Show a mask over the document. Multiple calls push masks onto a stack.
*
* @param string Optional class name to apply to the mask, if you have
* multiple masks (e.g., one dark and one light).
* @return void
*/
show : function(mask_type) {
var self = JX.Mask;
mask_type = mask_type || null;
if (!self._stack.length) {
self._mask = JX.$N('div', {className: 'jx-mask', sigil: 'jx-mask'});
document.body.appendChild(self._mask);
}
self._adjustType(mask_type)
JX.Mask._stack.push(mask_type);
},
/**
* Hide the current mask. The mask stack is popped, which may reveal another
* mask below the current mask.
*
* @return void
*/
hide : function() {
var self = JX.Mask;
var mask_type = self._stack.pop();
self._adjustType(mask_type);
if (!self._stack.length) {
JX.DOM.remove(JX.Mask._mask);
JX.Mask._mask = null;
}
},
_adjustType : function(new_type) {
var self = JX.Mask;
if (self._currentType) {
JX.DOM.alterClass(self._mask, self._currentType, false);
self._currentType = null;
}
if (new_type) {
JX.DOM.alterClass(self._mask, new_type, true);
self._currentType = new_type;
}
}
}
});

471
externals/javelinjs/src/lib/Request.js vendored Normal file
View file

@ -0,0 +1,471 @@
/**
* @requires javelin-install
* javelin-stratcom
* javelin-util
* javelin-behavior
* javelin-json
* javelin-dom
* javelin-resource
* @provides javelin-request
* @javelin
*/
/**
* Make basic AJAX XMLHTTPRequests.
*
* @group workflow
*/
JX.install('Request', {
construct : function(uri, handler) {
this.setURI(uri);
if (handler) {
this.listen('done', handler);
}
},
events : ['start', 'open', 'send', 'statechange', 'done', 'error', 'finally',
'uploadprogress'],
members : {
_xhrkey : null,
_transport : null,
_sent : false,
_finished : false,
_block : null,
_data : null,
_getSameOriginTransport : function() {
try {
try {
return new XMLHttpRequest();
} catch (x) {
return new ActiveXObject("Msxml2.XMLHTTP");
}
} catch (x) {
return new ActiveXObject("Microsoft.XMLHTTP");
}
},
_getCORSTransport : function() {
try {
var xport = new XMLHttpRequest();
if ('withCredentials' in xport) {
// XHR supports CORS
} else if (typeof XDomainRequest != 'undefined') {
xport = new XDomainRequest();
}
return xport;
} catch (x) {
return new XDomainRequest();
}
},
getTransport : function() {
if (!this._transport) {
this._transport = this.getCORS() ? this._getCORSTransport() :
this._getSameOriginTransport();
}
return this._transport;
},
send : function() {
if (this._sent || this._finished) {
if (__DEV__) {
if (this._sent) {
JX.$E(
'JX.Request.send(): ' +
'attempting to send a Request that has already been sent.');
}
if (this._finished) {
JX.$E(
'JX.Request.send(): ' +
'attempting to send a Request that has finished or aborted.');
}
}
return;
}
// Fire the "start" event before doing anything. A listener may
// perform pre-processing or validation on this request
this.invoke('start', this);
if (this._finished) {
return;
}
var xport = this.getTransport();
xport.onreadystatechange = JX.bind(this, this._onreadystatechange);
if (xport.upload) {
xport.upload.onprogress = JX.bind(this, this._onuploadprogress);
}
var method = this.getMethod().toUpperCase();
if (__DEV__) {
if (this.getRawData()) {
if (method != 'POST') {
JX.$E(
'JX.Request.send(): ' +
'attempting to send post data over GET. You must use POST.');
}
}
}
var list_of_pairs = this._data || [];
list_of_pairs.push(['__ajax__', true]);
this._block = JX.Stratcom.allocateMetadataBlock();
list_of_pairs.push(['__metablock__', this._block]);
var q = (this.getDataSerializer() ||
JX.Request.defaultDataSerializer)(list_of_pairs);
var uri = this.getURI();
// If we're sending a file, submit the metadata via the URI instead of
// via the request body, because the entire post body will be consumed by
// the file content.
if (method == 'GET' || this.getRawData()) {
uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q;
}
if (this.getTimeout()) {
this._timer = setTimeout(
JX.bind(
this,
this._fail,
JX.Request.ERROR_TIMEOUT),
this.getTimeout());
}
xport.open(method, uri, true);
// Must happen after xport.open so that listeners can modify the transport
// Some transport properties can only be set after the transport is open
this.invoke('open', this);
if (this._finished) {
return;
}
this.invoke('send', this);
if (this._finished) {
return;
}
if (method == 'POST') {
if (this.getRawData()) {
xport.send(this.getRawData());
} else {
xport.setRequestHeader(
'Content-Type',
'application/x-www-form-urlencoded');
xport.send(q);
}
} else {
xport.send(null);
}
this._sent = true;
},
abort : function() {
this._cleanup();
},
_onuploadprogress : function(progress) {
this.invoke('uploadprogress', progress);
},
_onreadystatechange : function() {
var xport = this.getTransport();
var response;
try {
this.invoke('statechange', this);
if (this._finished) {
return;
}
if (xport.readyState != 4) {
return;
}
// XHR requests to 'file:///' domains return 0 for success, which is why
// we treat it as a good result in addition to HTTP 2XX responses.
if (xport.status !== 0 && (xport.status < 200 || xport.status >= 300)) {
this._fail();
return;
}
if (__DEV__) {
var expect_guard = this.getExpectCSRFGuard();
if (!xport.responseText.length) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an empty response.');
}
if (expect_guard && xport.responseText.indexOf('for (;;);') != 0) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an invalid response.');
}
if (expect_guard && xport.responseText == 'for (;;);') {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an empty response.');
}
}
response = this._extractResponse(xport);
if (!response) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'server returned an invalid response.');
}
} catch (exception) {
if (__DEV__) {
JX.log(
'JX.Request("'+this.getURI()+'", ...): '+
'caught exception processing response: '+exception);
}
this._fail();
return;
}
try {
this._handleResponse(response);
this._cleanup();
} catch (exception) {
// In Firefox+Firebug, at least, something eats these. :/
setTimeout(function() {
throw exception;
}, 0);
}
},
_extractResponse : function(xport) {
var text = xport.responseText;
if (this.getExpectCSRFGuard()) {
text = text.substring('for (;;);'.length);
}
var type = this.getResponseType().toUpperCase();
if (type == 'TEXT') {
return text;
} else if (type == 'JSON' || type == 'JAVELIN') {
return JX.JSON.parse(text);
} else if (type == 'XML') {
var doc;
try {
if (typeof DOMParser != 'undefined') {
var parser = new DOMParser();
doc = parser.parseFromString(text, "text/xml");
} else { // IE
// an XDomainRequest
doc = new ActiveXObject("Microsoft.XMLDOM");
doc.async = false;
doc.loadXML(xport.responseText);
}
return doc.documentElement;
} catch (exception) {
if (__DEV__) {
JX.log(
'JX.Request("'+this.getURI()+'", ...): '+
'caught exception extracting response: '+exception);
}
this._fail();
return null;
}
}
if (__DEV__) {
JX.$E(
'JX.Request("'+this.getURI()+'", ...): '+
'unrecognized response type.');
}
return null;
},
_fail : function(error) {
this._cleanup();
this.invoke('error', error, this);
this.invoke('finally');
},
_done : function(response) {
this._cleanup();
if (response.onload) {
for (var ii = 0; ii < response.onload.length; ii++) {
(new Function(response.onload[ii]))();
}
}
var payload;
if (this.getRaw()) {
payload = response;
} else {
payload = response.payload;
JX.Request._parseResponsePayload(payload);
}
this.invoke('done', payload, this);
this.invoke('finally');
},
_cleanup : function() {
this._finished = true;
clearTimeout(this._timer);
this._timer = null;
// Should not abort the transport request if it has already completed
// Otherwise, we may see an "HTTP request aborted" error in the console
// despite it possibly having succeeded.
if (this._transport && this._transport.readyState != 4) {
this._transport.abort();
}
},
setData : function(dictionary) {
this._data = null;
this.addData(dictionary);
return this;
},
addData : function(dictionary) {
if (!this._data) {
this._data = [];
}
for (var k in dictionary) {
this._data.push([k, dictionary[k]]);
}
return this;
},
setDataWithListOfPairs : function(list_of_pairs) {
this._data = list_of_pairs;
return this;
},
_handleResponse : function(response) {
if (this.getResponseType().toUpperCase() == 'JAVELIN') {
if (response.error) {
this._fail(response.error);
} else {
JX.Stratcom.mergeData(
this._block,
response.javelin_metadata || {});
var when_complete = JX.bind(this, function() {
this._done(response);
JX.initBehaviors(response.javelin_behaviors || {});
});
if (response.javelin_resources) {
JX.Resource.load(response.javelin_resources, when_complete);
} else {
when_complete();
}
}
} else {
this._cleanup();
this.invoke('done', response, this);
this.invoke('finally');
}
}
},
statics : {
ERROR_TIMEOUT : -9000,
defaultDataSerializer : function(list_of_pairs) {
var uri = [];
for (var ii = 0; ii < list_of_pairs.length; ii++) {
var pair = list_of_pairs[ii];
var name = encodeURIComponent(pair[0]);
var value = encodeURIComponent(pair[1]);
uri.push(name + '=' + value);
}
return uri.join('&');
},
/**
* When we receive a JSON blob, parse it to introduce meaningful objects
* where there are magic keys for placeholders.
*
* Objects with the magic key '__html' are translated into JX.HTML objects.
*
* This function destructively modifies its input.
*/
_parseResponsePayload: function(parent, index) {
var recurse = JX.Request._parseResponsePayload;
var obj = (typeof index !== 'undefined') ? parent[index] : parent;
if (JX.isArray(obj)) {
for (var ii = 0; ii < obj.length; ii++) {
recurse(obj, ii);
}
} else if (obj && typeof obj == 'object') {
if (obj.__html != null) {
parent[index] = JX.$H(obj.__html);
} else {
for (var key in obj) {
recurse(obj, key);
}
}
}
}
},
properties : {
URI : null,
dataSerializer : null,
/**
* Configure which HTTP method to use for the request. Permissible values
* are "POST" (default) or "GET".
*
* @param string HTTP method, one of "POST" or "GET".
*/
method : 'POST',
/**
* Set the data parameter of transport.send. Useful if you want to send a
* file or FormData. Not that you cannot send raw data and data at the same
* time.
*
* @param Data, argument to transport.send
*/
rawData: null,
raw : false,
/**
* Configure a timeout, in milliseconds. If the request has not resolved
* (either with success or with an error) within the provided timeframe,
* it will automatically fail with error JX.Request.ERROR_TIMEOUT.
*
* @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds).
*/
timeout : null,
/**
* Whether or not we should expect the CSRF guard in the response.
*
* @param bool
*/
expectCSRFGuard : true,
/**
* Whether it should be a CORS (Cross-Origin Resource Sharing) request to
* a third party domain other than the current site.
*
* @param bool
*/
CORS : false,
/**
* Type of the response.
*
* @param enum 'JAVELIN', 'JSON', 'XML', 'TEXT'
*/
responseType : 'JAVELIN'
}
});

177
externals/javelinjs/src/lib/Resource.js vendored Normal file
View file

@ -0,0 +1,177 @@
/**
* @provides javelin-resource
* @requires javelin-magical-init
* javelin-stratcom
* javelin-util
* javelin-uri
*
* @javelin
*/
JX.install('Resource', {
statics: {
_loading: {},
_loaded: {},
_links: [],
_callbacks: [],
/**
* Loads one or many static resources (JavaScript or CSS) and executes a
* callback once these resources have finished loading.
*
* @param string|array static resource or list of resources to be loaded
* @param function callback when resources have finished loading
*/
load: function(list, callback) {
var resources = {},
uri, resource, path, type;
list = JX.$AX(list);
// In the event there are no resources to wait on, call the callback and
// exit. NOTE: it's better to do this check outside this function and not
// call through JX.Resource, but it's not always easy/possible to do so
if (!list.length) {
setTimeout(callback, 0);
return;
}
for (var ii = 0; ii < list.length; ii++) {
uri = new JX.URI(list[ii]);
resource = uri.toString();
path = uri.getPath();
resources[resource] = true;
if (JX.Resource._loaded[resource]) {
setTimeout(JX.bind(JX.Resource, JX.Resource._complete, resource), 0);
} else if (!JX.Resource._loading[resource]) {
JX.Resource._loading[resource] = true;
if (path.indexOf('.css') == path.length - 4) {
JX.Resource._loadCSS(resource);
} else {
JX.Resource._loadJS(resource);
}
}
}
JX.Resource._callbacks.push({
resources: resources,
callback: callback
});
},
_loadJS: function(uri) {
var script = document.createElement('script');
var load_callback = function() {
JX.Resource._complete(uri);
};
var error_callback = function() {
JX.$E('Resource: JS file download failure: ' + uri);
};
JX.copy(script, {
type: 'text/javascript',
src: uri
});
script.onload = load_callback;
script.onerror = error_callback;
script.onreadystatechange = function() {
var state = this.readyState;
if (state == 'complete' || state == 'loaded') {
load_callback();
}
};
document.getElementsByTagName('head')[0].appendChild(script);
},
_loadCSS: function(uri) {
var link = JX.copy(document.createElement('link'), {
type: 'text/css',
rel: 'stylesheet',
href: uri,
'data-href': uri // don't trust href
});
document.getElementsByTagName('head')[0].appendChild(link);
JX.Resource._links.push(link);
if (!JX.Resource._timer) {
JX.Resource._timer = setInterval(JX.Resource._poll, 20);
}
},
_poll: function() {
var sheets = document.styleSheets,
ii = sheets.length,
links = JX.Resource._links;
// Cross Origin CSS loading
// http://yearofmoo.com/2011/03/cross-browser-stylesheet-preloading/
while (ii--) {
var link = sheets[ii],
owner = link.ownerNode || link.owningElement,
jj = links.length;
if (owner) {
while (jj--) {
if (owner == links[jj]) {
JX.Resource._complete(links[jj]['data-href']);
links.splice(jj, 1);
}
}
}
}
if (!links.length) {
clearInterval(JX.Resource._timer);
JX.Resource._timer = null;
}
},
_complete: function(uri) {
var list = JX.Resource._callbacks,
current, ii;
delete JX.Resource._loading[uri];
JX.Resource._loaded[uri] = true;
for (ii = 0; ii < list.length; ii++) {
current = list[ii];
delete current.resources[uri];
if (!JX.Resource._hasResources(current.resources)) {
current.callback();
list.splice(ii--, 1);
}
}
},
_hasResources: function(resources) {
for (var hasResources in resources) {
return true;
}
return false;
}
},
initialize: function() {
var list = JX.$A(document.getElementsByTagName('link')),
ii = list.length,
node;
while ((node = list[--ii])) {
if (node.type == 'text/css' && node.href) {
JX.Resource._loaded[(new JX.URI(node.href)).toString()] = true;
}
}
list = JX.$A(document.getElementsByTagName('script'));
ii = list.length;
while ((node = list[--ii])) {
if (node.type == 'text/javascript' && node.src) {
JX.Resource._loaded[(new JX.URI(node.src)).toString()] = true;
}
}
}
});

234
externals/javelinjs/src/lib/URI.js vendored Normal file
View file

@ -0,0 +1,234 @@
/**
* @provides javelin-uri
* @requires javelin-install
* javelin-util
* javelin-stratcom
*
* @javelin-installs JX.$U
*
* @javelin
*/
/**
* Handy convenience function that returns a @{class:JX.URI} instance. This
* allows you to write things like:
*
* JX.$U('http://zombo.com/').getDomain();
*
* @param string Unparsed URI.
* @return @{class:JX.URI} JX.URI instance.
*
* @group uri
*/
JX.$U = function(uri) {
return new JX.URI(uri);
};
/**
* Convert a string URI into a maleable object.
*
* var uri = new JX.URI('http://www.example.com/asdf.php?a=b&c=d#anchor123');
* uri.getProtocol(); // http
* uri.getDomain(); // www.example.com
* uri.getPath(); // /asdf.php
* uri.getQueryParams(); // {a: 'b', c: 'd'}
* uri.getFragment(); // anchor123
*
* ...and back into a string:
*
* uri.setFragment('clowntown');
* uri.toString() // http://www.example.com/asdf.php?a=b&c=d#clowntown
*
* @group uri
*/
JX.install('URI', {
statics : {
_uriPattern : /(?:([^:\/?#]+):)?(?:\/\/([^:\/?#]*)(?::(\d*))?)?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/,
_queryPattern : /(?:^|&)([^&=]*)=?([^&]*)/g,
/**
* Convert a Javascript object into an HTTP query string.
*
* @param Object Map of query keys to values.
* @return String HTTP query string, like 'cow=quack&duck=moo'.
*/
_defaultQuerySerializer : function(obj) {
var kv_pairs = [];
for (var key in obj) {
if (obj[key] != null) {
var value = encodeURIComponent(obj[key]);
kv_pairs.push(encodeURIComponent(key) + (value ? '=' + value : ''));
}
}
return kv_pairs.join('&');
}
},
/**
* Construct a URI
*
* Accepts either absolute or relative URIs. Relative URIs may have protocol
* and domain properties set to undefined
*
* @param string absolute or relative URI
*/
construct : function(uri) {
// need to set the default value here rather than in the properties map,
// or else we get some crazy global state breakage
this.setQueryParams({});
if (uri) {
// parse the url
var result = JX.URI._uriPattern.exec(uri);
// fallback to undefined because IE has weird behavior otherwise
this.setProtocol(result[1] || undefined);
this.setDomain(result[2] || undefined);
this.setPort(result[3] || undefined);
var path = result[4];
var query = result[5];
this.setFragment(result[6] || undefined);
// parse the path
this.setPath(path.charAt(0) == '/' ? path : '/' + path);
// parse the query data
if (query) {
var queryData = {};
var data;
while ((data = JX.URI._queryPattern.exec(query)) != null) {
queryData[decodeURIComponent(data[1].replace(/\+/g, ' '))] =
decodeURIComponent(data[2].replace(/\+/g, ' '));
}
this.setQueryParams(queryData);
}
}
},
properties : {
protocol: undefined,
port: undefined,
path: undefined,
queryParams: undefined,
fragment: undefined,
querySerializer: undefined
},
members : {
_domain: undefined,
/**
* Append and override query data values
* Remove a query key by setting it undefined
*
* @param map
* @return @{JX.URI} self
*/
addQueryParams : function(map) {
JX.copy(this.getQueryParams(), map);
return this;
},
/**
* Set a specific query parameter
* Remove a query key by setting it undefined
*
* @param string
* @param wild
* @return @{JX.URI} self
*/
setQueryParam : function(key, value) {
var map = {};
map[key] = value;
return this.addQueryParams(map);
},
/**
* Set the domain
*
* This function checks the domain name to ensure that it is safe for
* browser consumption.
*/
setDomain : function(domain) {
var re = new RegExp(
// For the bottom 128 code points, we use a strict whitelist of
// characters that are allowed by all browsers: -.0-9:A-Z[]_a-z
'[\\x00-\\x2c\\x2f\\x3b-\\x40\\x5c\\x5e\\x60\\x7b-\\x7f' +
// In IE, these chararacters cause problems when entity-encoded.
'\\uFDD0-\\uFDEF\\uFFF0-\\uFFFF' +
// In Safari, these characters terminate the hostname.
'\\u2047\\u2048\\uFE56\\uFE5F\\uFF03\\uFF0F\\uFF1F]');
if (re.test(domain)) {
JX.$E('JX.URI.setDomain(...): invalid domain specified.');
}
this._domain = domain;
return this;
},
getDomain : function() {
return this._domain;
},
toString : function() {
if (__DEV__) {
if (this.getPath() && this.getPath().charAt(0) != '/') {
JX.$E(
'JX.URI.toString(): ' +
'Path does not begin with a "/" which means this URI will likely' +
'be malformed. Ensure any string passed to .setPath() leads "/"');
}
}
var str = '';
if (this.getProtocol()) {
str += this.getProtocol() + '://';
}
str += this.getDomain() || '';
if (this.getPort()) {
str += ':' + this.getPort();
}
// If there is a domain or a protocol, we need to provide '/' for the
// path. If we don't have either and also don't have a path, we can omit
// it to produce a partial URI without path information which begins
// with "?", "#", or is empty.
str += this.getPath() || (str ? '/' : '');
str += this._getQueryString();
if (this.getFragment()) {
str += '#' + this.getFragment();
}
return str;
},
_getQueryString : function() {
var str = (
this.getQuerySerializer() || JX.URI._defaultQuerySerializer
)(this.getQueryParams());
return str ? '?' + str : '';
},
/**
* Redirect the browser to another page by changing the window location. If
* the URI is empty, reloads the current page.
*
* You can install a Stratcom listener for the 'go' event if you need to log
* or prevent redirects.
*
* @return void
*/
go : function() {
var uri = this.toString();
if (JX.Stratcom.invoke('go', null, {uri: uri}).getPrevented()) {
return;
}
if (!uri) {
// window.location.reload clears cache in Firefox.
uri = window.location.pathname + (window.location.query || '');
}
window.location = uri;
}
}
});

350
externals/javelinjs/src/lib/Vector.js vendored Normal file
View file

@ -0,0 +1,350 @@
/**
* @requires javelin-install
* javelin-event
* @provides javelin-vector
*
* @javelin-installs JX.$V
*
* @javelin
*/
/**
* Convenience function that returns a @{class:JX.Vector} instance. This allows
* you to concisely write things like:
*
* JX.$V(x, y).add(10, 10); // Explicit coordinates.
* JX.$V(node).add(50, 50).setDim(node); // Position of a node.
*
* @param number|Node If a node, returns the node's position vector.
* If numeric, the x-coordinate for the new vector.
* @param number? The y-coordinate for the new vector.
* @return @{class:JX.Vector} New vector.
*
* @group dom
*/
JX.$V = function(x, y) {
return new JX.Vector(x, y);
};
/**
* Query and update positions and dimensions of nodes (and other things) within
* within a document. Each vector has two elements, 'x' and 'y', which usually
* represent width/height ('dimension vector') or left/top ('position vector').
*
* Vectors are used to manage the sizes and positions of elements, events,
* the document, and the viewport (the visible section of the document, i.e.
* how much of the page the user can actually see in their browser window).
* Unlike most Javelin classes, @{class:JX.Vector} exposes two bare properties,
* 'x' and 'y'. You can read and manipulate these directly:
*
* // Give the user information about elements when they click on them.
* JX.Stratcom.listen(
* 'click',
* null,
* function(e) {
* var p = new JX.Vector(e);
* var d = JX.Vector.getDim(e.getTarget());
*
* alert('You clicked at <' + p.x + ',' + p.y + '> and the element ' +
* 'you clicked is ' + d.x + 'px wide and ' + d.y + 'px high.');
* });
*
* You can also update positions and dimensions using vectors:
*
* // When the user clicks on something, make it 10px wider and 10px taller.
* JX.Stratcom.listen(
* 'click',
* null,
* function(e) {
* var target = e.getTarget();
* JX.$V(target).add(10, 10).setDim(target);
* });
*
* Additionally, vectors can be used to query document and viewport information:
*
* var v = JX.Vector.getViewport(); // Viewport (window) width and height.
* var d = JX.Vector.getDocument(); // Document width and height.
* var visible_area = parseInt(100 * (v.x * v.y) / (d.x * d.y), 10);
* alert('You can currently see ' + visible_area + ' % of the document.');
*
* The function @{function:JX.$V} provides convenience construction of common
* vectors.
*
* @task query Querying Positions and Dimensions
* @task update Changing Positions and Dimensions
* @task manip Manipulating Vectors
*
* @group dom
*/
JX.install('Vector', {
/**
* Construct a vector, either from explicit coordinates or from a node
* or event. You can pass two Numbers to construct an explicit vector:
*
* var p = new JX.Vector(35, 42);
*
* Otherwise, you can pass a @{class:JX.Event} or a Node to implicitly
* construct a vector:
*
* var q = new JX.Vector(some_event);
* var r = new JX.Vector(some_node);
*
* These are just like calling JX.Vector.getPos() on the @{class:JX.Event} or
* Node.
*
* For convenience, @{function:JX.$V} constructs a new vector so you don't
* need to use the 'new' keyword. That is, these are equivalent:
*
* var s = new JX.Vector(x, y);
* var t = JX.$V(x, y);
*
* Methods like @{method:getScroll}, @{method:getViewport} and
* @{method:getDocument} also create new vectors.
*
* Once you have a vector, you can manipulate it with add():
*
* var u = JX.$V(35, 42);
* var v = u.add(5, -12); // v = <40, 30>
*
* @param wild 'x' component of the vector, or a @{class:JX.Event}, or a
* Node.
* @param Number? If providing an 'x' component, the 'y' component of the
* vector.
* @return @{class:JX.Vector} Specified vector.
* @task query
*/
construct : function(x, y) {
if (typeof y == 'undefined') {
return JX.Vector.getPos(x);
}
this.x = (x === null) ? null : parseFloat(x);
this.y = (y === null) ? null : parseFloat(y);
},
members : {
x : null,
y : null,
/**
* Move a node around by setting the position of a Node to the vector's
* coordinates. For instance, if you want to move an element to the top left
* corner of the document, you could do this (assuming it has 'position:
* absolute'):
*
* JX.$V(0, 0).setPos(node);
*
* @param Node Node to move.
* @return this
* @task update
*/
setPos : function(node) {
node.style.left = (this.x === null) ? '' : (parseInt(this.x, 10) + 'px');
node.style.top = (this.y === null) ? '' : (parseInt(this.y, 10) + 'px');
return this;
},
/**
* Change the size of a node by setting its dimensions to the vector's
* coordinates. For instance, if you want to change an element to be 100px
* by 100px:
*
* JX.$V(100, 100).setDim(node);
*
* Or if you want to expand a node's dimensions by 50px:
*
* JX.$V(node).add(50, 50).setDim(node);
*
* @param Node Node to resize.
* @return this
* @task update
*/
setDim : function(node) {
node.style.width =
(this.x === null)
? ''
: (parseInt(this.x, 10) + 'px');
node.style.height =
(this.y === null)
? ''
: (parseInt(this.y, 10) + 'px');
return this;
},
/**
* Change a vector's x and y coordinates by adding numbers to them, or
* adding the coordinates of another vector. For example:
*
* var u = JX.$V(3, 4).add(100, 200); // u = <103, 204>
*
* You can also add another vector:
*
* var q = JX.$V(777, 999);
* var r = JX.$V(1000, 2000);
* var s = q.add(r); // s = <1777, 2999>
*
* Note that this method returns a new vector. It does not modify the
* 'this' vector.
*
* @param wild Value to add to the vector's x component, or another
* vector.
* @param Number? Value to add to the vector's y component.
* @return @{class:JX.Vector} New vector, with summed components.
* @task manip
*/
add : function(x, y) {
if (x instanceof JX.Vector) {
y = x.y;
x = x.x;
}
return new JX.Vector(this.x + parseFloat(x), this.y + parseFloat(y));
}
},
statics : {
_viewport: null,
/**
* Determine where in a document an element is (or where an event, like
* a click, occurred) by building a new vector containing the position of a
* Node or @{class:JX.Event}. The 'x' component of the vector will
* correspond to the pixel offset of the argument relative to the left edge
* of the document, and the 'y' component will correspond to the pixel
* offset of the argument relative to the top edge of the document. Note
* that all vectors are generated in document coordinates, so the scroll
* position does not affect them.
*
* See also @{method:getDim}, used to determine an element's dimensions.
*
* @param Node|@{class:JX.Event} Node or event to determine the position
* of.
* @return @{class:JX.Vector} New vector with the argument's position.
* @task query
*/
getPos : function(node) {
JX.Event && (node instanceof JX.Event) && (node = node.getRawEvent());
if (('pageX' in node) || ('clientX' in node)) {
var c = JX.Vector._viewport;
return new JX.Vector(
node.pageX || (node.clientX + c.scrollLeft),
node.pageY || (node.clientY + c.scrollTop)
);
}
var x = 0;
var y = 0;
do {
x += node.offsetLeft;
y += node.offsetTop;
node = node.offsetParent;
} while (node && node != document.body);
return new JX.Vector(x, y);
},
/**
* Determine the width and height of a node by building a new vector with
* dimension information. The 'x' component of the vector will correspond
* to the element's width in pixels, and the 'y' component will correspond
* to its height in pixels.
*
* See also @{method:getPos}, used to determine an element's position.
*
* @param Node Node to determine the display size of.
* @return @{JX.$V} New vector with the node's dimensions.
* @task query
*/
getDim : function(node) {
return new JX.Vector(node.offsetWidth, node.offsetHeight);
},
/**
* Determine the current scroll position by building a new vector where
* the 'x' component corresponds to how many pixels the user has scrolled
* from the left edge of the document, and the 'y' component corresponds to
* how many pixels the user has scrolled from the top edge of the document.
*
* See also @{method:getViewport}, used to determine the size of the
* viewport.
*
* @return @{JX.$V} New vector with the document scroll position.
* @task query
*/
getScroll : function() {
// We can't use JX.Vector._viewport here because there's diversity between
// browsers with respect to where position/dimension and scroll position
// information is stored.
var b = document.body;
var e = document.documentElement;
return new JX.Vector(
window.pageXOffset || b.scrollLeft || e.scrollLeft,
window.pageYOffset || b.scrollTop || e.scrollTop
);
},
/**
* Determine the size of the viewport (basically, the browser window) by
* building a new vector where the 'x' component corresponds to the width
* of the viewport in pixels and the 'y' component corresponds to the height
* of the viewport in pixels.
*
* See also @{method:getScroll}, used to determine the position of the
* viewport, and @{method:getDocument}, used to determine the size of the
* entire document.
*
* @return @{class:JX.Vector} New vector with the viewport dimensions.
* @task query
*/
getViewport : function() {
var c = JX.Vector._viewport;
return new JX.Vector(
window.innerWidth || c.clientWidth || 0,
window.innerHeight || c.clientHeight || 0
);
},
/**
* Determine the size of the document, including any area outside the
* current viewport which the user would need to scroll in order to see, by
* building a new vector where the 'x' component corresponds to the document
* width in pixels and the 'y' component corresponds to the document height
* in pixels.
*
* @return @{class:JX.Vector} New vector with the document dimensions.
* @task query
*/
getDocument : function() {
var c = JX.Vector._viewport;
return new JX.Vector(c.scrollWidth || 0, c.scrollHeight || 0);
}
},
/**
* On initialization, the browser-dependent viewport root is determined and
* stored.
*
* In ##__DEV__##, @{class:JX.Vector} installs a toString() method so
* vectors print in a debuggable way:
*
* <23, 92>
*
* This string representation of vectors is not available in a production
* context.
*
* @return void
*/
initialize : function() {
JX.Vector._viewport = document.documentElement || document.body;
if (__DEV__) {
JX.Vector.prototype.toString = function() {
return '<' + this.x + ', ' + this.y + '>';
};
}
}
});

285
externals/javelinjs/src/lib/Workflow.js vendored Normal file
View file

@ -0,0 +1,285 @@
/**
* @requires javelin-stratcom
* javelin-request
* javelin-dom
* javelin-vector
* javelin-install
* javelin-util
* javelin-mask
* javelin-uri
* @provides javelin-workflow
* @javelin
*/
/**
* @group workflow
*/
JX.install('Workflow', {
construct : function(uri, data) {
if (__DEV__) {
if (!uri || uri == '#') {
JX.$E(
'new JX.Workflow(<?>, ...): '+
'bogus URI provided when creating workflow.');
}
}
this.setURI(uri);
this.setData(data || {});
},
events : ['error', 'finally', 'submit'],
statics : {
_stack : [],
newFromForm : function(form, data) {
var pairs = JX.DOM.convertFormToListOfPairs(form);
for (var k in data) {
pairs.push([k, data[k]]);
}
// Disable form elements during the request
var inputs = [].concat(
JX.DOM.scry(form, 'input'),
JX.DOM.scry(form, 'button'),
JX.DOM.scry(form, 'textarea'));
for (var ii = 0; ii < inputs.length; ii++) {
if (inputs[ii].disabled) {
delete inputs[ii];
} else {
inputs[ii].disabled = true;
}
}
var workflow = new JX.Workflow(form.getAttribute('action'), {});
workflow.setDataWithListOfPairs(pairs);
workflow.setMethod(form.getAttribute('method'));
workflow.listen('finally', function() {
// Re-enable form elements
for (var ii = 0; ii < inputs.length; ii++) {
inputs[ii] && (inputs[ii].disabled = false);
}
});
return workflow;
},
newFromLink : function(link) {
var workflow = new JX.Workflow(link.href);
return workflow;
},
_push : function(workflow) {
JX.Mask.show();
JX.Workflow._stack.push(workflow);
},
_pop : function() {
var dialog = JX.Workflow._stack.pop();
(dialog.getCloseHandler() || JX.bag)();
dialog._destroy();
JX.Mask.hide();
},
disable : function() {
JX.Workflow._disabled = true;
},
_onbutton : function(event) {
if (JX.Stratcom.pass()) {
return;
}
if (JX.Workflow._disabled) {
return;
}
var t = event.getTarget();
if (t.name == '__cancel__' || t.name == '__close__') {
JX.Workflow._pop();
} else {
var form = event.getNode('jx-dialog');
var data = JX.DOM.convertFormToListOfPairs(form);
data.push([t.name, true]);
var active = JX.Workflow._getActiveWorkflow();
var e = active.invoke('submit', {form: form, data: data});
if (!e.getStopped()) {
active._destroy();
active
.setURI(form.getAttribute('action') || active.getURI())
.setDataWithListOfPairs(data)
.start();
}
}
event.prevent();
},
_getActiveWorkflow : function() {
var stack = JX.Workflow._stack;
return stack[stack.length - 1];
}
},
members : {
_root : null,
_pushed : false,
_data : null,
_onload : function(r) {
// It is permissible to send back a falsey redirect to force a page
// reload, so we need to take this branch if the key is present.
if (r && (typeof r.redirect != 'undefined')) {
JX.$U(r.redirect).go();
} else if (r && r.dialog) {
this._push();
this._root = JX.$N(
'div',
{className: 'jx-client-dialog'},
JX.$H(r.dialog));
JX.DOM.listen(
this._root,
'click',
[['jx-workflow-button'], ['tag:button']],
JX.Workflow._onbutton);
document.body.appendChild(this._root);
var d = JX.Vector.getDim(this._root);
var v = JX.Vector.getViewport();
var s = JX.Vector.getScroll();
JX.$V((v.x - d.x) / 2, s.y + 100).setPos(this._root);
try {
JX.DOM.focus(JX.DOM.find(this._root, 'button', '__default__'));
var inputs = JX.DOM.scry(this._root, 'input')
.concat(JX.DOM.scry(this._root, 'textarea'));
var miny = Number.POSITIVE_INFINITY;
var target = null;
for (var ii = 0; ii < inputs.length; ++ii) {
if (inputs[ii].type != 'hidden') {
// Find the topleft-most displayed element.
var p = JX.$V(inputs[ii]);
if (p.y < miny) {
miny = p.y;
target = inputs[ii];
}
}
}
target && JX.DOM.focus(target);
} catch (_ignored) {}
} else if (this.getHandler()) {
this.getHandler()(r);
this._pop();
} else if (r) {
if (__DEV__) {
JX.$E('Response to workflow request went unhandled.');
}
}
},
_push : function() {
if (!this._pushed) {
this._pushed = true;
JX.Workflow._push(this);
}
},
_pop : function() {
if (this._pushed) {
this._pushed = false;
JX.Workflow._pop();
}
},
_destroy : function() {
if (this._root) {
JX.DOM.remove(this._root);
this._root = null;
}
},
start : function() {
var uri = this.getURI();
var method = this.getMethod();
var r = new JX.Request(uri, JX.bind(this, this._onload));
var list_of_pairs = this._data;
list_of_pairs.push(['__wflow__', true]);
r.setDataWithListOfPairs(list_of_pairs);
r.setDataSerializer(this.getDataSerializer());
if (method) {
r.setMethod(method);
}
r.listen('finally', JX.bind(this, this.invoke, 'finally'));
r.listen('error', JX.bind(this, function(error) {
var e = this.invoke('error', error);
if (e.getStopped()) {
return;
}
// TODO: Default error behavior? On Facebook Lite, we just shipped the
// user to "/error/". We could emit a blanket 'workflow-failed' type
// event instead.
}));
r.send();
},
setData : function(dictionary) {
this._data = [];
for (var k in dictionary) {
this._data.push([k, dictionary[k]]);
}
return this;
},
setDataWithListOfPairs : function(list_of_pairs) {
this._data = list_of_pairs;
return this;
}
},
properties : {
handler : null,
closeHandler : null,
dataSerializer : null,
method : null,
URI : null
},
initialize : function() {
function close_dialog_when_user_presses_escape(e) {
if (e.getSpecialKey() != 'esc') {
// Some key other than escape.
return;
}
if (JX.Workflow._disabled) {
// Workflows are disabled on this page.
return;
}
if (JX.Stratcom.pass()) {
// Something else swallowed the event.
return;
}
var active = JX.Workflow._getActiveWorkflow();
if (!active) {
// No active workflow.
return;
}
// Note: the cancel button is actually an <a /> tag.
var buttons = JX.DOM.scry(active._root, 'a', 'jx-workflow-button');
if (!buttons.length) {
// No buttons in the dialog.
return;
}
var cancel = null;
for (var ii = 0; ii < buttons.length; ii++) {
if (buttons[ii].name == '__cancel__') {
cancel = buttons[ii];
break;
}
}
if (!cancel) {
// No 'Cancel' button.
return;
}
JX.Workflow._pop();
e.prevent();
};
JX.Stratcom.listen('keydown', null, close_dialog_when_user_presses_escape);
}
});

View file

@ -0,0 +1,48 @@
/**
* @requires javelin-cookie
*/
/*
* These all are hope-and-pray tests because cookies have such a piss poor
* API in HTTP and offer so little insight from JS. This is just a
* supplement to the battle testing the cookie library has.
*/
describe('Javelin Cookie', function() {
it('should create a cookie string with the correct format', function() {
var doc = { cookie : null };
var c = new JX.Cookie('omnom');
c.setValue('nommy');
c.setDaysToLive(5);
c.setTarget(doc);
c.setPath('/');
c.setSecure(true);
c.write();
// Should be something like:
// omnom=nommy; path=/; expires=Sat, 10 Dec 2011 05:00:34 GMT; Secure;
expect(doc.cookie).toMatch(
/^omnom=nommy;\sPath=\/;\sExpires=[^;]+;\sSecure;/);
});
it('should properly encode and decode special chars in cookie values',
function() {
var value = '!@#$%^&*()?+|/=\\{}[]<>';
var doc = { cookie : null };
var c = new JX.Cookie('data');
c.setTarget(doc);
c.setValue(value);
c.write();
var data = doc.cookie.substr(0, doc.cookie.indexOf(';'));
// Make sure the raw value is all escaped
expect(data).toEqual(
'data=!%40%23%24%25%5E%26*()%3F%2B%7C%2F%3D%5C%7B%7D%5B%5D%3C%3E');
// Make sure the retrieved value is all unescaped
expect(c.read()).toEqual(value);
});
});

View file

@ -0,0 +1,206 @@
/**
* @requires javelin-uri javelin-php-serializer
*/
describe('JX.DOM', function() {
describe('uniqID', function() {
it('must expect the unexpected', function() {
// Form with an in <input /> named "id", which collides with the "id"
// attribute.
var form_id = JX.$N('form', {}, JX.$N('input', {name : 'id'}));
var form_ok = JX.$N('form', {}, JX.$N('input', {name : 'ok'}));
// Test that we avoid issues when "form.id" is actually the node named
// "id".
var id = JX.DOM.uniqID(form_id);
expect(typeof id).toBe('string');
expect(!!id).toBe(true);
var ok = JX.DOM.uniqID(form_ok);
expect(typeof ok).toBe('string');
expect(!!ok).toBe(true);
expect(id).toNotEqual(ok);
});
});
describe('invoke', function() {
it('should invoke custom events', function() {
var span = JX.$N('span', 'test');
var div = JX.$N('div', {}, span);
var data = { duck: 'quack' };
var invoked = false;
var bubbled = false;
JX.DOM.listen(span, 'custom', null, function(event) {
expect(event.getTarget()).toBe(span);
expect(event.getType()).toBe('custom');
expect(event.getData()).toBe(data);
invoked = true;
});
JX.DOM.listen(div, 'custom', null, function(event) {
expect(event.getTarget()).toBe(span); // not div
bubbled = true;
});
JX.DOM.invoke(span, 'custom', data);
expect(invoked).toBe(true);
expect(bubbled).toBe(true);
});
it('should not allow invoking native events', function() {
ensure__DEV__(true, function() {
expect(function() {
JX.DOM.invoke(JX.$N('div'), 'click');
}).toThrow();
});
});
});
describe('setContent', function() {
var node;
beforeEach(function() {
node = JX.$N('div');
});
it('should insert a node', function() {
var content = JX.$N('p');
JX.DOM.setContent(node, content);
expect(node.childNodes[0]).toEqual(content);
expect(node.childNodes.length).toEqual(1);
});
it('should insert two nodes', function() {
var content = [JX.$N('p'), JX.$N('div')];
JX.DOM.setContent(node, content);
expect(node.childNodes[0]).toEqual(content[0]);
expect(node.childNodes[1]).toEqual(content[1]);
expect(node.childNodes.length).toEqual(2);
});
it('should accept a text node', function() {
var content = 'This is not the text you are looking for';
JX.DOM.setContent(node, content);
expect(node.innerText || node.textContent).toEqual(content);
expect(node.childNodes.length).toEqual(1);
});
it('should accept nodes and strings in an array', function() {
var content = [
'This is not the text you are looking for',
JX.$N('div')
];
JX.DOM.setContent(node, content);
expect(node.childNodes[0].nodeValue).toEqual(content[0]);
expect(node.childNodes[1]).toEqual(content[1]);
expect(node.childNodes.length).toEqual(2);
});
it('should accept a JX.HTML instance', function() {
var content = JX.$H('<div />');
JX.DOM.setContent(node, content);
// Can not rely on an equals match because JX.HTML creates nodes on
// the fly
expect(node.childNodes[0].tagName).toEqual('DIV');
expect(node.childNodes.length).toEqual(1);
});
it('should accept multiple JX.HTML instances', function() {
var content = [JX.$H('<div />'), JX.$H('<a href="#"></a>')];
JX.DOM.setContent(node, content);
expect(node.childNodes[0].tagName).toEqual('DIV');
expect(node.childNodes[1].tagName).toEqual('A');
expect(node.childNodes.length).toEqual(2);
});
it('should accept nested arrays', function() {
var content = [['a', 'b'], 'c'];
JX.DOM.setContent(node, content);
expect(node.childNodes.length).toEqual(3);
});
it('should retain order when prepending', function() {
var content = [JX.$N('a'), JX.$N('b')];
JX.DOM.setContent(node, JX.$N('div'));
JX.DOM.prependContent(node, content);
expect(node.childNodes[0].tagName).toEqual('A');
expect(node.childNodes[1].tagName).toEqual('B');
expect(node.childNodes[2].tagName).toEqual('DIV');
expect(node.childNodes.length).toEqual(3);
});
it('should retain order when doing nested prepends', function() {
// Note nesting.
var content = [[JX.$N('a'), JX.$N('b')]];
JX.DOM.prependContent(node, content);
expect(node.childNodes[0].tagName).toEqual('A');
expect(node.childNodes[1].tagName).toEqual('B');
expect(node.childNodes.length).toEqual(2);
});
it('should ignore empty elements', function() {
var content = [null, undefined, [], JX.$N('p'), 2, JX.$N('div'), false,
[false, [0], [[]]], [[undefined], [,,,,,,,]]];
JX.DOM.setContent(node, content);
expect(node.childNodes[0].tagName).toEqual('P');
expect(node.childNodes[2].tagName).toEqual('DIV');
expect(node.childNodes.length).toEqual(4);
});
it('should fail when given an object with toString', function() {
// This test is just documenting the behavior of an edge case, we could
// later choose to support these objects.
var content = {toString : function() { return 'quack'; }};
var ex;
try {
// We expect JX.DOM.setContent() to throw an exception when processing
// this object, since it will try to append it directly into the DOM
// and the browser will reject it, as it isn't a node.
JX.DOM.setContent(node, content);
} catch (exception) {
ex = exception;
}
expect(!!ex).toBe(true);
});
it('should not cause array order side effects', function() {
var content = ['a', 'b'];
var original = [].concat(content);
JX.DOM.prependContent(node, content);
expect(content).toEqual(original);
});
it('should allow numbers', function() {
var content = 3;
JX.DOM.setContent(node, content);
expect(node.innerText || node.textContent).toEqual('3');
});
it('should work by re-setting a value', function() {
JX.DOM.setContent(node, 'text');
JX.DOM.setContent(node, 'another text');
expect(node.innerText || node.textContent).toEqual('another text');
});
});
});

View file

@ -0,0 +1,36 @@
/**
* @requires javelin-json
*/
describe('JSON', function() {
it('should encode and decode an object', function() {
var object = {
a: [0, 1, 2],
s: "Javelin Stuffs",
u: '\x01',
n: 1,
f: 3.14,
b: false,
nil: null,
o: {
a: 1,
b: [1, 2],
c: {
a: 2,
b: 3
}
}
};
expect(JX.JSON.parse(JX.JSON.stringify(object))).toEqual(object);
});
it('should encode undefined array indices as null', function() {
var a = [];
a.length = 2;
var o = { x : a };
expect(JX.JSON.stringify(o)).toEqual('{"x":[null,null]}');
});
});

View file

@ -0,0 +1,293 @@
/**
* @requires javelin-uri javelin-php-serializer
*/
describe('Javelin URI', function() {
it('should understand parts of a uri', function() {
var uri = JX.$U('http://www.facebook.com:123/home.php?key=value#fragment');
expect(uri.getProtocol()).toEqual('http');
expect(uri.getDomain()).toEqual('www.facebook.com');
expect(uri.getPort()).toEqual('123');
expect(uri.getPath()).toEqual('/home.php');
expect(uri.getQueryParams()).toEqual({'key' : 'value'});
expect(uri.getFragment()).toEqual('fragment');
});
it('can accept null as uri string', function() {
var uri = JX.$U(null);
expect(uri.getProtocol()).toEqual(undefined);
expect(uri.getDomain()).toEqual(undefined);
expect(uri.getPath()).toEqual(undefined);
expect(uri.getQueryParams()).toEqual({});
expect(uri.getFragment()).toEqual(undefined);
expect(uri.toString()).toEqual('');
});
it('can accept empty string as uri string', function() {
var uri = JX.$U('');
expect(uri.getProtocol()).toEqual(undefined);
expect(uri.getDomain()).toEqual(undefined);
expect(uri.getPath()).toEqual(undefined);
expect(uri.getQueryParams()).toEqual({});
expect(uri.getFragment()).toEqual(undefined);
expect(uri.toString()).toEqual('');
});
it('should understand relative uri', function() {
var uri = JX.$U('/home.php?key=value#fragment');
expect(uri.getProtocol()).toEqual(undefined);
expect(uri.getDomain()).toEqual(undefined);
expect(uri.getPath()).toEqual('/home.php');
expect(uri.getQueryParams()).toEqual({'key' : 'value'});
expect(uri.getFragment()).toEqual('fragment');
});
function charRange(from, to) {
res = '';
for (var i = from.charCodeAt(0); i <= to.charCodeAt(0); i++) {
res += String.fromCharCode(i);
}
return res;
}
it('should reject unsafe domains', function() {
var unsafe_chars =
'\x00;\\%\u2047\u2048\ufe56\ufe5f\uff03\uff0f\uff1f' +
charRange('\ufdd0', '\ufdef') + charRange('\ufff0', '\uffff');
for (var i = 0; i < unsafe_chars.length; i++) {
expect(function() {
JX.$U('http://foo' + unsafe_chars.charAt(i) + 'bar');
}).toThrow();
}
});
it('should allow safe domains', function() {
var safe_chars =
'-._' + charRange('a', 'z') + charRange('A', 'Z') + charRange('0', '9') +
'\u2046\u2049\ufdcf\ufdf0\uffef';
for (var i = 0; i < safe_chars.length; i++) {
var domain = 'foo' + safe_chars.charAt(i) + 'bar';
var uri = JX.$U('http://' + domain);
expect(uri.getDomain()).toEqual(domain);
}
});
it('should set slash as the default path', function() {
var uri = JX.$U('http://www.facebook.com');
expect(uri.getPath()).toEqual('/');
});
it('should set empty map as the default query data', function() {
var uri = JX.$U('http://www.facebook.com/');
expect(uri.getQueryParams()).toEqual({});
});
it('should set undefined as the default fragment', function() {
var uri = JX.$U('http://www.facebook.com/');
expect(uri.getFragment()).toEqual(undefined);
});
it('should understand uri with no path', function() {
var uri = JX.$U('http://www.facebook.com?key=value');
expect(uri.getPath()).toEqual('/');
expect(uri.getQueryParams()).toEqual({'key' : 'value'});
});
it('should understand multiple query keys', function() {
var uri = JX.$U('/?clown=town&herp=derp');
expect(uri.getQueryParams()).toEqual({
'clown' : 'town',
'herp' : 'derp'
});
});
it('does not set keys for nonexistant data', function() {
var uri = JX.$U('/?clown=town');
expect(uri.getQueryParams().herp).toEqual(undefined);
});
it('does not parse different types of query data', function() {
var uri = JX.$U('/?str=string&int=123&bool=true&badbool=false&raw');
expect(uri.getQueryParams()).toEqual({
'str' : 'string',
'int' : '123',
'bool' : 'true',
'badbool' : 'false',
'raw' : ''
});
});
it('should act as string', function() {
var string = 'http://www.facebook.com/home.php?key=value';
var uri = JX.$U(string);
expect(uri.toString()).toEqual(string);
expect('' + uri).toEqual(string);
});
it('can remove path', function() {
var uri = JX.$U('http://www.facebook.com/home.php?key=value');
uri.setPath(undefined);
expect(uri.getPath()).toEqual(undefined);
expect(uri.toString()).toEqual('http://www.facebook.com/?key=value');
});
it('can remove queryData by undefining it', function() {
var uri = JX.$U('http://www.facebook.com/home.php?key=value');
uri.setQueryParams(undefined);
expect(uri.getQueryParams()).toEqual(undefined);
expect(uri.toString()).toEqual('http://www.facebook.com/home.php');
});
it('can remove queryData by replacing it', function() {
var uri = JX.$U('http://www.facebook.com/home.php?key=value');
uri.setQueryParams({});
expect(uri.getQueryParams()).toEqual({});
expect(uri.toString()).toEqual('http://www.facebook.com/home.php');
});
it('can amend to removed queryData', function() {
var uri = JX.$U('http://www.facebook.com/home.php?key=value');
uri.setQueryParams({});
expect(uri.getQueryParams()).toEqual({});
uri.addQueryParams({'herp' : 'derp'});
expect(uri.getQueryParams()).toEqual({'herp' : 'derp'});
expect(uri.toString()).toEqual(
'http://www.facebook.com/home.php?herp=derp');
});
it('should properly decode entities', function() {
var uri = JX.$U('/?from=clown+town&to=cloud%20city&pass=cloud%2Bcountry');
expect(uri.getQueryParams()).toEqual({
'from' : 'clown town',
'to' : 'cloud city',
'pass' : 'cloud+country'
});
expect(uri.toString()).toEqual(
'/?from=clown%20town&to=cloud%20city&pass=cloud%2Bcountry');
});
it('can add query data', function() {
var uri = JX.$U('http://www.facebook.com/');
uri.addQueryParams({'key' : 'value'});
expect(uri.getQueryParams()).toEqual({'key' : 'value'});
expect(uri.toString()).toEqual('http://www.facebook.com/?key=value');
uri.setQueryParam('key', 'lock');
expect(uri.getQueryParams()).toEqual({'key' : 'lock'});
expect(uri.toString()).toEqual('http://www.facebook.com/?key=lock');
});
it('can add different types of query data', function() {
var uri = new JX.URI();
uri.setQueryParams({
'str' : 'string',
'int' : 123,
'bool' : true,
'badbool' : false,
'raw' : ''
});
expect(uri.toString()).toEqual(
'?str=string&int=123&bool=true&badbool=false&raw');
});
it('should properly encode entities in added query data', function() {
var uri = new JX.URI();
uri.addQueryParams({'key' : 'two words'});
expect(uri.getQueryParams()).toEqual({'key' : 'two words'});
expect(uri.toString()).toEqual('?key=two%20words');
});
it('can add multiple query data', function() {
var uri = JX.$U('http://www.facebook.com/');
uri.addQueryParams({
'clown' : 'town',
'herp' : 'derp'
});
expect(uri.getQueryParams()).toEqual({
'clown' : 'town',
'herp' : 'derp'
});
expect(uri.toString()).toEqual(
'http://www.facebook.com/?clown=town&herp=derp');
});
it('can append to existing query data', function() {
var uri = JX.$U('/?key=value');
uri.addQueryParams({'clown' : 'town'});
expect(uri.getQueryParams()).toEqual({
'key' : 'value',
'clown' : 'town'
});
expect(uri.toString()).toEqual('/?key=value&clown=town');
});
it('can merge with existing query data', function() {
var uri = JX.$U('/?key=value&clown=town');
uri.addQueryParams({
'clown' : 'ville',
'herp' : 'derp'
});
expect(uri.getQueryParams()).toEqual({
'key' : 'value',
'clown' : 'ville',
'herp' : 'derp'
});
expect(uri.toString()).toEqual('/?key=value&clown=ville&herp=derp');
});
it('can replace query data', function() {
var uri = JX.$U('/?key=value&clown=town');
uri.setQueryParams({'herp' : 'derp'});
expect(uri.getQueryParams()).toEqual({'herp' : 'derp'});
expect(uri.toString()).toEqual('/?herp=derp');
});
it('can remove query data', function() {
var uri = JX.$U('/?key=value&clown=town');
uri.addQueryParams({'key' : null});
expect(uri.getQueryParams()).toEqual({
'clown' : 'town',
'key' : null
});
expect(uri.toString()).toEqual('/?clown=town');
});
it('can remove multiple query data', function() {
var uri = JX.$U('/?key=value&clown=town&herp=derp');
uri.addQueryParams({'key' : null, 'herp' : undefined});
expect(uri.getQueryParams()).toEqual({
'clown' : 'town',
'key' : null,
'herp' : undefined
});
expect(uri.toString()).toEqual('/?clown=town');
});
it('can remove non existant query data', function() {
var uri = JX.$U('/?key=value');
uri.addQueryParams({'magic' : null});
expect(uri.getQueryParams()).toEqual({
'key' : 'value',
'magic' : null
});
expect(uri.toString()).toEqual('/?key=value');
});
it('can build uri from scratch', function() {
var uri = new JX.URI();
uri.setProtocol('http');
uri.setDomain('www.facebook.com');
uri.setPath('/home.php');
uri.setQueryParams({'key' : 'value'});
uri.setFragment('fragment');
expect(uri.toString()).toEqual(
'http://www.facebook.com/home.php?key=value#fragment');
});
it('no global state interference', function() {
var uri1 = JX.$U('/?key=value');
var uri2 = JX.$U();
expect(uri2.getQueryParams()).not.toEqual({'key' : 'value'});
});
});

View file

@ -0,0 +1,96 @@
/**
* @requires javelin-behavior
*/
describe('Javelin Behaviors', function() {
beforeEach(function() {
// Don't try this at home, kids.
JX.behavior._behaviors = {};
JX.behavior._initialized = {};
JX.behavior._statics = {};
});
it('JX.behavior should not work with clowny names', function() {
ensure__DEV__(true, function() {
expect(function() {
JX.behavior('toString', function() {});
}).toThrow();
});
});
it('JX.initBehavior should pass a config object', function() {
var called = false;
var config = 'no-value';
JX.behavior('my-behavior', function(cfg) {
called = true;
config = cfg;
});
JX.initBehaviors({});
expect(called).toBe(false);
expect(config).toEqual('no-value');
called = false;
config = null;
JX.initBehaviors({ 'my-behavior': [] });
expect(called).toBe(true);
expect(config).toBeNull();
called = false;
config = null;
JX.initBehaviors({ 'my-behavior': ['foo'] });
expect(called).toBe(true);
expect(config).toEqual('foo');
});
it('JX.initBehavior should init a behavior with no config once', function() {
var count = 0;
JX.behavior('foo', function() {
count++;
});
JX.initBehaviors({ 'foo': [] });
expect(count).toEqual(1);
JX.initBehaviors({ 'foo': [] });
expect(count).toEqual(1);
JX.initBehaviors({ 'foo': ['test'] });
expect(count).toEqual(2);
});
it('Behavior statics should persist across behavior invocations', function() {
var expect_value;
var asserted = 0;
JX.behavior('static-test', function(config, statics) {
statics.value = (statics.value || 0) + 1;
expect(statics.value).toBe(expect_value);
asserted++;
});
expect_value = 1;
JX.initBehaviors({'static-test' : [{ hog : 0 }]});
expect_value = 2;
JX.initBehaviors({'static-test' : [{ hog : 0 }]});
// Test that we actually invoked the behavior.
expect(asserted).toBe(2);
});
it('should throw for undefined behaviors', function() {
var called;
JX.behavior('can-haz', function() {
called = true;
});
expect(function() {
JX.initBehaviors({
'no-can-haz': [],
'can-haz': [],
'i-fail': []
});
}).toThrow();
expect(called).toBe(true);
});
});

110
externals/javelinjs/src/lib/behavior.js vendored Normal file
View file

@ -0,0 +1,110 @@
/**
* @provides javelin-behavior
* @requires javelin-magical-init
*
* @javelin-installs JX.behavior
* @javelin-installs JX.initBehaviors
*
* @javelin
*/
/**
* Define a Javelin behavior, which holds glue code in a structured way. See
* @{article:Concepts: Behaviors} for a detailed description of Javelin
* behaviors.
*
* To define a behavior, provide a name and a function:
*
* JX.behavior('win-a-hog', function(config, statics) {
* alert("YOU WON A HOG NAMED " + config.hogName + "!");
* });
*
* @param string Behavior name.
* @param function Behavior callback/definition.
* @return void
* @group behavior
*/
JX.behavior = function(name, control_function) {
if (__DEV__) {
if (JX.behavior._behaviors.hasOwnProperty(name)) {
JX.$E(
'JX.behavior("' + name + '", ...): '+
'behavior is already registered.');
}
if (!control_function) {
JX.$E(
'JX.behavior("' + name + '", <nothing>): '+
'initialization function is required.');
}
if (typeof control_function != 'function') {
JX.$E(
'JX.behavior("' + name + '", <garbage>): ' +
'initialization function is not a function.');
}
// IE does not enumerate over these properties
var enumerables = {
toString: true,
hasOwnProperty: true,
valueOf: true,
isPrototypeOf: true,
propertyIsEnumerable: true,
toLocaleString: true,
constructor: true
};
if (enumerables[name]) {
JX.$E(
'JX.behavior("' + name + '", <garbage>): ' +
'do not use this property as a behavior.'
);
}
}
JX.behavior._behaviors[name] = control_function;
JX.behavior._statics[name] = {};
};
/**
* Execute previously defined Javelin behaviors, running the glue code they
* contain to glue stuff together. See @{article:Concepts: Behaviors} for more
* information on Javelin behaviors.
*
* Normally, you do not call this function yourself; instead, your server-side
* library builds it for you.
*
* @param dict Map of behaviors to invoke: keys are behavior names, and values
* are lists of configuration dictionaries. The behavior will be
* invoked once for each configuration dictionary.
* @return void
* @group behavior
*/
JX.initBehaviors = function(map) {
var missing_behaviors = [];
for (var name in map) {
if (!(name in JX.behavior._behaviors)) {
missing_behaviors.push(name);
continue;
}
var configs = map[name];
if (!configs.length) {
if (JX.behavior._initialized.hasOwnProperty(name)) {
continue;
}
configs = [null];
}
for (var ii = 0; ii < configs.length; ii++) {
JX.behavior._behaviors[name](configs[ii], JX.behavior._statics[name]);
}
JX.behavior._initialized[name] = true;
}
if (missing_behaviors.length) {
JX.$E(
'JX.initBehavior(map): behavior(s) not registered: ' +
missing_behaviors.join(', ')
);
}
};
JX.behavior._behaviors = {};
JX.behavior._statics = {};
JX.behavior._initialized = {};
JX.flushHoldingQueue('behavior', JX.behavior);

View file

@ -0,0 +1,384 @@
/**
* @requires javelin-dom
* javelin-util
* javelin-stratcom
* javelin-install
* @provides javelin-tokenizer
* @javelin
*/
/**
* A tokenizer is a UI component similar to a text input, except that it
* allows the user to input a list of items ("tokens"), generally from a fixed
* set of results. A familiar example of this UI is the "To:" field of most
* email clients, where the control autocompletes addresses from the user's
* address book.
*
* @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the
* ability to choose multiple items.
*
* To build a @{JX.Tokenizer}, you need to do four things:
*
* 1. Construct it, padding a DOM node for it to attach to. See the constructor
* for more information.
* 2. Build a {@JX.Typeahead} and configure it with setTypeahead().
* 3. Configure any special options you want.
* 4. Call start().
*
* If you do this correctly, the input should suggest items and enter them as
* tokens as the user types.
*
* @group control
*/
JX.install('Tokenizer', {
construct : function(containerNode) {
this._containerNode = containerNode;
},
events : [
/**
* Emitted when the value of the tokenizer changes, similar to an 'onchange'
* from a <select />.
*/
'change'],
properties : {
limit : null,
nextInput : null
},
members : {
_containerNode : null,
_root : null,
_focus : null,
_orig : null,
_typeahead : null,
_tokenid : 0,
_tokens : null,
_tokenMap : null,
_initialValue : null,
_seq : 0,
_lastvalue : null,
_placeholder : null,
start : function() {
if (__DEV__) {
if (!this._typeahead) {
throw new Error(
'JX.Tokenizer.start(): ' +
'No typeahead configured! Use setTypeahead() to provide a ' +
'typeahead.');
}
}
this._orig = JX.DOM.find(this._containerNode, 'input', 'tokenizer-input');
this._tokens = [];
this._tokenMap = {};
var focus = this.buildInput(this._orig.value);
this._focus = focus;
var input_container = JX.DOM.scry(
this._containerNode,
'div',
'tokenizer-input-container'
);
input_container = input_container[0] || this._containerNode;
JX.DOM.listen(
focus,
['click', 'focus', 'blur', 'keydown', 'keypress'],
null,
JX.bind(this, this.handleEvent));
JX.DOM.listen(
input_container,
'click',
null,
JX.bind(
this,
function(e) {
if (e.getNode('remove')) {
this._remove(e.getNodeData('token').key, true);
} else if (e.getTarget() == this._root) {
this.focus();
}
}));
var root = JX.$N('div');
root.id = this._orig.id;
JX.DOM.alterClass(root, 'jx-tokenizer', true);
root.style.cursor = 'text';
this._root = root;
root.appendChild(focus);
var typeahead = this._typeahead;
typeahead.setInputNode(this._focus);
typeahead.start();
setTimeout(JX.bind(this, function() {
var container = this._orig.parentNode;
JX.DOM.setContent(container, root);
var map = this._initialValue || {};
for (var k in map) {
this.addToken(k, map[k]);
}
JX.DOM.appendContent(
root,
JX.$N('div', {style: {clear: 'both'}})
);
this._redraw();
}), 0);
},
setInitialValue : function(map) {
this._initialValue = map;
return this;
},
setTypeahead : function(typeahead) {
typeahead.setAllowNullSelection(false);
typeahead.removeListener();
typeahead.listen(
'choose',
JX.bind(this, function(result) {
JX.Stratcom.context().prevent();
if (this.addToken(result.rel, result.name)) {
if (this.shouldHideResultsOnChoose()) {
this._typeahead.hide();
}
this._typeahead.clear();
this._redraw();
this.focus();
}
})
);
typeahead.listen(
'query',
JX.bind(
this,
function(query) {
// TODO: We should emit a 'query' event here to allow the caller to
// generate tokens on the fly, e.g. email addresses or other freeform
// or algorithmic tokens.
// Then do this if something handles the event.
// this._focus.value = '';
// this._redraw();
// this.focus();
if (query.length) {
// Prevent this event if there's any text, so that we don't submit
// the form (either we created a token or we failed to create a
// token; in either case we shouldn't submit). If the query is
// empty, allow the event so that the form submission takes place.
JX.Stratcom.context().prevent();
}
}));
this._typeahead = typeahead;
return this;
},
shouldHideResultsOnChoose : function() {
return true;
},
handleEvent : function(e) {
this._typeahead.handleEvent(e);
if (e.getPrevented()) {
return;
}
if (e.getType() == 'click') {
if (e.getTarget() == this._root) {
this.focus();
e.prevent();
return;
}
} else if (e.getType() == 'keydown') {
this._onkeydown(e);
} else if (e.getType() == 'blur') {
this._focus.value = '';
this._redraw();
// Explicitly update the placeholder since we just wiped the field
// value.
this._typeahead.updatePlaceholder();
}
},
refresh : function() {
this._redraw(true);
return this;
},
_redraw : function(force) {
// If there are tokens in the tokenizer, never show a placeholder.
// Otherwise, show one if one is configured.
if (JX.keys(this._tokenMap).length) {
this._typeahead.setPlaceholder(null);
} else {
this._typeahead.setPlaceholder(this._placeholder);
}
var focus = this._focus;
if (focus.value === this._lastvalue && !force) {
return;
}
this._lastvalue = focus.value;
var root = this._root;
var metrics = JX.DOM.textMetrics(
this._focus,
'jx-tokenizer-metrics');
metrics.y = null;
metrics.x += 24;
metrics.setDim(focus);
// This is a pretty ugly hack to force a redraw after copy/paste in
// Firefox. If we don't do this, it doesn't redraw the input so pasting
// in an email address doesn't give you a very good behavior.
focus.value = focus.value;
},
setPlaceholder : function(string) {
this._placeholder = string;
return this;
},
addToken : function(key, value) {
if (key in this._tokenMap) {
return false;
}
var focus = this._focus;
var root = this._root;
var token = this.buildToken(key, value);
this._tokenMap[key] = {
value : value,
key : key,
node : token
};
this._tokens.push(key);
root.insertBefore(token, focus);
this.invoke('change', this);
return true;
},
removeToken : function(key) {
return this._remove(key, false);
},
buildInput: function(value) {
return JX.$N('input', {
className: 'jx-tokenizer-input',
type: 'text',
autocomplete: 'off',
value: value
});
},
/**
* Generate a token based on a key and value. The "token" and "remove"
* sigils are observed by a listener in start().
*/
buildToken: function(key, value) {
var input = JX.$N('input', {
type: 'hidden',
value: key,
name: this._orig.name + '[' + (this._seq++) + ']'
});
var remove = JX.$N('a', {
className: 'jx-tokenizer-x',
sigil: 'remove'
}, '\u00d7'); // U+00D7 multiplication sign
return JX.$N('a', {
className: 'jx-tokenizer-token',
sigil: 'token',
meta: {key: key}
}, [value, input, remove]);
},
getTokens : function() {
var result = {};
for (var key in this._tokenMap) {
result[key] = this._tokenMap[key].value;
}
return result;
},
_onkeydown : function(e) {
var focus = this._focus;
var root = this._root;
switch (e.getSpecialKey()) {
case 'tab':
var completed = this._typeahead.submit();
if (this.getNextInput()) {
if (!completed) {
this._focus.value = '';
}
setTimeout(JX.bind(this, function() {
this.getNextInput().focus();
}), 0);
}
break;
case 'delete':
if (!this._focus.value.length) {
var tok;
while (tok = this._tokens.pop()) {
if (this._remove(tok, true)) {
break;
}
}
}
break;
case 'return':
// Don't subject this to token limits.
break;
default:
if (this.getLimit() &&
JX.keys(this._tokenMap).length == this.getLimit()) {
e.prevent();
}
setTimeout(JX.bind(this, this._redraw), 0);
break;
}
},
_remove : function(index, focus) {
if (!this._tokenMap[index]) {
return false;
}
JX.DOM.remove(this._tokenMap[index].node);
delete this._tokenMap[index];
this._redraw(true);
focus && this.focus();
this.invoke('change', this);
return true;
},
focus : function() {
var focus = this._focus;
JX.DOM.show(focus);
setTimeout(function() { JX.DOM.focus(focus); }, 0);
}
}
});

View file

@ -0,0 +1,507 @@
/**
* @requires javelin-install
* javelin-dom
* javelin-vector
* javelin-util
* @provides javelin-typeahead
* @javelin
*/
/**
* A typeahead is a UI component similar to a text input, except that it
* suggests some set of results (like friends' names, common searches, or
* repository paths) as the user types them. Familiar examples of this UI
* include Google Suggest, the Facebook search box, and OS X's Spotlight
* feature.
*
* To build a @{JX.Typeahead}, you need to do four things:
*
* 1. Construct it, passing some DOM nodes for it to attach to. See the
* constructor for more information.
* 2. Attach a datasource by calling setDatasource() with a valid datasource,
* often a @{JX.TypeaheadPreloadedSource}.
* 3. Configure any special options that you want.
* 4. Call start().
*
* If you do this correctly, a dropdown menu should appear under the input as
* the user types, suggesting matching results.
*
* @task build Building a Typeahead
* @task datasource Configuring a Datasource
* @task config Configuring Options
* @task start Activating a Typeahead
* @task control Controlling Typeaheads from Javascript
* @task internal Internal Methods
* @group control
*/
JX.install('Typeahead', {
/**
* Construct a new Typeahead on some "hardpoint". At a minimum, the hardpoint
* should be a ##<div>## with "position: relative;" wrapped around a text
* ##<input>##. The typeahead's dropdown suggestions will be appended to the
* hardpoint in the DOM. Basically, this is the bare minimum requirement:
*
* LANG=HTML
* <div style="position: relative;">
* <input type="text" />
* </div>
*
* Then get a reference to the ##<div>## and pass it as 'hardpoint', and pass
* the ##<input>## as 'control'. This will enhance your boring old
* ##<input />## with amazing typeahead powers.
*
* On the Facebook/Tools stack, ##<javelin:typeahead-template />## can build
* this for you.
*
* @param Node "Hardpoint", basically an anchorpoint in the document which
* the typeahead can append its suggestion menu to.
* @param Node? Actual ##<input />## to use; if not provided, the typeahead
* will just look for a (solitary) input inside the hardpoint.
* @task build
*/
construct : function(hardpoint, control) {
this._hardpoint = hardpoint;
this._control = control || JX.DOM.find(hardpoint, 'input');
this._root = JX.$N(
'div',
{className: 'jx-typeahead-results'});
this._display = [];
this._listener = JX.DOM.listen(
this._control,
['focus', 'blur', 'keypress', 'keydown', 'input'],
null,
JX.bind(this, this.handleEvent));
JX.DOM.listen(
this._root,
['mouseover', 'mouseout'],
null,
JX.bind(this, this._onmouse));
JX.DOM.listen(
this._root,
'mousedown',
'tag:a',
JX.bind(this, function(e) {
if (!e.isRightButton()) {
this._choose(e.getNode('tag:a'));
}
}));
},
events : ['choose', 'query', 'start', 'change', 'show'],
properties : {
/**
* Boolean. If true (default), the user is permitted to submit the typeahead
* with a custom or empty selection. This is a good behavior if the
* typeahead is attached to something like a search input, where the user
* might type a freeform query or select from a list of suggestions.
* However, sometimes you require a specific input (e.g., choosing which
* user owns something), in which case you can prevent null selections.
*
* @task config
*/
allowNullSelection : true
},
members : {
_root : null,
_control : null,
_hardpoint : null,
_listener : null,
_value : null,
_stop : false,
_focus : -1,
_focused : false,
_placeholderVisible : false,
_placeholder : null,
_display : null,
_datasource : null,
_waitingListener : null,
_readyListener : null,
/**
* Activate your properly configured typeahead. It won't do anything until
* you call this method!
*
* @task start
* @return void
*/
start : function() {
this.invoke('start');
if (__DEV__) {
if (!this._datasource) {
throw new Error(
"JX.Typeahead.start(): " +
"No datasource configured. Create a datasource and call " +
"setDatasource().");
}
}
this.updatePlaceholder();
},
/**
* Configure a datasource, which is where the Typeahead gets suggestions
* from. See @{JX.TypeaheadDatasource} for more information. You must
* provide exactly one datasource.
*
* @task datasource
* @param JX.TypeaheadDatasource The datasource which the typeahead will
* draw from.
*/
setDatasource : function(datasource) {
if (this._datasource) {
this._datasource.unbindFromTypeahead();
this._waitingListener.remove();
this._readyListener.remove();
}
this._waitingListener = datasource.listen(
'waiting',
JX.bind(this, this.waitForResults)
);
this._readyListener = datasource.listen(
'resultsready',
JX.bind(this, this.showResults)
);
datasource.bindToTypeahead(this);
this._datasource = datasource;
},
getDatasource : function() {
return this._datasource;
},
/**
* Override the <input /> selected in the constructor with some other input.
* This is primarily useful when building a control on top of the typeahead,
* like @{JX.Tokenizer}.
*
* @task config
* @param node An <input /> node to use as the primary control.
*/
setInputNode : function(input) {
this._control = input;
return this;
},
/**
* Hide the typeahead's dropdown suggestion menu.
*
* @task control
* @return void
*/
hide : function() {
this._changeFocus(Number.NEGATIVE_INFINITY);
this._display = [];
this._moused = false;
JX.DOM.hide(this._root);
},
/**
* Show a given result set in the typeahead's dropdown suggestion menu.
* Normally, you don't call this method directly. Usually it gets called
* in response to events from the datasource you have configured.
*
* @task control
* @param list List of ##<a />## tags to show as suggestions/results.
* @return void
*/
showResults : function(results) {
var obj = {show: results};
var e = this.invoke('show', obj);
// Note that the results list may have been update by the "show" event
// listener. Non-result node (e.g. divider or label) may have been
// inserted.
JX.DOM.setContent(this._root, results);
this._display = JX.DOM.scry(this._root, 'a', 'typeahead-result');
if (this._display.length && !e.getPrevented()) {
this._changeFocus(Number.NEGATIVE_INFINITY);
var d = JX.Vector.getDim(this._hardpoint);
d.x = 0;
d.setPos(this._root);
if (this._root.parentNode !== this._hardpoint) {
this._hardpoint.appendChild(this._root);
}
JX.DOM.show(this._root);
} else {
this.hide();
JX.DOM.setContent(this._root, null);
}
},
refresh : function() {
if (this._stop) {
return;
}
this._value = this._control.value;
this.invoke('change', this._value);
},
/**
* Show a "waiting for results" UI in place of the typeahead's dropdown
* suggestion menu. NOTE: currently there's no such UI, lolol.
*
* @task control
* @return void
*/
waitForResults : function() {
// TODO: Build some sort of fancy spinner or "..." type UI here to
// visually indicate that we're waiting on the server.
// Wait on the datasource 'complete' event for hiding the spinner.
this.hide();
},
/**
* @task internal
*/
_onmouse : function(event) {
this._moused = (event.getType() == 'mouseover');
this._drawFocus();
},
/**
* @task internal
*/
_changeFocus : function(d) {
var n = Math.min(Math.max(-1, this._focus + d), this._display.length - 1);
if (!this.getAllowNullSelection()) {
n = Math.max(0, n);
}
if (this._focus >= 0 && this._focus < this._display.length) {
JX.DOM.alterClass(this._display[this._focus], 'focused', false);
}
this._focus = n;
this._drawFocus();
return true;
},
/**
* @task internal
*/
_drawFocus : function() {
var f = this._display[this._focus];
if (f) {
JX.DOM.alterClass(f, 'focused', !this._moused);
}
},
/**
* @task internal
*/
_choose : function(target) {
var result = this.invoke('choose', target);
if (result.getPrevented()) {
return;
}
this._control.value = target.name;
this.hide();
},
/**
* @task control
*/
clear : function() {
this._control.value = '';
this._value = '';
this.hide();
},
/**
* @task control
*/
enable : function() {
this._control.disabled = false;
this._stop = false;
},
/**
* @task control
*/
disable : function() {
this._control.blur();
this._control.disabled = true;
this._stop = true;
},
/**
* @task control
*/
submit : function() {
if (this._focus >= 0 && this._display[this._focus]) {
this._choose(this._display[this._focus]);
return true;
} else {
result = this.invoke('query', this._control.value);
if (result.getPrevented()) {
return true;
}
}
return false;
},
setValue : function(value) {
this._control.value = value;
},
getValue : function() {
return this._control.value;
},
/**
* @task internal
*/
_update : function(event) {
if (event.getType() == 'focus') {
this._focused = true;
this.updatePlaceholder();
}
var k = event.getSpecialKey();
if (k && event.getType() == 'keydown') {
switch (k) {
case 'up':
if (this._display.length && this._changeFocus(-1)) {
event.prevent();
}
break;
case 'down':
if (this._display.length && this._changeFocus(1)) {
event.prevent();
}
break;
case 'return':
if (this.submit()) {
event.prevent();
return;
}
break;
case 'esc':
if (this._display.length && this.getAllowNullSelection()) {
this.hide();
event.prevent();
}
break;
case 'tab':
// If the user tabs out of the field, don't refresh.
return;
}
}
// We need to defer because the keystroke won't be present in the input's
// value field yet.
setTimeout(JX.bind(this, function() {
if (this._value == this._control.value) {
// The typeahead value hasn't changed.
return;
}
this.refresh();
}), 0);
},
/**
* This method is pretty much internal but @{JX.Tokenizer} needs access to
* it for delegation. You might also need to delegate events here if you
* build some kind of meta-control.
*
* Reacts to user events in accordance to configuration.
*
* @task internal
* @param JX.Event User event, like a click or keypress.
* @return void
*/
handleEvent : function(e) {
if (this._stop || e.getPrevented()) {
return;
}
var type = e.getType();
if (type == 'blur') {
this._focused = false;
this.updatePlaceholder();
this.hide();
} else {
this._update(e);
}
},
removeListener : function() {
if (this._listener) {
this._listener.remove();
}
},
/**
* Set a string to display in the control when it is not focused, like
* "Type a user's name...". This string hints to the user how to use the
* control.
*
* When the string is displayed, the input will have class
* "jx-typeahead-placeholder".
*
* @param string Placeholder string, or null for no placeholder.
* @return this
*
* @task config
*/
setPlaceholder : function(string) {
this._placeholder = string;
this.updatePlaceholder();
return this;
},
/**
* Update the control to either show or hide the placeholder text as
* necessary.
*
* @return void
* @task internal
*/
updatePlaceholder : function() {
if (this._placeholderVisible) {
// If the placeholder is visible, we want to hide if the control has
// been focused or the placeholder has been removed.
if (this._focused || !this._placeholder) {
this._placeholderVisible = false;
this._control.value = '';
}
} else if (!this._focused) {
// If the placeholder is not visible, we want to show it if the control
// has benen blurred.
if (this._placeholder && !this._control.value) {
this._placeholderVisible = true;
}
}
if (this._placeholderVisible) {
// We need to resist the Tokenizer wiping the input on blur.
this._control.value = this._placeholder;
}
JX.DOM.alterClass(
this._control,
'jx-typeahead-placeholder',
this._placeholderVisible);
}
}
});

View file

@ -0,0 +1,27 @@
/**
* @requires javelin-install
* @provides javelin-typeahead-normalizer
* @javelin
*/
/**
* @group control
*/
JX.install('TypeaheadNormalizer', {
statics : {
/**
* Normalizes a string by lowercasing it and stripping out extra spaces
* and punctuation.
*
* @param string
* @return string Normalized string.
*/
normalize : function(str) {
return ('' + str)
.toLocaleLowerCase()
.replace(/[\.,-\/#!$%\^&\*;:{}=\-_`~()]/g, '')
.replace(/ +/g, ' ')
.replace(/^\s*|\s*$/g, '');
}
}
});

View file

@ -0,0 +1,78 @@
/**
* @requires javelin-install
* javelin-typeahead-source
* javelin-util
* @provides javelin-typeahead-composite-source
* @javelin
*/
/**
* @group control
*/
JX.install('TypeaheadCompositeSource', {
extend : 'TypeaheadSource',
construct : function(sources) {
JX.TypeaheadSource.call(this);
this.sources = sources;
for (var ii = 0; ii < this.sources.length; ++ii) {
var child = this.sources[ii];
child.listen('waiting', JX.bind(this, this.childWaiting));
child.listen('resultsready', JX.bind(this, this.childResultsReady));
child.listen('complete', JX.bind(this, this.childComplete));
}
},
members : {
sources : null,
results : null,
completeCount : 0,
didChange : function(value) {
this.results = [];
this.completeCount = 0;
for (var ii = 0; ii < this.sources.length; ++ii) {
this.sources[ii].didChange(value);
}
},
didStart : function() {
for (var ii = 0; ii < this.sources.length; ++ii) {
this.sources[ii].didStart();
}
},
childWaiting : function() {
if (!this.results || !this.results.length) {
this.invoke('waiting');
}
},
childResultsReady : function(nodes) {
this.results = this.mergeResults(this.results || [], nodes);
this.invoke('resultsready', this.results);
},
childComplete : function() {
this.completeCount++;
if (this.completeCount == this.sources.length) {
this.invoke('complete');
}
},
/**
* Overrideable strategy for combining results.
* By default, appends results as they come in
* so that results don't jump around.
*/
mergeResults : function(oldResults, newResults) {
for (var ii = 0; ii < newResults.length; ++ii) {
oldResults.push(newResults[ii]);
}
return oldResults;
}
}
});

View file

@ -0,0 +1,87 @@
/**
* @requires javelin-install
* javelin-util
* javelin-stratcom
* javelin-request
* javelin-typeahead-source
* @provides javelin-typeahead-ondemand-source
* @javelin
*/
/**
* @group control
*/
JX.install('TypeaheadOnDemandSource', {
extend : 'TypeaheadSource',
construct : function(uri) {
JX.TypeaheadSource.call(this);
this.uri = uri;
this.haveData = {
'' : true
};
},
properties : {
/**
* Configures how many milliseconds we wait after the user stops typing to
* send a request to the server. Setting a value of 250 means "wait 250
* milliseconds after the user stops typing to request typeahead data".
* Higher values reduce server load but make the typeahead less responsive.
*/
queryDelay : 125,
/**
* Auxiliary data to pass along when sending the query for server results.
*/
auxiliaryData : {}
},
members : {
uri : null,
lastChange : null,
haveData : null,
didChange : function(value) {
this.lastChange = JX.now();
value = this.normalize(value);
if (this.haveData[value]) {
this.matchResults(value);
} else {
this.waitForResults();
setTimeout(
JX.bind(this, this.sendRequest, this.lastChange, value),
this.getQueryDelay()
);
}
},
sendRequest : function(when, value) {
if (when != this.lastChange) {
return;
}
var r = new JX.Request(
this.uri,
JX.bind(this, this.ondata, this.lastChange, value));
r.setMethod('GET');
r.setData(JX.copy(this.getAuxiliaryData(), {q : value}));
r.send();
},
ondata : function(when, value, results) {
if (results) {
for (var ii = 0; ii < results.length; ii++) {
this.addResult(results[ii]);
}
}
this.haveData[value] = true;
if (when != this.lastChange) {
return;
}
this.matchResults(value);
}
}
});

View file

@ -0,0 +1,62 @@
/**
* @requires javelin-install
* javelin-util
* javelin-stratcom
* javelin-request
* javelin-typeahead-source
* @provides javelin-typeahead-preloaded-source
* @javelin
*/
/**
* Simple datasource that loads all possible results from a single call to a
* URI. This is appropriate if the total data size is small (up to perhaps a
* few thousand items). If you have more items so you can't ship them down to
* the client in one repsonse, use @{JX.TypeaheadOnDemandSource}.
*
* @group control
*/
JX.install('TypeaheadPreloadedSource', {
extend : 'TypeaheadSource',
construct : function(uri) {
JX.TypeaheadSource.call(this);
this.uri = uri;
},
members : {
ready : false,
uri : null,
lastValue : null,
didChange : function(value) {
if (this.ready) {
this.matchResults(value);
} else {
this.lastValue = value;
this.waitForResults();
}
},
didStart : function() {
var r = new JX.Request(this.uri, JX.bind(this, this.ondata));
r.setMethod('GET');
r.send();
},
ondata : function(results) {
for (var ii = 0; ii < results.length; ++ii) {
this.addResult(results[ii]);
}
if (this.lastValue !== null) {
this.matchResults(this.lastValue);
}
this.ready = true;
}
}
});

View file

@ -0,0 +1,349 @@
/**
* @requires javelin-install
* javelin-util
* javelin-dom
* javelin-typeahead-normalizer
* @provides javelin-typeahead-source
* @javelin
*/
/**
* @group control
*/
JX.install('TypeaheadSource', {
construct : function() {
this._raw = {};
this._lookup = {};
this.setNormalizer(JX.TypeaheadNormalizer.normalize);
this._excludeIDs = {};
},
events : ['waiting', 'resultsready', 'complete'],
properties : {
/**
* Allows you to specify a function which will be used to normalize strings.
* Strings are normalized before being tokenized, and before being sent to
* the server. The purpose of normalization is to strip out irrelevant data,
* like uppercase/lowercase, extra spaces, or punctuation. By default,
* the @{JX.TypeaheadNormalizer} is used to normalize strings, but you may
* want to provide a different normalizer, particiularly if there are
* special characters with semantic meaning in your object names.
*
* @param function
*/
normalizer : null,
/**
* If a typeahead query should be processed before being normalized and
* tokenized, specify a queryExtractor.
*
* @param function
*/
queryExtractor : null,
/**
* Transformers convert data from a wire format to a runtime format. The
* transformation mechanism allows you to choose an efficient wire format
* and then expand it on the client side, rather than duplicating data
* over the wire. The transformation is applied to objects passed to
* addResult(). It should accept whatever sort of object you ship over the
* wire, and produce a dictionary with these keys:
*
* - **id**: a unique id for each object.
* - **name**: the string used for matching against user input.
* - **uri**: the URI corresponding with the object (must be present
* but need not be meaningful)
*
* You can also give:
* - **display**: the text or nodes to show in the DOM. Usually just the
* same as ##name##.
* - **tokenizable**: if you want to tokenize something other than the
* ##name##, for the typeahead to complete on, specify it here. A
* selected entry from the typeahead will still insert the ##name##
* into the input, but the ##tokenizable## field lets you complete on
* non-name things.
*
* The default transformer expects a three element list with elements
* [name, uri, id]. It assigns the first element to both ##name## and
* ##display##.
*
* @param function
*/
transformer : null,
/**
* Configures the maximum number of suggestions shown in the typeahead
* dropdown.
*
* @param int
*/
maximumResultCount : 5,
/**
* Optional function which is used to sort results. Inputs are the input
* string, the list of matches, and a default comparator. The function
* should sort the list for display. This is the minimum useful
* implementation:
*
* function(value, list, comparator) {
* list.sort(comparator);
* }
*
* Alternatively, you may pursue more creative implementations.
*
* The `value` is a raw string; you can bind the datasource into the
* function and use normalize() or tokenize() to parse it.
*
* The `list` is a list of objects returned from the transformer function,
* see the `transformer` property. These are the objects in the list which
* match the value.
*
* The `comparator` is a sort callback which implements sensible default
* sorting rules (e.g., alphabetic order), which you can use as a fallback
* if you just want to tweak the results (e.g., put some items at the top).
*
* The function is called after the user types some text, immediately before
* the possible completion results are displayed to the user.
*
* @param function
*/
sortHandler : null
},
members : {
_raw : null,
_lookup : null,
_excludeIDs : null,
_changeListener : null,
_startListener : null,
bindToTypeahead : function(typeahead) {
this._changeListener = typeahead.listen(
'change',
JX.bind(this, this.didChange)
);
this._startListener = typeahead.listen(
'start',
JX.bind(this, this.didStart)
);
},
unbindFromTypeahead : function() {
this._changeListener.remove();
this._startListener.remove();
},
didChange : function(value) {
return;
},
didStart : function() {
return;
},
clearCache : function() {
this._raw = {};
this._lookup = {};
},
addExcludeID : function(id) {
if (id) {
this._excludeIDs[id] = true;
}
},
removeExcludeID : function (id) {
if (id) {
delete this._excludeIDs[id];
}
},
addResult : function(obj) {
obj = (this.getTransformer() || this._defaultTransformer)(obj);
if (obj.id in this._raw) {
// We're already aware of this result. This will happen if someone
// searches for "zeb" and then for "zebra" with a
// TypeaheadRequestSource, for example, or the datasource just doesn't
// dedupe things properly. Whatever the case, just ignore it.
return;
}
if (__DEV__) {
for (var k in {name : 1, id : 1, display : 1, uri : 1}) {
if (!(k in obj)) {
throw new Error(
"JX.TypeaheadSource.addResult(): " +
"result must have properties 'name', 'id', 'uri' and 'display'.");
}
}
}
this._raw[obj.id] = obj;
var t = this.tokenize(obj.tokenizable || obj.name);
for (var jj = 0; jj < t.length; ++jj) {
this._lookup[t[jj]] = this._lookup[t[jj]] || [];
this._lookup[t[jj]].push(obj.id);
}
},
waitForResults : function() {
this.invoke('waiting');
return this;
},
/**
* Get the raw state of a result by its ID. A number of other events and
* mechanisms give a list of result IDs and limited additional data; if you
* need to act on the full result data you can look it up here.
*
* @param scalar Result ID.
* @return dict Corresponding raw result.
*/
getResult : function(id) {
return this._raw[id];
},
matchResults : function(value) {
// This table keeps track of the number of tokens each potential match
// has actually matched. When we're done, the real matches are those
// which have matched every token (so the value is equal to the token
// list length).
var match_count = {};
// This keeps track of distinct matches. If the user searches for
// something like "Chris C" against "Chris Cox", the "C" will match
// both fragments. We need to make sure we only count distinct matches.
var match_fragments = {};
var matched = {};
var seen = {};
var query_extractor = this.getQueryExtractor();
if (query_extractor) {
value = query_extractor(value);
}
var t = this.tokenize(value);
// Sort tokens by longest-first. We match each name fragment with at
// most one token.
t.sort(function(u, v) { return v.length - u.length; });
for (var ii = 0; ii < t.length; ++ii) {
// Do something reasonable if the user types the same token twice; this
// is sort of stupid so maybe kill it?
if (t[ii] in seen) {
t.splice(ii--, 1);
continue;
}
seen[t[ii]] = true;
var fragment = t[ii];
for (var name_fragment in this._lookup) {
if (name_fragment.substr(0, fragment.length) === fragment) {
if (!(name_fragment in matched)) {
matched[name_fragment] = true;
} else {
continue;
}
var l = this._lookup[name_fragment];
for (var jj = 0; jj < l.length; ++jj) {
var match_id = l[jj];
if (!match_fragments[match_id]) {
match_fragments[match_id] = {};
}
if (!(fragment in match_fragments[match_id])) {
match_fragments[match_id][fragment] = true;
match_count[match_id] = (match_count[match_id] || 0) + 1;
}
}
}
}
}
var hits = [];
for (var k in match_count) {
if (match_count[k] == t.length && !this._excludeIDs[k]) {
hits.push(k);
}
}
this.sortHits(value, hits);
var nodes = this.renderNodes(value, hits);
this.invoke('resultsready', nodes);
this.invoke('complete');
},
sortHits : function(value, hits) {
var objs = [];
for (var ii = 0; ii < hits.length; ii++) {
objs.push(this._raw[hits[ii]]);
}
var default_comparator = function(u, v) {
var key_u = u.sort || u.name;
var key_v = v.sort || v.name;
return key_u.localeCompare(key_v);
};
var handler = this.getSortHandler() || function(value, list, cmp) {
list.sort(cmp);
};
handler(value, objs, default_comparator);
hits.splice(0, hits.length);
for (var ii = 0; ii < objs.length; ii++) {
hits.push(objs[ii].id);
}
},
renderNodes : function(value, hits) {
var n = Math.min(this.getMaximumResultCount(), hits.length);
var nodes = [];
for (var kk = 0; kk < n; kk++) {
nodes.push(this.createNode(this._raw[hits[kk]]));
}
return nodes;
},
createNode : function(data) {
return JX.$N(
'a',
{
sigil: 'typeahead-result',
href: data.uri,
name: data.name,
rel: data.id,
className: 'jx-result'
},
data.display
);
},
normalize : function(str) {
return this.getNormalizer()(str);
},
tokenize : function(str) {
str = this.normalize(str);
if (!str.length) {
return [];
}
return str.split(/\s/g);
},
_defaultTransformer : function(object) {
return {
name : object[0],
display : object[0],
uri : object[1],
id : object[2]
};
}
}
});

View file

@ -0,0 +1,40 @@
/**
* @requires javelin-install
* javelin-typeahead-source
* @provides javelin-typeahead-static-source
* @javelin
*/
/**
* Typeahead source that uses static data passed to the constructor. For larger
* datasets, use @{class:JX.TypeaheadPreloadedSource} or
* @{class:JX.TypeaheadOnDemandSource} to improve performance.
*
* @group control
*/
JX.install('TypeaheadStaticSource', {
extend : 'TypeaheadSource',
construct : function(data) {
JX.TypeaheadSource.call(this);
this._data = data;
},
members : {
_data : null,
didChange : function(value) {
this.matchResults(value);
},
didStart : function() {
for (var ii = 0; ii < this._data.length; ii++) {
this.addResult(this._data[ii]);
}
}
}
});

View file

@ -138,8 +138,6 @@ else
(cd phabricator && git pull --rebase)
fi
(cd phabricator && git submodule update --init)
echo
echo
echo "Install probably worked mostly correctly. Continue with the 'Configuration Guide':";

View file

@ -81,8 +81,6 @@ else
(cd phabricator && git pull --rebase)
fi
(cd phabricator && git submodule update --init)
echo
echo
echo "Install probably worked mostly correctly. Continue with the 'Configuration Guide':";

View file

@ -28,7 +28,6 @@ git pull
cd $ROOT/phabricator
git pull
git submodule update --init
### RUN TESTS ##################################################################

View file

@ -3192,7 +3192,7 @@ celerity_register_resource_map(array(
),
'sprite-icon-css' =>
array(
'uri' => '/res/698745d1/rsrc/css/sprite-icon.css',
'uri' => '/res/e7d63fcf/rsrc/css/sprite-icon.css',
'type' => 'css',
'requires' =>
array(
@ -3238,7 +3238,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
57036208 =>
'86c4a3b2' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -3282,7 +3282,7 @@ celerity_register_resource_map(array(
36 => 'phabricator-object-item-list-view-css',
37 => 'global-drag-and-drop-css',
),
'uri' => '/res/pkg/57036208/core.pkg.css',
'uri' => '/res/pkg/86c4a3b2/core.pkg.css',
'type' => 'css',
),
'c90b892e' =>
@ -3472,19 +3472,19 @@ celerity_register_resource_map(array(
'reverse' =>
array(
'aphront-attached-file-view-css' => '83f07678',
'aphront-crumbs-view-css' => '57036208',
'aphront-dialog-view-css' => '57036208',
'aphront-error-view-css' => '57036208',
'aphront-form-view-css' => '57036208',
'aphront-crumbs-view-css' => '86c4a3b2',
'aphront-dialog-view-css' => '86c4a3b2',
'aphront-error-view-css' => '86c4a3b2',
'aphront-form-view-css' => '86c4a3b2',
'aphront-headsup-action-list-view-css' => 'ec01d039',
'aphront-headsup-view-css' => '57036208',
'aphront-list-filter-view-css' => '57036208',
'aphront-pager-view-css' => '57036208',
'aphront-panel-view-css' => '57036208',
'aphront-table-view-css' => '57036208',
'aphront-tokenizer-control-css' => '57036208',
'aphront-tooltip-css' => '57036208',
'aphront-typeahead-control-css' => '57036208',
'aphront-headsup-view-css' => '86c4a3b2',
'aphront-list-filter-view-css' => '86c4a3b2',
'aphront-pager-view-css' => '86c4a3b2',
'aphront-panel-view-css' => '86c4a3b2',
'aphront-table-view-css' => '86c4a3b2',
'aphront-tokenizer-control-css' => '86c4a3b2',
'aphront-tooltip-css' => '86c4a3b2',
'aphront-typeahead-control-css' => '86c4a3b2',
'differential-changeset-view-css' => 'ec01d039',
'differential-core-view-css' => 'ec01d039',
'differential-inline-comment-editor' => 'ac53d36a',
@ -3498,7 +3498,7 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => 'ec01d039',
'diffusion-commit-view-css' => 'c8ce2d88',
'diffusion-icons-css' => 'c8ce2d88',
'global-drag-and-drop-css' => '57036208',
'global-drag-and-drop-css' => '86c4a3b2',
'inline-comment-summary-css' => 'ec01d039',
'javelin-aphlict' => 'c90b892e',
'javelin-behavior' => 'fbeded59',
@ -3568,48 +3568,48 @@ celerity_register_resource_map(array(
'javelin-util' => 'fbeded59',
'javelin-vector' => 'fbeded59',
'javelin-workflow' => 'fbeded59',
'lightbox-attachment-css' => '57036208',
'lightbox-attachment-css' => '86c4a3b2',
'maniphest-task-summary-css' => '83f07678',
'maniphest-transaction-detail-css' => '83f07678',
'phabricator-busy' => 'c90b892e',
'phabricator-content-source-view-css' => 'ec01d039',
'phabricator-core-buttons-css' => '57036208',
'phabricator-core-css' => '57036208',
'phabricator-crumbs-view-css' => '57036208',
'phabricator-directory-css' => '57036208',
'phabricator-core-buttons-css' => '86c4a3b2',
'phabricator-core-css' => '86c4a3b2',
'phabricator-crumbs-view-css' => '86c4a3b2',
'phabricator-directory-css' => '86c4a3b2',
'phabricator-drag-and-drop-file-upload' => 'ac53d36a',
'phabricator-dropdown-menu' => 'c90b892e',
'phabricator-file-upload' => 'c90b892e',
'phabricator-filetree-view-css' => '57036208',
'phabricator-flag-css' => '57036208',
'phabricator-form-view-css' => '57036208',
'phabricator-header-view-css' => '57036208',
'phabricator-jump-nav' => '57036208',
'phabricator-filetree-view-css' => '86c4a3b2',
'phabricator-flag-css' => '86c4a3b2',
'phabricator-form-view-css' => '86c4a3b2',
'phabricator-header-view-css' => '86c4a3b2',
'phabricator-jump-nav' => '86c4a3b2',
'phabricator-keyboard-shortcut' => 'c90b892e',
'phabricator-keyboard-shortcut-manager' => 'c90b892e',
'phabricator-main-menu-view' => '57036208',
'phabricator-main-menu-view' => '86c4a3b2',
'phabricator-menu-item' => 'c90b892e',
'phabricator-nav-view-css' => '57036208',
'phabricator-nav-view-css' => '86c4a3b2',
'phabricator-notification' => 'c90b892e',
'phabricator-notification-css' => '57036208',
'phabricator-notification-menu-css' => '57036208',
'phabricator-object-item-list-view-css' => '57036208',
'phabricator-notification-css' => '86c4a3b2',
'phabricator-notification-menu-css' => '86c4a3b2',
'phabricator-object-item-list-view-css' => '86c4a3b2',
'phabricator-object-selector-css' => 'ec01d039',
'phabricator-paste-file-upload' => 'c90b892e',
'phabricator-prefab' => 'c90b892e',
'phabricator-project-tag-css' => '83f07678',
'phabricator-remarkup-css' => '57036208',
'phabricator-remarkup-css' => '86c4a3b2',
'phabricator-shaped-request' => 'ac53d36a',
'phabricator-side-menu-view-css' => '57036208',
'phabricator-standard-page-view' => '57036208',
'phabricator-side-menu-view-css' => '86c4a3b2',
'phabricator-standard-page-view' => '86c4a3b2',
'phabricator-textareautils' => 'c90b892e',
'phabricator-tooltip' => 'c90b892e',
'phabricator-transaction-view-css' => '57036208',
'phabricator-zindex-css' => '57036208',
'sprite-apps-large-css' => '57036208',
'sprite-gradient-css' => '57036208',
'sprite-icon-css' => '57036208',
'sprite-menu-css' => '57036208',
'syntax-highlighting-css' => '57036208',
'phabricator-transaction-view-css' => '86c4a3b2',
'phabricator-zindex-css' => '86c4a3b2',
'sprite-apps-large-css' => '86c4a3b2',
'sprite-gradient-css' => '86c4a3b2',
'sprite-icon-css' => '86c4a3b2',
'sprite-menu-css' => '86c4a3b2',
'syntax-highlighting-css' => '86c4a3b2',
),
));

View file

@ -11,8 +11,7 @@ final class PhabricatorCaches {
/**
* Highly specialized cache for performing setup checks. We use this cache
* to determine if we need to run expensive setup checks (e.g., verifying
* submodule versions, PATH, the presence of binaries, etc.) when the page
* to determine if we need to run expensive setup checks when the page
* loads. Without it, we would need to run these checks every time.
*
* Normally, this cache is just APC. In the absence of APC, this cache

View file

@ -63,8 +63,6 @@ dependencies:
somewhere/ $ git clone git://github.com/facebook/libphutil.git
somewhere/ $ git clone git://github.com/facebook/arcanist.git
somewhere/ $ git clone git://github.com/facebook/phabricator.git
somewhere/ $ cd phabricator
somewhere/phabricator/ $ git submodule update --init
= Installing APC (Optional) =
@ -124,8 +122,7 @@ Since Phabricator is under active development, you should update frequently. To
update Phabricator:
- Stop the webserver.
- Run `git pull && git submodule update --init` in `libphutil/`,
`arcanist/` and `phabricator/`.
- Run `git pull` in `libphutil/`, `arcanist/` and `phabricator/`.
- Run `phabricator/bin/storage upgrade`.
- Restart the webserver.

View file

@ -154,51 +154,6 @@ final class PhabricatorSetup {
$root = dirname(phutil_get_library_root('phabricator'));
self::writeHeader("GIT SUBMODULES");
if (!Filesystem::pathExists($root.'/.git')) {
self::write(" skip Not a git clone.\n\n");
} else {
list($info) = execx(
'(cd %s && git submodule status)',
$root);
foreach (explode("\n", rtrim($info)) as $line) {
$matches = null;
if (!preg_match('/^(.)([0-9a-f]{40}) (\S+)(?: |$)/', $line, $matches)) {
self::writeFailure();
self::write(
"Setup failure! 'git submodule' produced unexpected output:\n".
$line);
return;
}
$status = $matches[1];
$module = $matches[3];
switch ($status) {
case '-':
case '+':
case 'U':
self::writeFailure();
self::write(
"Setup failure! Git submodule '{$module}' is not up to date. ".
"Run:\n\n".
" cd {$root} && git submodule update --init\n\n".
"...to update submodules.");
return;
case ' ':
self::write(" okay Git submodule '{$module}' up to date.\n");
break;
default:
self::writeFailure();
self::write(
"Setup failure! 'git submodule' reported unknown status ".
"'{$status}' for submodule '{$module}'. This is a bug; report ".
"it to the Phabricator maintainers.");
return;
}
}
}
self::write("[OKAY] All submodules OKAY.\n");
self::writeHeader("BASIC CONFIGURATION");

View file

@ -19,8 +19,8 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
require_once $root.'/scripts/__init_script__.php';
if ($this->haveSymbolsBinary === null) {
$binary = $this->getSymbolsBinaryPath();
$this->haveSymbolsBinary = Filesystem::pathExists($binary);
list($err) = exec_manual('which javelinsymbols');
$this->haveSymbolsBinary = !$err;
if (!$this->haveSymbolsBinary) {
return;
}
@ -28,6 +28,10 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
$futures = array();
foreach ($paths as $path) {
if ($this->shouldIgnorePath($path)) {
continue;
}
$future = $this->newSymbolsFuture($path);
$futures[$path] = $future;
}
@ -53,11 +57,18 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency',
self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency',
self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency',
self::LINT_MISSING_BINARY => '`javelinsymbols` Binary Not Built',
self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path',
);
}
private function shouldIgnorePath($path) {
return preg_match('@/__tests__/|externals/javelinjs/src/docs/@', $path);
}
public function lintPath($path) {
if ($this->shouldIgnorePath($path)) {
return;
}
if (!$this->haveSymbolsBinary) {
if (!$this->haveWarnedAboutBinary) {
@ -68,9 +79,10 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
1,
0,
self::LINT_MISSING_BINARY,
"The 'javelinsymbols' binary in the Javelin project has not been ".
"built, so the Javelin linter can't run. This isn't a big concern, ".
"but means some Javelin problems can't be automatically detected.");
"The 'javelinsymbols' binary in the Javelin project is not ".
"available in \$PATH, so the Javelin linter can't run. This ".
"isn't a big concern, but means some Javelin problems can't be ".
"automatically detected.");
}
return;
}
@ -114,7 +126,7 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
$celerity = CelerityResourceMap::getInstance();
$path = preg_replace(
'@^externals/javelin/src/@',
'@^externals/javelinjs/src/@',
'webroot/rsrc/js/javelin/',
$path);
$need = $external_classes;
@ -175,20 +187,13 @@ final class PhabricatorJavelinLinter extends ArcanistLinter {
}
private function newSymbolsFuture($path) {
$javelinsymbols = $this->getSymbolsBinaryPath();
$javelinsymbols = 'javelinsymbols';
$future = new ExecFuture($javelinsymbols.' # '.escapeshellarg($path));
$future->write($this->getData($path));
return $future;
}
private function getSymbolsBinaryPath() {
$root = dirname(phutil_get_library_root('phabricator'));
$support = $root.'/externals/javelin/support';
return $support.'/javelinsymbols/javelinsymbols';
}
private function getUsedAndInstalledSymbolsForPath($path) {
list($symbols) = $this->loadSymbols($path);
$symbols = trim($symbols);

View file

@ -1 +1 @@
../../../externals/javelin/src/
../../../externals/javelinjs/src/