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:
parent
22c64c67ff
commit
07767fda00
70 changed files with 10264 additions and 117 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -1,3 +0,0 @@
|
|||
[submodule "externals/javelin"]
|
||||
path = externals/javelin
|
||||
url = git://github.com/facebook/javelin.git
|
1
externals/javelin
vendored
1
externals/javelin
vendored
|
@ -1 +0,0 @@
|
|||
Subproject commit 32c6e43f4b8b84df940bed8ed8d073e67f6c2b28
|
25
externals/javelinjs/.gitignore
vendored
Normal file
25
externals/javelinjs/.gitignore
vendored
Normal 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
25
externals/javelinjs/LICENSE
vendored
Normal 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
33
externals/javelinjs/README
vendored
Normal 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
321
externals/javelinjs/src/core/Event.js
vendored
Normal 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
646
externals/javelinjs/src/core/Stratcom.js
vendored
Normal 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++;
|
||||
}
|
||||
}
|
||||
});
|
39
externals/javelinjs/src/core/__tests__/event-stop-and-kill.js
vendored
Normal file
39
externals/javelinjs/src/core/__tests__/event-stop-and-kill.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
152
externals/javelinjs/src/core/__tests__/install.js
vendored
Normal file
152
externals/javelinjs/src/core/__tests__/install.js
vendored
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
184
externals/javelinjs/src/core/__tests__/stratcom.js
vendored
Normal file
184
externals/javelinjs/src/core/__tests__/stratcom.js
vendored
Normal 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');
|
||||
// });
|
||||
|
||||
});
|
85
externals/javelinjs/src/core/__tests__/util.js
vendored
Normal file
85
externals/javelinjs/src/core/__tests__/util.js
vendored
Normal 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
224
externals/javelinjs/src/core/init.js
vendored
Normal 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
459
externals/javelinjs/src/core/install.js
vendored
Normal 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
367
externals/javelinjs/src/core/util.js
vendored
Normal 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
75
externals/javelinjs/src/docs/Base.js
vendored
Normal 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()> //
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
182
externals/javelinjs/src/docs/concepts/behaviors.diviner
vendored
Normal file
182
externals/javelinjs/src/docs/concepts/behaviors.diviner
vendored
Normal 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.
|
||||
|
191
externals/javelinjs/src/docs/concepts/event_delegation.diviner
vendored
Normal file
191
externals/javelinjs/src/docs/concepts/event_delegation.diviner
vendored
Normal 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) {
|
||||
// ...
|
||||
});
|
129
externals/javelinjs/src/docs/concepts/sigils_metadata.diviner
vendored
Normal file
129
externals/javelinjs/src/docs/concepts/sigils_metadata.diviner
vendored
Normal 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}.
|
82
externals/javelinjs/src/docs/facebook.diviner
vendored
Normal file
82
externals/javelinjs/src/docs/facebook.diviner
vendored
Normal 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
22
externals/javelinjs/src/docs/onload.js
vendored
Normal 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
33
externals/javelinjs/src/ext/fx/Color.js
vendored
Normal 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
214
externals/javelinjs/src/ext/fx/FX.js
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
48
externals/javelinjs/src/ext/reactor/core/DynVal.js
vendored
Normal file
48
externals/javelinjs/src/ext/reactor/core/DynVal.js
vendored
Normal 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;
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
90
externals/javelinjs/src/ext/reactor/core/Reactor.js
vendored
Normal file
90
externals/javelinjs/src/ext/reactor/core/Reactor.js
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
97
externals/javelinjs/src/ext/reactor/core/ReactorNode.js
vendored
Normal file
97
externals/javelinjs/src/ext/reactor/core/ReactorNode.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
48
externals/javelinjs/src/ext/reactor/core/ReactorNodeCalmer.js
vendored
Normal file
48
externals/javelinjs/src/ext/reactor/core/ReactorNodeCalmer.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
406
externals/javelinjs/src/ext/reactor/dom/RDOM.js
vendored
Normal file
406
externals/javelinjs/src/ext/reactor/dom/RDOM.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
136
externals/javelinjs/src/ext/view/HTMLView.js
vendored
Normal file
136
externals/javelinjs/src/ext/view/HTMLView.js
vendored
Normal 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
189
externals/javelinjs/src/ext/view/View.js
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
71
externals/javelinjs/src/ext/view/ViewInterpreter.js
vendored
Normal file
71
externals/javelinjs/src/ext/view/ViewInterpreter.js
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
103
externals/javelinjs/src/ext/view/ViewPlaceholder.js
vendored
Normal file
103
externals/javelinjs/src/ext/view/ViewPlaceholder.js
vendored
Normal 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: {}
|
||||
}
|
||||
});
|
19
externals/javelinjs/src/ext/view/ViewRenderer.js
vendored
Normal file
19
externals/javelinjs/src/ext/view/ViewRenderer.js
vendored
Normal 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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
36
externals/javelinjs/src/ext/view/ViewVisitor.js
vendored
Normal file
36
externals/javelinjs/src/ext/view/ViewVisitor.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
25
externals/javelinjs/src/ext/view/__tests__/HTMLView.js
vendored
Normal file
25
externals/javelinjs/src/ext/view/__tests__/HTMLView.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
61
externals/javelinjs/src/ext/view/__tests__/View.js
vendored
Normal file
61
externals/javelinjs/src/ext/view/__tests__/View.js
vendored
Normal 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');
|
||||
});
|
||||
});
|
47
externals/javelinjs/src/ext/view/__tests__/ViewInterpreter.js
vendored
Normal file
47
externals/javelinjs/src/ext/view/__tests__/ViewInterpreter.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
25
externals/javelinjs/src/ext/view/__tests__/ViewRenderer.js
vendored
Normal file
25
externals/javelinjs/src/ext/view/__tests__/ViewRenderer.js
vendored
Normal 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
102
externals/javelinjs/src/lib/Cookie.js
vendored
Normal 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
883
externals/javelinjs/src/lib/DOM.js
vendored
Normal 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><script src="evil.com" /></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, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 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
222
externals/javelinjs/src/lib/History.js
vendored
Executable 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
155
externals/javelinjs/src/lib/JSON.js
vendored
Normal 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
109
externals/javelinjs/src/lib/Mask.js
vendored
Normal 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
471
externals/javelinjs/src/lib/Request.js
vendored
Normal 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
177
externals/javelinjs/src/lib/Resource.js
vendored
Normal 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
234
externals/javelinjs/src/lib/URI.js
vendored
Normal 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
350
externals/javelinjs/src/lib/Vector.js
vendored
Normal 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
285
externals/javelinjs/src/lib/Workflow.js
vendored
Normal 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);
|
||||
}
|
||||
|
||||
});
|
48
externals/javelinjs/src/lib/__tests__/Cookie.js
vendored
Normal file
48
externals/javelinjs/src/lib/__tests__/Cookie.js
vendored
Normal 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);
|
||||
});
|
||||
|
||||
});
|
206
externals/javelinjs/src/lib/__tests__/DOM.js
vendored
Normal file
206
externals/javelinjs/src/lib/__tests__/DOM.js
vendored
Normal 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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
36
externals/javelinjs/src/lib/__tests__/JSON.js
vendored
Normal file
36
externals/javelinjs/src/lib/__tests__/JSON.js
vendored
Normal 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]}');
|
||||
});
|
||||
|
||||
});
|
293
externals/javelinjs/src/lib/__tests__/URI.js
vendored
Normal file
293
externals/javelinjs/src/lib/__tests__/URI.js
vendored
Normal 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'});
|
||||
});
|
||||
|
||||
});
|
||||
|
96
externals/javelinjs/src/lib/__tests__/behavior.js
vendored
Normal file
96
externals/javelinjs/src/lib/__tests__/behavior.js
vendored
Normal 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
110
externals/javelinjs/src/lib/behavior.js
vendored
Normal 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);
|
384
externals/javelinjs/src/lib/control/tokenizer/Tokenizer.js
vendored
Normal file
384
externals/javelinjs/src/lib/control/tokenizer/Tokenizer.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
507
externals/javelinjs/src/lib/control/typeahead/Typeahead.js
vendored
Normal file
507
externals/javelinjs/src/lib/control/typeahead/Typeahead.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
27
externals/javelinjs/src/lib/control/typeahead/normalizer/TypeaheadNormalizer.js
vendored
Executable file
27
externals/javelinjs/src/lib/control/typeahead/normalizer/TypeaheadNormalizer.js
vendored
Executable 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, '');
|
||||
}
|
||||
}
|
||||
});
|
78
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadCompositeSource.js
vendored
Normal file
78
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadCompositeSource.js
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
87
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadOnDemandSource.js
vendored
Normal file
87
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadOnDemandSource.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
62
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadPreloadedSource.js
vendored
Normal file
62
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadPreloadedSource.js
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
349
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadSource.js
vendored
Normal file
349
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadSource.js
vendored
Normal 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]
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
40
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadStaticSource.js
vendored
Normal file
40
externals/javelinjs/src/lib/control/typeahead/source/TypeaheadStaticSource.js
vendored
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -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':";
|
||||
|
|
|
@ -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':";
|
||||
|
|
|
@ -28,7 +28,6 @@ git pull
|
|||
|
||||
cd $ROOT/phabricator
|
||||
git pull
|
||||
git submodule update --init
|
||||
|
||||
|
||||
### RUN TESTS ##################################################################
|
||||
|
|
|
@ -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',
|
||||
),
|
||||
));
|
||||
|
|
3
src/applications/cache/PhabricatorCaches.php
vendored
3
src/applications/cache/PhabricatorCaches.php
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1 +1 @@
|
|||
../../../externals/javelin/src/
|
||||
../../../externals/javelinjs/src/
|
Loading…
Reference in a new issue