Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/view/page/standard/PhabricatorStandardPageView.php b/src/view/page/standard/PhabricatorStandardPageView.php
index d5376d2fa6..a350955b95 100755
--- a/src/view/page/standard/PhabricatorStandardPageView.php
+++ b/src/view/page/standard/PhabricatorStandardPageView.php
@@ -1,111 +1,121 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorStandardPageView extends AphrontPageView {
private $baseURI;
private $applicationName;
private $tabs = array();
private $selectedTab;
private $glyph;
public function setApplicationName($application_name) {
$this->applicationName = $application_name;
return $this;
}
public function getApplicationName() {
return $this->applicationName;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setTabs(array $tabs, $selected_tab) {
$this->tabs = $tabs;
$this->selectedTab = $selected_tab;
return $this;
}
public function getTitle() {
return $this->getGlyph().' '.parent::getTitle();
}
protected function getHead() {
return
'<link rel="stylesheet" type="text/css" href="/rsrc/css/base.css" />'.
- '<link rel="stylesheet" type="text/css" href="/rsrc/css/syntax.css" />';
+ '<link rel="stylesheet" type="text/css" href="/rsrc/css/syntax.css" />'.
+ '<script type="text/javascript">window.__DEV__=1;</script>'.
+ '<script type="text/javascript" src="/rsrc/js/javelin/init.dev.js">'.
+ '</script>';
}
public function setGlyph($glyph) {
$this->glyph = $glyph;
return $this;
}
public function getGlyph() {
return $this->glyph;
}
protected function getBody() {
$tabs = array();
foreach ($this->tabs as $name => $tab) {
$tabs[] = phutil_render_tag(
'a',
array(
'href' => idx($tab, 'href'),
'class' => ($name == $this->selectedTab)
? 'aphront-selected-tab'
: null,
),
phutil_escape_html(idx($tab, 'name')));
}
$tabs = implode('', $tabs);
if ($tabs) {
$tabs = '<span class="aphront-head-tabs">'.$tabs.'</span>';
}
return
'<div class="aphront-standard-page">'.
'<div class="aphront-standard-header">'.
'<a href="/">Phabricator</a> '.
phutil_render_tag(
'a',
array(
'href' => $this->getBaseURI(),
'class' => 'aphront-head-appname',
),
phutil_escape_html($this->getApplicationName())).
$tabs.
'</div>'.
$this->renderChildren().
'<div style="clear: both;"></div>'.
'</div>';
}
protected function getTail() {
- return '';
+ return
+ '<script type="text/javascript" src="/rsrc/js/javelin/javelin.dev.js">'.
+ '</script>'.
+ '<script type="text/javascript">'.
+ 'JX.Stratcom.mergeData(0, {});'.
+ '</script>';
+
+ ;
}
}
diff --git a/webroot/rsrc/js/javelin/init.dev.js b/webroot/rsrc/js/javelin/init.dev.js
new file mode 100644
index 0000000000..38c386e7fb
--- /dev/null
+++ b/webroot/rsrc/js/javelin/init.dev.js
@@ -0,0 +1,179 @@
+/**
+ * Javelin core; installs Javelin and Stratcom event delegation.
+ *
+ * @provides javelin-magical-init
+ * @nopackage
+ *
+ * @javelin-installs JX.__rawEventQueue
+ * @javelin-installs JX.__simulate
+ * @javelin-installs JX.enableDispatch
+ * @javelin-installs JX.onload
+ *
+ * @javelin
+ */
+(function() {
+
+
+ if (window.JX) {
+ return;
+ }
+
+ window.JX = {};
+ 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 ii = 0; ii < onload.length; ii++) {
+ onload[ii]();
+ }
+
+ }
+
+ Stratcom.dispatch(evt);
+ }
+ } else {
+ var target = what.srcElement || what.target;
+ if (target &&
+ (what.type in {click: 1, submit: 1}) &&
+ (/ FI_CAPTURE /).test(' ' + target.className + ' ')) {
+ 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 (target.addEventListener) {
+ target.addEventListener(type, JX.__rawEventQueue, true);
+ } else if (target.attachEvent) {
+ target.attachEvent('on' + type, JX.__rawEventQueue);
+ }
+ };
+
+ var document_events = [
+ 'click',
+ 'change',
+ 'keypress',
+ 'mousedown',
+ 'mouseover',
+ 'mouseout',
+ 'mouseup',
+ 'keydown',
+ 'drop',
+ 'dragenter',
+ 'dragleave',
+ 'dragover'
+ ];
+
+ // 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 odd, and submit differently
+ if (window.opera) {
+ document_events.push('focus', 'blur');
+ } else {
+ document_events.push('submit');
+ }
+
+ 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',
+ 'focus',
+ 'blur'
+ ];
+
+ 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);
+ }
+ }
+
+
+})();
diff --git a/webroot/rsrc/js/javelin/init.min.js b/webroot/rsrc/js/javelin/init.min.js
new file mode 100644
index 0000000000..af32f431c8
--- /dev/null
+++ b/webroot/rsrc/js/javelin/init.min.js
@@ -0,0 +1 @@
+(function(){if(window.JX)return;window.JX={};window.__DEV__=window.__DEV__||0;var d=false;var f=[];var e=[];var h=document.documentElement;var b=!!h.addEventListener;JX.__rawEventQueue=function(o){e.push(o);var j=JX.Stratcom;if(j&&j.ready){var m=e;e=[];for(var l=0;l<m.length;++l){var k=m[l];try{var test=k.type;}catch(p){continue;}if(!d&&k.type=='domready'){document.body&&(document.body.id=null);d=true;for(var l=0;l<f.length;l++)f[l]();}j.dispatch(k);}}else{var n=o.srcElement||o.target;if(n&&(o.type in {click:1,submit:1})&&(/ FI_CAPTURE /).test(' '+n.className+' ')){o.returnValue=false;o.preventDefault&&o.preventDefault();document.body.id='event_capture';if(!add_event_listener&&document.createEventObject){e.pop();e.push(document.createEventObject(o));}return false;}}};JX.enableDispatch=function(j,k){if(j.addEventListener){j.addEventListener(k,JX.__rawEventQueue,true);}else if(j.attachEvent)j.attachEvent('on'+k,JX.__rawEventQueue);};var a=['click','change','keypress','mousedown','mouseover','mouseout','mouseup','keydown','drop','dragenter','dragleave','dragover'];if(!b)a.push('focusin','focusout');if(window.opera){a.push('focus','blur');}else a.push('submit');for(var c=0;c<a.length;++c)JX.enableDispatch(h,a[c]);var i=[('onpagehide' in window)?'pagehide':'unload','resize','focus','blur'];for(var c=0;c<i.length;++c)JX.enableDispatch(window,i[c]);JX.__simulate=function(k,event){if(!b){var j={target:k,type:event};JX.__rawEventQueue(j);if(j.returnValue===false)return false;}};if(b){document.addEventListener('DOMContentLoaded',function(){JX.__rawEventQueue({type:'domready'});},true);}else{var g="if (this.readyState == 'complete') {"+"JX.__rawEventQueue({type: 'domready'});"+"}";document.write('<script'+' defer="defer"'+' src="javascript:void(0)"'+' onreadystatechange="'+g+'"'+'><\/sc'+'ript\>');}JX.onload=function(j){if(d){j();}else f.push(j);};})();
\ No newline at end of file
diff --git a/webroot/rsrc/js/javelin/javelin.dev.js b/webroot/rsrc/js/javelin/javelin.dev.js
new file mode 100644
index 0000000000..ad7e5cfdd1
--- /dev/null
+++ b/webroot/rsrc/js/javelin/javelin.dev.js
@@ -0,0 +1,2934 @@
+/**
+ * Javelin utility functions.
+ *
+ * @provides javelin-util
+ *
+ * @javelin-installs JX.$A
+ * @javelin-installs JX.$AX
+ * @javelin-installs JX.copy
+ * @javelin-installs JX.bind
+ * @javelin-installs JX.bag
+ * @javelin-installs JX.keys
+ * @javelin-installs JX.defer
+ * @javelin-installs JX.go
+ * @javelin-installs JX.log
+ *
+ * @javelin
+ */
+
+
+/**
+ * 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.
+ */
+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 an "instanceof Array" check so you may need to
+ * convert array-like objects (such as ##arguments## and Array instances from
+ * iframes) into real arrays with @{JX.$A()}.
+ *
+ * @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.
+ *
+ */
+JX.$AX = function(maybe_scalar) {
+ return (maybe_scalar instanceof Array) ? maybe_scalar : [maybe_scalar];
+};
+
+
+/**
+ * Copy properties from one object to another. Note: 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. 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'
+ * // }
+ *
+ * @param obj Destination object, which properties should be copied to.
+ * @param obj Source object, which properties should be copied from.
+ * @return obj Destination object.
+ */
+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; @{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 =
+ *
+ * @{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.
+ */
+JX.bind = function(context, func, more) {
+
+ if (__DEV__) {
+ if (typeof func != 'function') {
+ throw new Error(
+ 'JX.bind(context, <yuck>, ...): '+
+ 'Attempting to bind something that is not a function.');
+ }
+ }
+
+ var bound = JX.$A(arguments).slice(2);
+ 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
+ */
+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.
+ */
+JX.keys = function(obj) {
+ var r = [];
+ for (var k in obj) {
+ r.push(k);
+ }
+ return r;
+};
+
+
+/**
+ * Defer a function for later execution, similar to ##setTimeout()##. Returns
+ * an object with a ##stop()## method, which cancels the deferred call.
+ *
+ * var ref = JX.defer(yell, 3000); // Yell in 3 seconds.
+ * // ...
+ * ref.stop(); // Cancel the yell.
+ *
+ * @param function Function to invoke after the timeout.
+ * @param int? Timeout, in milliseconds. If this value is omitted, the
+ * function will be invoked once control returns to the browser
+ * event loop, as with ##setTimeout(func, 0)##.
+ * @return obj An object with a ##stop()## method, which cancels function
+ * execution.
+ */
+JX.defer = function(func, timeout) {
+ var t = setTimeout(func, timeout || 0);
+ return {stop : function() { clearTimeout(t); }}
+};
+
+
+/**
+ * Redirect the browser to another page by changing the window location.
+ *
+ * @param string Optional URI to redirect the browser to. If no URI is
+ * provided, the current page will be reloaded.
+ * @return void
+ */
+JX.go = function(uri) {
+
+ // Foil static analysis, etc. Strictly speaking, JX.go() doesn't really need
+ // to be in javelin-utils so we could do this properly at some point.
+ JX['Stratcom'] && JX['Stratcom'].invoke('go', null, {uri: uri});
+
+ (uri && (window.location = uri)) || window.location.reload(true);
+};
+
+
+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
+ */
+ 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(new Date().getTime());
+
+ 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);
+
+}
+/**
+ * @requires javelin-util
+ * @provides javelin-install
+ * @javelin-installs JX.install
+ * @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.
+ * - ##instance.__super__## Reference to the parent class constructor, if one
+ * exists. Allows use of ##this.__super__.apply(this, ...)## to call the
+ * superclass's constructor.
+ * - ##instance.__parent__## Reference to the parent class prototype, if one
+ * exists. Allows use of ##this.__parent__.someMethod.apply(this, ...)##
+ * to call the superclass's methods.
+ * - ##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__## //DEV ONLY!// Readable class name. You could
+ * plausibly use this when constructing error messages.
+ * - ##constructor.__events__## //DEV ONLY!// List of events supported by
+ * this class.
+ * - ##constructor.listen()## Listen to all instances of this class. See
+ * @{JX.Base}.
+ * - ##instance.listen()## Listen to one instance of this class. See
+ * @{JX.Base}.
+ * - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}.
+ *
+ *
+ * @param string Name of the class to install. It will appear in the JX
+ * "namespace" (e.g., JX.Pancake).
+ * @param map Map of properties, see method documentation.
+ * @return void
+ *
+ * @author epriestley
+ */
+JX.install = function(new_name, new_junk) {
+
+ if (typeof JX.install._nextObjectID == 'undefined') {
+ JX.install._nextObjectID = 0;
+ }
+
+ // If we've already installed this, something is up.
+ if (new_name in JX) {
+ if (__DEV__) {
+ throw new Error(
+ 'JX.install("' + new_name + '", ...): ' +
+ 'trying to reinstall something that has already been installed.');
+ }
+ return;
+ }
+
+ // Since we may end up loading things out of order (e.g., Dog extends Animal
+ // but we load Dog first) we need to keep a list of things that we've been
+ // asked to install but haven't yet been able to install around.
+ if (!JX.install._queue) {
+ JX.install._queue = [];
+ }
+ JX.install._queue.push([new_name, new_junk]);
+ do {
+ var junk;
+ var 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[ii][0];
+ JX.install._queue.splice(ii, 1);
+ --ii;
+
+ if (__DEV__) {
+ var valid = {
+ construct : 1,
+ statics : 1,
+ members : 1,
+ extend : 1,
+ initialize: 1,
+ properties : 1,
+ events : 1,
+ canCallAsFunction : 1
+ };
+ for (var k in junk) {
+ if (!(k in valid)) {
+ throw new Error(
+ 'JX.install("' + name + '", {"' + k + '": ...}): ' +
+ 'trying to install unknown property `' + k + '`.');
+ }
+ }
+ if (junk.constructor !== {}.constructor) {
+ throw new Error(
+ 'JX.install("' + 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).
+ JX[name] = (function(name, junk) {
+ var result = function() {
+ this.__id__ = '__obj__' + (++JX.install._nextObjectID);
+ this.__super__ = JX[junk.extend] || JX.bag;
+ this.__parent__ = JX[name].prototype;
+ if (JX[name].__prototyping__) {
+ return;
+ }
+ return (junk.construct || JX.bag).apply(this, arguments);
+ // TODO: Allow mixins to initialize here?
+ // TODO: Also, build mixins?
+ };
+
+ if (__DEV__) {
+ if (!junk.canCallAsFunction) {
+ var inner = result;
+ result = function() {
+ if (this === window || this === JX) {
+ throw new Error("<" + JX[name].__readable__ + ">: " +
+ "Tried to construct an instance " +
+ "without the 'new' operator. Either use " +
+ "'new' or set 'canCallAsFunction' where you " +
+ "install the class.");
+ }
+ return inner.apply(this, arguments);
+ };
+ }
+ }
+ return result;
+ })(name, junk);
+
+ // Copy in all the static methods and properties.
+ JX.copy(JX[name], junk.statics);
+
+ if (__DEV__) {
+ JX[name].__readable__ = 'JX.' + name;
+ }
+
+ JX[name].__prototyping__ = 0;
+
+ var proto;
+ if (junk.extend) {
+ JX[junk.extend].__prototyping__++;
+ proto = JX[name].prototype = new JX[junk.extend]();
+ JX[junk.extend].__prototyping__--;
+ } else {
+ proto = JX[name].prototype = {};
+ }
+
+ proto.__class__ = JX[name];
+
+ // Build getters and setters from the `prop' map.
+ for (var k in (junk.properties || {})) {
+ var base = k.charAt(0).toUpperCase()+k.substr(1);
+ var prop = '__auto__' + k;
+ proto[prop] = junk.properties[k];
+ proto['set' + base] = (function(prop) {
+ return function(v) {
+ this[prop] = v;
+ return this;
+ }
+ })(prop);
+
+ proto['get' + base] = (function(prop) {
+ return function() {
+ return this[prop];
+ }
+ })(prop);
+ }
+
+ if (__DEV__) {
+
+ // Check for aliasing in default values of members. If we don't do this,
+ // you can run into a problem like this:
+ //
+ // JX.install('List', { members : { stuff : [] }});
+ //
+ // var i_love = new JX.List();
+ // var i_hate = new JX.List();
+ //
+ // i_love.stuff.push('Psyduck'); // I love psyduck!
+ // JX.log(i_hate.stuff); // Show stuff I hate.
+ //
+ // This logs ["Psyduck"] because the push operation modifies
+ // JX.List.prototype.stuff, which is what both i_love.stuff and
+ // i_hate.stuff resolve to. To avoid this, set the default value to
+ // null (or any other scalar) and do "this.stuff = [];" in the
+ // constructor.
+
+ for (var member_name in junk.members) {
+ if (junk.extend && member_name[0] == '_') {
+ throw new Error(
+ 'JX.install("' + name + '", ...): ' +
+ 'installed member "' + member_name + '" must not be named with ' +
+ 'a leading underscore because it is in a subclass. Variables ' +
+ 'are analyzed and crushed one file at a time, and crushed ' +
+ 'member variables in subclasses alias crushed member variables ' +
+ 'in superclasses. Remove the underscore, refactor the class so ' +
+ 'it does not extend anything, or fix the minifier to be ' +
+ 'capable of safely crushing subclasses.');
+ }
+ var member_value = junk.members[member_name];
+ if (typeof member_value == 'object' && member_value !== null) {
+ throw new Error(
+ 'JX.install("' + 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.
+ JX.copy(proto, junk.members);
+
+
+ // 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.
+ if (junk.events && junk.events.length) {
+
+ var parent = JX[junk.extend] || {};
+
+ // If we're in dev, we build up a list of valid events (for this
+ // class or some parent class) and then check them whenever we try
+ // to listen or invoke.
+ if (__DEV__) {
+ var valid_events = parent.__events__ || {};
+ for (var ii = 0; ii < junk.events.length; ++ii) {
+ valid_events[junk.events[ii]] = true;
+ }
+ JX[name].__events__ = valid_events;
+ }
+
+ // Build the class name chain.
+ JX[name].__name__ = 'class:' + name;
+ var ancestry = parent.__path__ || [];
+ JX[name].__path__ = ancestry.concat([JX[name].__name__]);
+
+ proto.invoke = function(type) {
+ if (__DEV__) {
+ if (!(type in this.__class__.__events__)) {
+ throw new Error(
+ this.__class__.__readable__ + '.invoke("' + type + '", ...): ' +
+ 'invalid event type. Valid event types are: ' +
+ JX.keys(this.__class__.__events__).join(', ') + '.');
+ }
+ }
+ // Here and below, this nonstandard access notation is used to mask
+ // these callsites from the static analyzer. JX.Stratcom is always
+ // available by the time we hit these execution points.
+ return JX['Stratcom'].invoke(
+ 'obj:' + type,
+ this.__class__.__path__.concat([this.__id__]),
+ {args : JX.$A(arguments).slice(1)});
+ };
+
+ proto.listen = function(type, callback) {
+ if (__DEV__) {
+ if (!(type in this.__class__.__events__)) {
+ throw new Error(
+ this.__class__.__readable__ + '.listen("' + type + '", ...): ' +
+ 'invalid event type. Valid event types are: ' +
+ JX.keys(this.__class__.__events__).join(', ') + '.');
+ }
+ }
+ return JX['Stratcom'].listen(
+ 'obj:' + type,
+ this.__id__,
+ JX.bind(this, function(e) {
+ return callback.apply(this, e.getData().args);
+ }));
+ };
+
+ JX[name].listen = function(type, callback) {
+ if (__DEV__) {
+ if (!(type in this.__events__)) {
+ throw new Error(
+ this.__readable__ + '.listen("' + type + '", ...): ' +
+ 'invalid event type. Valid event types are: ' +
+ JX.keys(this.__events__).join(', ') + '.');
+ }
+ }
+ return JX['Stratcom'].listen(
+ 'obj:' + type,
+ this.__name__,
+ JX.bind(this, function(e) {
+ return callback.apply(this, e.getData().args);
+ }));
+ };
+ } else if (__DEV__) {
+ var error_message =
+ 'class does not define any events. Pass an "events" property to ' +
+ 'JX.install() to define events.';
+ JX[name].listen = JX[name].listen || function() {
+ throw new Error(
+ this.__readable__ + '.listen(...): ' +
+ error_message);
+ };
+ JX[name].invoke = JX[name].invoke || function() {
+ throw new Error(
+ this.__readable__ + '.invoke(...): ' +
+ error_message);
+ };
+ proto.listen = proto.listen || function() {
+ throw new Error(
+ this.__class__.__readable__ + '.listen(...): ' +
+ error_message);
+ };
+ proto.invoke = proto.invoke || function() {
+ throw new Error(
+ this.__class__.__readable__ + '.invoke(...): ' +
+ error_message);
+ };
+ }
+
+ // Finally, run the init function if it was provided.
+ (junk.initialize || JX.bag)();
+ }
+
+ // 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);
+}
+/**
+ * @requires javelin-install
+ * @provides javelin-event
+ * @javelin
+ */
+
+/**
+ * A generic event, routed by @{JX.Stratcom}. All events within Javelin are
+ * represented by a {@JX.Event}, regardless of whether they originate from
+ * a native DOM event (like a mouse click) or are custom application events.
+ *
+ * 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().
+ *
+ * @author epriestley
+ * @task stop Stopping Event Behaviors
+ * @task info Getting Event Information
+ */
+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;
+ }
+
+ var c = r.keyCode;
+ do {
+ c = JX.Event._keymap[c] || null;
+ } while (c && JX.Event._keymap[c])
+
+ return c;
+ },
+
+ /**
+ * 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('nearest: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;
+ }
+
+ },
+
+ statics : {
+ _keymap : {
+ 8 : 'delete',
+ 9 : 'tab',
+ 13 : 'return',
+ 27 : 'esc',
+ 37 : 'left',
+ 38 : 'up',
+ 39 : 'right',
+ 40 : 'down',
+ 63232 : 38,
+ 63233 : 40,
+ 62234 : 37,
+ 62235 : 39
+ }
+ },
+
+ properties : {
+
+ /**
+ * Native Javascript event which generated this @{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 : {}
+ },
+
+ /**
+ * @{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()+'>';
+ }
+ }
+ }
+});
+/**
+ * @requires javelin-install javelin-event javelin-util javelin-magical-init
+ * @provides javelin-stratcom
+ * @javelin
+ */
+
+/**
+ * Javelin strategic command, the master event delegation core. This class is
+ * a sort of hybrid between Arbiter and traditional event delegation, and
+ * serves to route event information to handlers in a general way.
+ *
+ * Each Javelin :JX.Event has a 'type', which may be a normal Javascript type
+ * (for instance, a click or a keypress) or an application-defined type. It
+ * also has a "path", based on the path in the DOM from the root node to the
+ * event target. Note that, while the type is required, the path may be empty
+ * (it often will be for application-defined events which do not originate
+ * from the DOM).
+ *
+ * The path is determined by walking down the tree to the event target and
+ * looking for nodes that have been tagged with metadata. These names are used
+ * to build the event path, and unnamed nodes are ignored. Each named node may
+ * also have data attached to it.
+ *
+ * Listeners specify one or more event types they are interested in handling,
+ * and, optionally, one or more paths. A listener will only receive events
+ * which occurred on paths it is listening to. See listen() for more details.
+ *
+ * @author epriestley
+ *
+ * @task invoke Invoking Events
+ * @task listen Listening to Events
+ * @task handle Responding to Events
+ * @task sigil Managing Sigils
+ * @task internal Internals
+ */
+JX.install('Stratcom', {
+ statics : {
+ ready : false,
+ _targets : {},
+ _handlers : [],
+ _need : {},
+ _matchName : /\bFN_([^ ]+)/,
+ _matchData : /\bFD_([^ ]+)_([^ ]+)/,
+ _auto : '*',
+ _data : {},
+ _execContext : [],
+ _typeMap : {focusin: 'focus', focusout: 'blur'},
+
+ /**
+ * Node metadata is stored in a series of blocks to prevent collisions
+ * between indexes that are generated on the server side (and potentially
+ * concurrently). Block 0 is for metadata on the initial page load, block 1
+ * is for metadata added at runtime with JX.Stratcom.siglize(), and blocks
+ * 2 and up are for metadata generated from other sources (e.g. JX.Request).
+ * Use allocateMetadataBlock() to reserve a block, and mergeData() to fill
+ * a block with data.
+ *
+ * When a JX.Request is sent, a block is allocated for it and any metadata
+ * it returns is filled into that block.
+ */
+ _dataBlock : 2,
+
+ /**
+ * Within each datablock, data is identified by a unique index. The data
+ * pointer on a node looks like this:
+ *
+ * FD_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.sigilize(), we
+ * need to allocate indexes on the client.
+ */
+ _dataIndex : 0,
+
+ /**
+ * Dispatch a simple event that does not have a corresponding native event
+ * object. It is unusual to call this directly. Generally, you will instead
+ * dispatch events from an object using the invoke() method present on all
+ * objects. See @{JX.Base.invoke()} for documentation.
+ *
+ * @param string Event type.
+ * @param list? Optionally, a path to attach to the event. This is
+ * rarely meaingful for simple events.
+ * @param object? Optionally, arbitrary data to send with the event.
+ * @return @{JX.Event} The event object which was dispatched to listeners.
+ * The main use of this is to test whether any
+ * listeners prevented the event.
+ * @task invoke
+ */
+ invoke : function(type, path, data) {
+ var proxy = new JX.Event()
+ .setType(type)
+ .setData(data || {})
+ .setPath(path || []);
+
+ return this._dispatchProxy(proxy);
+ },
+
+
+ /**
+ * Listen for events on given paths. Specify one or more event types, and
+ * zero or more paths to filter on. If you don't specify a path, you will
+ * receive all events of the given type:
+ *
+ * // Listen to all clicks.
+ * JX.Stratcom.listen('click', null, handler);
+ *
+ * This will notify you of all clicks anywhere in the document (unless
+ * they are intercepted and killed by a higher priority handler before they
+ * get to you).
+ *
+ * Often, you may be interested in only clicks on certain elements. You
+ * can specify the paths you're interested in to filter out events which
+ * you do not want to be notified of.
+ *
+ * // Listen to all clicks inside elements annotated "news-feed".
+ * JX.Stratcom.listen('click', 'news-feed', handler);
+ *
+ * By adding more elements to the path, you can create a finer-tuned
+ * filter:
+ *
+ * // Listen to only "like" clicks inside "news-feed".
+ * JX.Stratcom.listen('click', ['news-feed', 'like'], handler);
+ *
+ *
+ * TODO: Further explain these shenanigans.
+ *
+ * @param string|list<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.
+ * @author epriestley
+ * @task listen
+ */
+ listen : function(types, paths, func) {
+
+ if (__DEV__) {
+ if (arguments.length == 4) {
+ throw new Error(
+ 'JX.Stratcom.listen(...): '+
+ 'requires exactly 3 arguments. Did you mean JX.DOM.listen?');
+ }
+ if (arguments.length != 3) {
+ throw new Error(
+ 'JX.Stratcom.listen(...): '+
+ 'requires exactly 3 arguments.');
+ }
+ if (typeof func != 'function') {
+ throw new Error(
+ 'JX.Stratcom.listen(...): '+
+ 'callback is not a function.');
+ }
+ }
+
+ var ids = [];
+
+ types = JX.$AX(types);
+
+ if (!paths) {
+ paths = this._auto;
+ }
+ if (!(paths instanceof Array)) {
+ paths = [[paths]];
+ } else if (!(paths[0] instanceof Array)) {
+ paths = [paths];
+ }
+
+ // To listen to multiple event types on multiple paths, we just install
+ // the same listener a whole bunch of times: if we install for two
+ // event types on three paths, we'll end up with six references to the
+ // listener.
+ //
+ // TODO: we'll call your listener twice if you install on two paths where
+ // one path is a subset of another. The solution is "don't do that", but
+ // it would be nice to verify that the caller isn't doing so, in __DEV__.
+ for (var ii = 0; ii < types.length; ++ii) {
+ var type = types[ii];
+ if (('onpagehide' in window) && type == 'unload') {
+ // If we use "unload", we break the bfcache ("Back-Forward Cache") in
+ // Safari and Firefox. The BFCache makes using the back/forward
+ // buttons really fast since the pages can come out of magical
+ // fairyland instead of over the network, so use "pagehide" as a proxy
+ // for "unload" in these browsers.
+ type = 'pagehide';
+ }
+ if (!(type in this._targets)) {
+ this._targets[type] = {};
+ }
+ var type_target = this._targets[type];
+ for (var jj = 0; jj < paths.length; ++jj) {
+ var path = paths[jj];
+ var id = this._handlers.length;
+ this._handlers.push(func);
+ this._need[id] = path.length;
+ ids.push(id);
+ for (var kk = 0; kk < path.length; ++kk) {
+ if (__DEV__) {
+ if (path[kk] == 'tag:#document') {
+ throw new Error(
+ 'JX.Stratcom.listen(..., "tag:#document", ...): ' +
+ 'listen for document events as "tag:window", not ' +
+ '"tag:#document", in order to get consistent behavior ' +
+ 'across browsers.');
+ }
+ }
+ if (!type_target[path[kk]]) {
+ type_target[path[kk]] = [];
+ }
+ type_target[path[kk]].push(id);
+ }
+ }
+ }
+
+ return {
+ remove : function() {
+ for (var ii = 0; ii < ids.length; ii++) {
+ delete JX.Stratcom._handlers[ids[ii]];
+ }
+ }
+ };
+ },
+
+
+ /**
+ * Dispatch a native Javascript event through the Stratcom control flow.
+ * Generally, this is automatically called for you by the master dipatcher
+ * 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) {
+ // TODO: simplify this :P
+ var target;
+ try {
+ target = event.srcElement || event.target;
+ if (target === window || (!target || target.nodeName == '#document')) {
+ target = {nodeName: 'window'};
+ }
+ } catch (x) {
+ target = null;
+ }
+
+ var path = [];
+ var nodes = {};
+ var push = function(key, node) {
+ // we explicitly only store the first occurrence of each key
+ if (!(key in nodes)) {
+ nodes[key] = node;
+ path.push(key);
+ }
+ };
+
+ var cursor = target;
+ while (cursor) {
+ push('tag:' + cursor.nodeName.toLowerCase(), cursor);
+
+ var id = cursor.id;
+ if (id) {
+ push('id:' + id, cursor);
+ }
+
+ var source = cursor.className || '';
+ // className is an SVGAnimatedString for SVG elements, use baseVal
+ var token = ((source.baseVal || source).match(this._matchName) || [])[1];
+ if (token) {
+ push(token, cursor);
+ }
+
+ cursor = cursor.parentNode;
+ }
+
+ var etype = event.type;
+ if (etype in this._typeMap) {
+ etype = this._typeMap[etype];
+ }
+
+ var data = {};
+ for (var key in nodes) {
+ data[key] = this.getData(nodes[key]);
+ }
+
+ var proxy = new JX.Event()
+ .setRawEvent(event)
+ .setType(etype)
+ .setTarget(target)
+ .setData(data)
+ .setNodes(nodes)
+ .setPath(path.reverse());
+
+// JX.log('~> '+proxy.toString());
+
+ return this._dispatchProxy(proxy);
+ },
+
+
+ /**
+ * Dispatch a previously constructed proxy :JX.Event.
+ *
+ * @param :JX.Event Event to dispatch.
+ * @return :JX.Event Returns the event argument.
+ * @task internal
+ */
+ _dispatchProxy : function(proxy) {
+
+ var scope = this._targets[proxy.getType()];
+
+ if (!scope) {
+ return proxy;
+ }
+
+ var path = proxy.getPath();
+ var len = path.length;
+ var hits = {};
+ var matches;
+
+ for (var root = -1; root < len; ++root) {
+ if (root == -1) {
+ matches = scope[this._auto];
+ } else {
+ matches = scope[path[root]];
+ }
+ if (!matches) {
+ continue;
+ }
+ for (var ii = 0; ii < matches.length; ++ii) {
+ hits[matches[ii]] = (hits[matches[ii]] || 0) + 1;
+ }
+ }
+
+ var exec = [];
+
+ for (var k in hits) {
+ if (hits[k] == this._need[k]) {
+ var handler = this._handlers[k];
+ if (handler) {
+ exec.push(handler);
+ }
+ }
+ }
+
+ this._execContext.push({
+ handlers: exec,
+ event: proxy,
+ cursor: 0
+ });
+
+ this.pass();
+
+ this._execContext.pop();
+
+ return proxy;
+ },
+
+ /**
+ * Pass on an event, allowing other handlers to process it. The use case
+ * here is generally something like:
+ *
+ * if (JX.Stratcom.pass()) {
+ * // something else handled the event
+ * return;
+ * }
+ * // handle the event
+ * event.prevent();
+ *
+ * This allows you to install event handlers that operate at a lower
+ * effective priority, and provide a default behavior which is overridable
+ * by listeners.
+ *
+ * @return bool True if the event was stopped or prevented by another
+ * handler.
+ * @task handle
+ */
+ pass : function() {
+ var context = this._execContext[this._execContext.length - 1];
+ while (context.cursor < context.handlers.length) {
+ var cursor = context.cursor;
+ ++context.cursor;
+ (context.handlers[cursor] || JX.bag)(context.event);
+ if (context.event.getStopped()) {
+ break;
+ }
+ }
+ return context.event.getStopped() || context.event.getPrevented();
+ },
+
+
+ /**
+ * Retrieve the event (if any) which is currently being dispatched.
+ *
+ * @return :JX.Event|null Event which is currently being dispatched, or
+ * null if there is no active dispatch.
+ * @task handle
+ */
+ context : function() {
+ var len = this._execContext.length;
+ if (!len) {
+ return null;
+ }
+ return this._execContext[len - 1].event;
+ },
+
+
+ /**
+ * Merge metadata. You must call this (even if you have no metadata) to
+ * start the Stratcom queue.
+ *
+ * @param int The datablock to merge data into.
+ * @param dict Dictionary of metadata.
+ * @return void
+ * @task internal
+ */
+ mergeData : function(block, data) {
+ this._data[block] = data;
+ if (block == 0) {
+ JX.Stratcom.ready = true;
+ JX.__rawEventQueue({type: 'start-queue'});
+ }
+ },
+
+
+ /**
+ * Attach a sigil (and, optionally, metadata) to a node. Note that you can
+ * not overwrite, remove or replace a sigil.
+ *
+ * @param Node Node without any sigil.
+ * @param string Sigil to name the node with.
+ * @param object? Optional metadata object to attach to the node.
+ * @return void
+ * @task sigil
+ */
+ sigilize : function(node, sigil, data) {
+ if (__DEV__) {
+ if (node.className.match(this._matchName)) {
+ throw new Error(
+ 'JX.Stratcom.sigilize(<node>, ' + sigil + ', ...): ' +
+ 'node already has a sigil, sigils may not be overwritten.');
+ }
+ if (typeof data != 'undefined' &&
+ (data === null || typeof data != 'object')) {
+ throw new Error(
+ 'JX.Stratcom.sigilize(..., ..., <nonobject>): ' +
+ 'data to attach to node is not an object. You must use ' +
+ 'objects, not primitives, for metadata.');
+ }
+ }
+
+ if (data) {
+ JX.Stratcom._setData(node, data);
+ }
+
+ node.className = 'FN_' + sigil + ' ' + node.className;
+ },
+
+
+ /**
+ * 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 (!node.className) {
+ // Some nodes don't have a className, notably 'document'. We hit
+ // 'document' when following .parentNode chains, e.g. in
+ // JX.DOM.nearest(), so exit early if we don't have a className to avoid
+ // fataling on 'node.className.match' being undefined.
+ return false;
+ }
+ return (node.className.match(this._matchName) || [])[1] == sigil;
+ },
+
+
+ /**
+ * Retrieve a node's metadata.
+ *
+ * @param Node Node from which to retrieve data.
+ * @return object Data attached to the node, or an empty dictionary if
+ * the node has no data attached. In this case, the empty
+ * dictionary is set as the node's metadata -- i.e.,
+ * subsequent calls to getData() will retrieve the same
+ * object.
+ *
+ * @task sigil
+ */
+ getData : function(node) {
+ if (__DEV__) {
+ if (!node) {
+ throw new Error(
+ 'JX.Stratcom.getData(<empty>): ' +
+ 'you must provide a node to get associated data from.');
+ }
+ }
+
+ var matches = (node.className || '').match(this._matchData);
+ if (matches) {
+ var block = this._data[matches[1]];
+ var index = matches[2];
+ if (block && (index in block)) {
+ return block[index];
+ }
+ }
+
+ return JX.Stratcom._setData(node, {});
+ },
+
+ /**
+
+ * @task internal
+ */
+ allocateMetadataBlock : function() {
+ return this._dataBlock++;
+ },
+
+ /**
+ * Attach metadata to a node. This data can later be retrieved through
+ * @{JX.Stratcom.getData()}, or @{JX.Event.getData()}.
+ *
+ * @param Node Node which data should be attached to.
+ * @param object Data to attach.
+ * @return object Attached data.
+ *
+ * @task internal
+ */
+ _setData : function(node, data) {
+ if (!this._data[1]) { // data block 1 is reserved for javascript
+ this._data[1] = {};
+ }
+ this._data[1][this._dataIndex] = data;
+ node.className = 'FD_1_' + (this._dataIndex++) + ' ' + node.className;
+ return data;
+ }
+ }
+});
+/**
+ * @provides javelin-behavior
+ *
+ * @javelin-installs JX.behavior
+ * @javelin-installs JX.initBehaviors
+ *
+ * @javelin
+ */
+
+JX.behavior = function(name, control_function) {
+ if (__DEV__) {
+ if (name in JX.behavior._behaviors) {
+ throw new Error(
+ 'JX.behavior("'+name+'", ...): '+
+ 'behavior is already registered.');
+ }
+ if (!control_function) {
+ throw new Error(
+ 'JX.behavior("'+name+'", <nothing>): '+
+ 'initialization function is required.');
+ }
+ if (typeof control_function != 'function') {
+ throw new Error(
+ 'JX.behavior("'+name+'", <garbage>): '+
+ 'initialization function is not a function.');
+ }
+ }
+ JX.behavior._behaviors[name] = control_function;
+};
+
+
+JX.initBehaviors = function(map) {
+ for (var name in map) {
+ if (__DEV__) {
+ if (!(name in JX.behavior._behaviors)) {
+ throw new Error(
+ 'JX.initBehavior("'+name+'", ...): '+
+ 'behavior is not registered.');
+ }
+ }
+ var configs = map[name];
+ if (!configs.length) {
+ if (name in JX.behavior._initialized) {
+ continue;
+ } else {
+ configs = [null];
+ }
+ }
+ for (var ii = 0; ii < configs.length; ii++) {
+ JX.behavior._behaviors[name](configs[ii]);
+ }
+ JX.behavior._initialized[name] = true;
+ }
+};
+
+!function(JX) {
+ JX.behavior._behaviors = {};
+ JX.behavior._initialized = {};
+}(JX);
+/**
+ * @requires javelin-install
+ * javelin-stratcom
+ * javelin-util
+ * javelin-behavior
+ * @provides javelin-request
+ * @javelin
+ */
+
+/**
+ * Make basic AJAX XMLHTTPRequests.
+ */
+JX.install('Request', {
+ construct : function(uri, handler) {
+ this.setURI(uri);
+ if (handler) {
+ this.listen('done', handler);
+ }
+ },
+
+ events : ['send', 'done', 'error', 'finally'],
+
+ members : {
+
+ _xhrkey : null,
+ _transport : null,
+ _finished : false,
+ _block : null,
+
+ send : function() {
+ var xport = null;
+
+ try {
+ try {
+ xport = new XMLHttpRequest();
+ } catch (x) {
+ xport = new ActiveXObject("Msxml2.XMLHTTP");
+ }
+ } catch (x) {
+ xport = new ActiveXObject("Microsoft.XMLHTTP");
+ }
+
+ this._transport = xport;
+ this._xhrkey = JX.Request._xhr.length;
+ JX.Request._xhr.push(this);
+
+ xport.onreadystatechange = JX.bind(this, this._onreadystatechange);
+
+ var data = this.getData() || {};
+ data.__ajax__ = true;
+
+ this._block = JX.Stratcom.allocateMetadataBlock();
+ data.__metablock__ = this._block;
+
+ var q = (this.getDataSerializer() ||
+ JX.Request.defaultDataSerializer)(data);
+ var uri = this.getURI();
+ var method = this.getMethod().toUpperCase();
+
+ if (method == 'GET') {
+ uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q;
+ }
+
+ this.invoke('send', this);
+
+ if (this.getTimeout()) {
+ this._timer = JX.defer(
+ JX.bind(
+ this,
+ this._fail,
+ JX.Request.ERROR_TIMEOUT),
+ this.getTimeout());
+ }
+
+ xport.open(method, uri, true);
+
+ if (__DEV__) {
+ if (this.getFile()) {
+ if (method != 'POST') {
+ throw new Error(
+ 'JX.Request.send(): ' +
+ 'attempting to send a file over GET. You must use POST.');
+ }
+ if (this.getData()) {
+ throw new Error(
+ 'JX.Request.send(): ' +
+ 'attempting to send data and a file. You can not send both ' +
+ 'at once.');
+ }
+ }
+ }
+
+ if (method == 'POST') {
+ if (this.getFile()) {
+ xport.send(this.getFile());
+ } else {
+ xport.setRequestHeader(
+ 'Content-Type',
+ 'application/x-www-form-urlencoded');
+ xport.send(q);
+ }
+ } else {
+ xport.send(null);
+ }
+ },
+
+ abort : function() {
+ this._cleanup();
+ },
+
+ _onreadystatechange : function() {
+ var xport = this._transport;
+ try {
+ if (this._finished) {
+ return;
+ }
+ if (xport.readyState != 4) {
+ return;
+ }
+ if (xport.status < 200 || xport.status >= 300) {
+ this._fail();
+ return;
+ }
+
+ if (__DEV__) {
+ if (!xport.responseText.length) {
+ throw new Error(
+ 'JX.Request("'+this.getURI()+'", ...): '+
+ 'server returned an empty response.');
+ }
+ if (xport.responseText.indexOf('for (;;);') != 0) {
+ throw new Error(
+ 'JX.Request("'+this.getURI()+'", ...): '+
+ 'server returned an invalid response.');
+ }
+ }
+
+ var text = xport.responseText.substring('for (;;);'.length);
+ var response = eval('('+text+')');
+ } catch (exception) {
+
+ if (__DEV__) {
+ JX.log(
+ 'JX.Request("'+this.getURI()+'", ...): '+
+ 'caught exception processing response: '+exception);
+ }
+ this._fail();
+ return;
+ }
+
+ try {
+ if (response.error) {
+ this._fail(response.error);
+ } else {
+ JX.Stratcom.mergeData(
+ this._block,
+ response.javelin_metadata || {});
+ this._done(response);
+ JX.initBehaviors(response.javelin_behaviors || {});
+ }
+ } catch (exception) {
+ // In Firefox+Firebug, at least, something eats these. :/
+ JX.defer(function() {
+ throw exception;
+ });
+ }
+ },
+
+ _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]))();
+ }
+ }
+
+ this.invoke('done', this.getRaw() ? response : response.payload, this);
+ this.invoke('finally');
+ },
+
+ _cleanup : function() {
+ this._finished = true;
+ delete JX.Request._xhr[this._xhrkey];
+ this._timer && this._timer.stop();
+ this._transport.abort();
+ }
+
+ },
+
+ statics : {
+ _xhr : [],
+ shutdown : function() {
+ for (var ii = 0; ii < JX.Request._xhr.length; ii++) {
+ try {
+ JX.Request._xhr[ii] && JX.Request._xhr[ii].abort();
+ } catch (x) {
+ // Ignore.
+ }
+ }
+ JX.Request._xhr = [];
+ },
+ ERROR_TIMEOUT : -9000,
+ defaultDataSerializer : function(data) {
+ var uri = [];
+ for (var k in data) {
+ uri.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
+ }
+ return uri.join('&');
+ }
+ },
+
+ properties : {
+ URI : null,
+ data : 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',
+ file : 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
+ },
+
+ initialize : function() {
+ JX.Stratcom.listen('unload', 'tag:window', JX.Request.shutdown);
+ }
+
+});
+
+/**
+ * @requires javelin-install javelin-event
+ * @provides javelin-vector
+ * @javelin
+ */
+
+/**
+ * Query and update positions and dimensions of nodes (and other things)
+ * within a document. 'V' stands for 'Vector'. Each vector has two elements,
+ * 'x' and 'y', which usually represent width/height (a "dimension vector") or
+ * left/top (a "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, @{JX.$V} 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 = JX.$V(e);
+ * var d = JX.$V.getDim(e.getTarget());
+ *
+ * alert('You clicked at <'+p.x+','+p.y'>; the element you clicked '+
+ * 'is '+d.x+' pixels wide and '+d.y+' pixels 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 t = e.getTarget();
+ * JX.$V(t).add(10, 10).setDim(t);
+ * });
+ *
+ * Additionally, vectors can be used to query document and viewport information:
+ *
+ * var v = JX.$V.getViewport(); // Viewport (window) width and height.
+ * var d = JX.$V.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'+ percent of the document.');
+ *
+ * @author epriestley
+ *
+ * @task query Querying Positions and Dimensions
+ * @task update Changing Positions and Dimensions
+ * @task manip Manipulating Vectors
+ *
+ */
+JX.install('$V', {
+
+ /**
+ * Construct a vector, either from explicit coordinates or from a node
+ * or event. You can pass two Numbers to construct an explicit vector:
+ *
+ * var v = JX.$V(35, 42);
+ *
+ * Otherwise, you can pass a @{JX.Event} or a Node to implicitly construct a
+ * vector:
+ *
+ * var u = JX.$V(some_event);
+ * var v = JX.$V(some_node);
+ *
+ * These are just like calling getPos() on the @{JX.Event} or Node.
+ *
+ * For convenience, @{JX.$V()} constructs a new vector even without the 'new'
+ * keyword. That is, these are equivalent:
+ *
+ * var q = new JX.$V(x, y);
+ * var r = JX.$V(x, y);
+ *
+ * Methods like getScroll(), getViewport() and 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 @{JX.Event}, or a Node.
+ * @param Number? If providing an 'x' component, the 'y' component of the
+ * vector.
+ * @return @{JX.$V} Specified vector.
+ * @task query
+ */
+ construct : function(x, y) {
+ if (this == JX || this == window) {
+ return new JX.$V(x, y);
+ }
+ if (typeof y == 'undefined') {
+ return JX.$V.getPos(x);
+ }
+
+ this.x = parseFloat(x);
+ this.y = parseFloat(y);
+ },
+ canCallAsFunction : true,
+ 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 @{JX.$V} New vector, with summed components.
+ * @task manip
+ */
+ add : function(x, y) {
+ if (x instanceof JX.$V) {
+ return this.add(x.x, x.y);
+ }
+ return JX.$V(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 @{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 getDim(), used to determine an element's dimensions.
+ *
+ * @param Node|@{JX.Event} Node or event to determine the position of.
+ * @return @{JX.$V} 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.$V._viewport;
+ return JX.$V(
+ node.pageX || (node.clientX + c.scrollLeft),
+ node.pageY || (node.clientY + c.scrollTop));
+ }
+
+ var x = node.offsetLeft;
+ var y = node.offsetTop;
+ while (node.offsetParent && (node.offsetParent != document.body)) {
+ node = node.offsetParent;
+ x += node.offsetLeft;
+ y += node.offsetTop;
+ }
+
+ return JX.$V(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 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 JX.$V(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 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 $V._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 JX.$V(b.scrollLeft || e.scrollLeft, 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 getScroll(), used to determine the position of the viewport, and
+ * getDocument(), used to determine the size of the entire document.
+ *
+ * @return @{JX.$V} New vector with the viewport dimensions.
+ * @task query
+ */
+ getViewport : function() {
+ var c = JX.$V._viewport;
+ var w = window;
+
+ return JX.$V(
+ w.innerWidth || c.clientWidth || 0,
+ w.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 @{JX.$V} New vector with the document dimensions.
+ * @task query
+ */
+ getDocument : function() {
+ var c = JX.$V._viewport;
+ return JX.$V(c.scrollWidth || 0, c.scrollHeight || 0);
+ }
+ },
+
+ /**
+ * On initialization, the browser-dependent viewport root is determined and
+ * stored.
+ *
+ * In ##__DEV__##, @{JX.$V} installs a toString() method so vectors print in a
+ * debuggable way:
+ *
+ * <23, 92>
+ *
+ * @return void
+ */
+ initialize : function() {
+ var c = ((c = document) && (c = c.documentElement)) ||
+ ((c = document) && (c = c.body))
+ JX.$V._viewport = c;
+
+ if (__DEV__) {
+ JX.$V.prototype.toString = function() {
+ return '<'+this.x+', '+this.y+'>';
+ }
+ }
+
+ }
+});
+/**
+ * @requires javelin-install javelin-util javelin-vector javelin-stratcom
+ * @provides javelin-dom
+ *
+ * @javelin-installs JX.$
+ * @javelin-installs JX.$N
+ *
+ * @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 ##JX.$.NotFound##.
+ * 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.
+ */
+JX.$ = function(id) {
+
+ if (__DEV__) {
+ if (!id) {
+ throw new Error('Empty ID passed to JX.$()!');
+ }
+ }
+
+ var node = document.getElementById(id);
+ if (!node || (node.id != id)) {
+ if (__DEV__) {
+ if (node && (node.id != id)) {
+ throw new Error(
+ '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.');
+ }
+ }
+ throw JX.$.NotFound;
+ }
+
+ return node;
+};
+
+JX.$.NotFound = {};
+if (__DEV__) {
+ // If we're in dev, upgrade this object into an Error so that it will
+ // print something useful if it escapes the stack after being thrown.
+ JX.$.NotFound = new Error(
+ 'JX.$() or JX.DOM.find() call matched no nodes.');
+}
+
+
+/**
+ * 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.
+ *
+ * @task build String into HTML
+ * @task nodes HTML into Nodes
+ */
+JX.install('HTML', {
+
+ /**
+ * Build a new HTML object from a trustworthy string.
+ *
+ * @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}.
+ */
+ construct : function(str) {
+ if (this == JX || this == window) {
+ return new JX.HTML(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)) {
+ throw new Error(
+ '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)) {
+ throw new Error(
+ '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)) {
+ throw new Error(
+ '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;
+ },
+ canCallAsFunction : true,
+ 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;
+ }
+ }
+});
+
+
+/**
+ * Create a new DOM node with attributes and content.
+ *
+ * var link = JX.$N('a');
+ *
+ * This creates a new, empty anchor tag without any attributes. The equivalent
+ * markup would be:
+ *
+ * LANG=HTML
+ * <a />
+ *
+ * You can also specify attributes by passing a dictionary:
+ *
+ * JX.$N('a', {name: 'anchor'});
+ *
+ * This is equivalent to:
+ *
+ * LANG=HTML
+ * <a name="anchor" />
+ *
+ * Additionally, you can specify content:
+ *
+ * JX.$N(
+ * 'a',
+ * {href: 'http://www.javelinjs.com'},
+ * 'Visit the Javelin Homepage');
+ *
+ * This is equivalent to:
+ *
+ * LANG=HTML
+ * <a href="http://www.javelinjs.com">Visit the Javelin Homepage</a>
+ *
+ * If you only want to specify content, you can omit the attribute parameter.
+ * That is, these calls are equivalent:
+ *
+ * JX.$N('div', {}, 'Lorem ipsum...'); // No attributes.
+ * JX.$N('div', 'Lorem ipsum...') // Same as above.
+ *
+ * Both are equivalent to:
+ *
+ * LANG=HTML
+ * <div>Lorem ipsum...</div>
+ *
+ * Note that the content is treated as plain text, not HTML. This means it is
+ * safe to use untrusted strings:
+ *
+ * JX.$N('div', '<script src="evil.com" />');
+ *
+ * This is equivalent to:
+ *
+ * LANG=HTML
+ * <div>&lt;script src="evil.com" /&gt;</div>
+ *
+ * That is, the content will be properly escaped and will not create a
+ * vulnerability. If you want to set HTML content, you can use @{JX.HTML}:
+ *
+ * JX.$N('div', JX.HTML(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 "metadata" 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.
+ */
+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) {
+ throw new Error(
+ '$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.sigilize(node, attr.sigil, attr.meta);
+ delete attr.sigil;
+ delete attr.meta;
+ }
+
+ if (__DEV__) {
+ if (('metadata' in attr) || ('data' in attr)) {
+ throw new Error(
+ '$N(' + tag + ', ...): ' +
+ 'use the key "meta" to specify metadata, not "data" or "metadata".');
+ }
+ if (attr.meta) {
+ throw new Error(
+ '$N(' + tag + ', ...): ' +
+ 'if you specify "meta" metadata, you must also specify a "sigil".');
+ }
+ }
+
+ // prevent sigil from being wiped by blind copying the className
+ if (attr.className) {
+ JX.DOM.alterClass(node, attr.className, true);
+ delete attr.className;
+ }
+
+ 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 test Testing DOM Properties
+ * @task convenience Convenience Methods
+ * @task query Finding Nodes in the DOM
+ * @task view Changing View State
+ */
+JX.install('DOM', {
+ statics : {
+ _autoid : 0,
+ _metrics : {},
+
+ /**
+ * @task content
+ */
+ setContent : function(node, content) {
+ if (__DEV__) {
+ if (!JX.DOM.isNode(node)) {
+ throw new Error(
+ 'JX.DOM.setContent(<yuck>, ...): '+
+ 'first argument must be a DOM node.');
+ }
+ }
+
+ while (node.firstChild) {
+ JX.DOM.remove(node.firstChild);
+ }
+ JX.DOM.appendContent(node, content);
+ },
+
+
+ /**
+ * @task content
+ */
+ prependContent : function(node, content) {
+ if (__DEV__) {
+ if (!JX.DOM.isNode(node)) {
+ throw new Error(
+ 'JX.DOM.prependContent(<junk>, ...): '+
+ 'first argument must be a DOM node.');
+ }
+ }
+
+ this._insertContent(node, content, this._mechanismPrepend);
+ },
+
+
+ /**
+ * @task content
+ */
+ appendContent : function(node, content) {
+ if (__DEV__) {
+ if (!JX.DOM.isNode(node)) {
+ throw new Error(
+ 'JX.DOM.appendContent(<bleh>, ...): '+
+ 'first argument must be a DOM node.');
+ }
+ }
+
+ this._insertContent(node, content, this._mechanismAppend);
+ },
+
+
+ /**
+ * @task content
+ */
+ _mechanismPrepend : function(node, content) {
+ node.insertBefore(content, node.firstChild);
+ },
+
+
+ /**
+ * @task content
+ */
+ _mechanismAppend : function(node, content) {
+ node.appendChild(content);
+ },
+
+
+ /**
+ * @task content
+ */
+ _insertContent : function(parent, content, mechanism) {
+ if (content === null || typeof content == 'undefined') {
+ return;
+ }
+ if (content instanceof JX.HTML) {
+ content = content.getFragment();
+ }
+ if (content instanceof Array) {
+ for (var ii = 0; ii < content.length; ii++) {
+ var child = (typeof content[ii] == 'string')
+ ? document.createTextNode(content[ii])
+ : content[ii];
+ mechanism(parent, child);
+ }
+ } else if (content.nodeType) {
+ mechanism(parent, content);
+ } else {
+ mechanism(parent, document.createTextNode(content));
+ }
+ },
+
+
+ /**
+ * @task nodes
+ */
+ remove : function(node) {
+ node.parentNode && JX.DOM.replace(node, null);
+ return node;
+ },
+
+
+ /**
+ * @task nodes
+ */
+ replace : function(node, replacement) {
+ if (__DEV__) {
+ if (!node.parentNode) {
+ throw new Error(
+ '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;
+ node.parentNode.removeChild(node);
+ this._insertContent(parent, replacement, mechanism);
+
+ return node;
+ },
+
+
+ /**
+ * Retrieve the nearest parent node matching the desired sigil.
+ * @param Node The child element to search from
+ * @return The matching parent or null if no parent could be found
+ * @author jgabbard
+ */
+ nearest : function(node, sigil) {
+ while (node && !JX.Stratcom.hasSigil(node, sigil)) {
+ node = node.parentNode;
+ }
+ return node;
+ },
+
+
+ serialize : function(form) {
+ var elements = form.getElementsByTagName('*');
+ var data = {};
+ for (var ii = 0; ii < elements.length; ++ii) {
+ if (!elements[ii].name) {
+ 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} ||
+ tag in {TEXTAREA: 1, SELECT: 1}) {
+ data[elements[ii].name] = elements[ii].value;
+ }
+ }
+ return data;
+ },
+
+
+ /**
+ * Test if an object is a valid Node.
+ *
+ * @task test
+ * @param wild Something which might be a Node.
+ * @return bool True if the parameter is a DOM node.
+ */
+ 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']);
+ *
+ * @task test
+ * @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.
+ */
+ 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.
+ * @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) {
+ return JX.Stratcom.listen(
+ type,
+ ['id:'+JX.DOM.uniqID(node)].concat(JX.$AX(path || [])),
+ callback);
+ },
+
+ uniqID : function(node) {
+ if (!node.id) {
+ node.id = 'autoid_'+(++JX.DOM._autoid);
+ }
+ return node.id;
+ },
+
+ alterClass : function(node, className, add) {
+ var has = ((' '+node.className+' ').indexOf(' '+className+' ') > -1);
+ if (add && !has) {
+ node.className += ' '+className;
+ } else if (has && !add) {
+ node.className = node.className.replace(
+ new RegExp('(^|\\s)' + className + '(?:\\s|$)', 'g'), ' ');
+ }
+ },
+
+ htmlize : function(str) {
+ return (''+str)
+ .replace(/&/g, '&amp;')
+ .replace(/"/g, '&quot;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ },
+
+
+ /**
+ * Show one or more elements, by removing their "display" style. This
+ * assumes you have hidden them with 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]) {
+ throw new Error(
+ '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 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]) {
+ throw new Error(
+ '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.HTML(JX.DOM.htmlize(node.value).replace(/\n/g, '<br />')));
+ var metrics = JX.$V.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)) {
+ throw new Error(
+ '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. It will
+ * throw JX.$.NotFound if it matches no results.
+ *
+ * @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)) {
+ throw new Error(
+ '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) {
+ throw new Error(
+ 'JX.DOM.find(<node>, "'+tagname+'", "'+sigil+'"): '+
+ 'matched more than one node.');
+ }
+ }
+
+ if (!result.length) {
+ throw JX.$.NotFound;
+ }
+
+ 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);
+ }
+ }
+});
+
+/**
+ * Simple JSON serializer.
+ *
+ * @requires javelin-install javelin-util
+ * @provides javelin-json
+ * @javelin
+ */
+
+JX.install('JSON', {
+ statics : {
+ serialize : function(obj) {
+ if (__DEV__) {
+ try {
+ return JX.JSON._val(obj);
+ } catch (x) {
+ JX.log(
+ 'JX.JSON.serialize(...): '+
+ 'caught exception while serializing object. ('+x+')');
+ }
+ } else {
+ return JX.JSON._val(obj);
+ }
+ },
+ _val : function(val) {
+ var out = [];
+ if (val === null) {
+ return 'null';
+ } else if (val.push && val.pop) {
+ for (var ii = 0; ii < val.length; ii++) {
+ if (typeof val[ii] != 'undefined') {
+ out.push(JX.JSON._val(val[ii]));
+ }
+ }
+ return '['+out.join(',')+']';
+ } else if (val === true) {
+ return 'true';
+ } else if (val === false) {
+ return 'false';
+ } else if (typeof val == 'string') {
+ return JX.JSON._esc(val);
+ } else if (typeof val == 'number') {
+ return val;
+ } else {
+ for (var k in val) {
+ out.push(JX.JSON._esc(k)+':'+JX.JSON._val(val[k]));
+ }
+ return '{'+out.join(',')+'}';
+ }
+ },
+ _esc : function(str) {
+ return '"'+str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')+'"';
+ }
+ }
+});
diff --git a/webroot/rsrc/js/javelin/javelin.min.js b/webroot/rsrc/js/javelin/javelin.min.js
new file mode 100644
index 0000000000..60bb82e21f
--- /dev/null
+++ b/webroot/rsrc/js/javelin/javelin.min.js
@@ -0,0 +1 @@
+JX.$A=function(b){var c=[];for(var a=0;a<b.length;a++)c.push(b[a]);return c;};JX.$AX=function(a){return (a instanceof Array)?a:[a];};JX.copy=function(a,b){for(var c in b)a[c]=b[c];return a;};JX.bind=function(b,c,d){var a=JX.$A(arguments).slice(2);return function(){return c.apply(b||window,a.concat(JX.$A(arguments)));};};JX.bag=function(){};JX.keys=function(b){var c=[];for(var a in b)c.push(a);return c;};JX.defer=function(a,c){var b=setTimeout(a,c||0);return {stop:function(){clearTimeout(b);}};};JX.go=function(a){JX.Stratcom&&JX.Stratcom.invoke('go',null,{uri:a});(a&&(window.location=a))||window.location.reload(true);};JX.install=function(h,g){if(typeof JX.install._a=='undefined')JX.install._a=0;if(h in JX)return;if(!JX.install._b)JX.install._b=[];JX.install._b.push([h,g]);do{var d;var f=null;for(var c=0;c<JX.install._b.length;++c){d=JX.install._b[c][1];if(d.extend&&!JX[d.extend])continue;f=JX.install._b[c][0];JX.install._b.splice(c,1);--c;JX[f]=(function(m,l){var n=function(){this.__id__='__obj__'+(++JX.install._a);this.__super__=JX[l.extend]||JX.bag;this.__parent__=JX[m].prototype;if(JX[m].__prototyping__)return;return (l.construct||JX.bag).apply(this,arguments);};return n;})(f,d);JX.copy(JX[f],d.statics);JX[f].__prototyping__=0;var k;if(d.extend){JX[d.extend].__prototyping__++;k=JX[f].prototype=new JX[d.extend]();JX[d.extend].__prototyping__--;}else k=JX[f].prototype={};k.__class__=JX[f];for(var e in (d.properties||{})){var b=e.charAt(0).toUpperCase()+e.substr(1);var j='__auto__'+e;k[j]=d.properties[e];k['set'+b]=(function(l){return function(m){this[l]=m;return this;};})(j);k['get'+b]=(function(l){return function(){return this[l];};})(j);}JX.copy(k,d.members);if(d.events&&d.events.length){var i=JX[d.extend]||{};JX[f].__name__='class:'+f;var a=i.__path__||[];JX[f].__path__=a.concat([JX[f].__name__]);k.invoke=function(l){return JX.Stratcom.invoke('obj:'+l,this.__class__.__path__.concat([this.__id__]),{args:JX.$A(arguments).slice(1)});};k.listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__id__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};JX[f].listen=function(m,l){return JX.Stratcom.listen('obj:'+m,this.__name__,JX.bind(this,function(n){return l.apply(this,n.getData().args);}));};}(d.initialize||JX.bag)();}}while(f);};JX.install('Event',{members:{stop:function(){var a=this.getRawEvent();if(a){a.cancelBubble=true;a.stopPropagation&&a.stopPropagation();}this.setStopped(true);return this;},prevent:function(){var a=this.getRawEvent();if(a){a.returnValue=false;a.preventDefault&&a.preventDefault();}this.setPrevented(true);return this;},kill:function(){this.prevent();this.stop();return this;},getSpecialKey:function(){var b=this.getRawEvent();if(!b||b.shiftKey)return null;var a=b.keyCode;do{a=JX.Event._c[a]||null;}while(a&&JX.Event._c[a]);return a;},getNode:function(a){return this.getNodes()[a]||null;}},statics:{_c:{8:'delete',9:'tab',13:'return',27:'esc',37:'left',38:'up',39:'right',40:'down',63232:38,63233:40,62234:37,62235:39}},properties:{rawEvent:null,type:null,target:null,data:null,path:[],stopped:false,prevented:false,nodes:{}},initialize:function(){}});JX.install('Stratcom',{statics:{ready:false,_d:{},_e:[],_f:{},_g:/\bFN_([^ ]+)/,_h:/\bFD_([^ ]+)_([^ ]+)/,_i:'*',_j:{},_k:[],_l:{focusin:'focus',focusout:'blur'},_m:2,_n:0,invoke:function(d,b,a){var c=new JX.Event().setType(d).setData(a||{}).setPath(b||[]);return this._o(c);},listen:function(k,h,a){var c=[];k=JX.$AX(k);if(!h)h=this._i;if(!(h instanceof Array)){h=[[h]];}else if(!(h[0] instanceof Array))h=[h];for(var d=0;d<k.length;++d){var i=k[d];if(('onpagehide' in window)&&i=='unload')i='pagehide';if(!(i in this._d))this._d[i]={};var j=this._d[i];for(var e=0;e<h.length;++e){var g=h[e];var b=this._e.length;this._e.push(a);this._f[b]=g.length;c.push(b);for(var f=0;f<g.length;++f){if(!j[g[f]])j[g[f]]=[];j[g[f]].push(b);}}}return {remove:function(){for(var l=0;l<c.length;l++)delete JX.Stratcom._e[c[l]];}};},dispatch:function(event){var k;try{k=event.srcElement||event.target;if(k===window||(!k||k.nodeName=='#document'))k={nodeName:'window'};}catch(m){k=null;}var g=[];var f={};var i=function(n,o){if(!(n in f)){f[n]=o;g.push(n);}};var a=k;while(a){i('tag:'+a.nodeName.toLowerCase(),a);var d=a.id;if(d)i('id:'+d,a);var j=a.className||'';var l=((j.baseVal||j).match(this._g)||[])[1];if(l)i(l,a);a=a.parentNode;}var c=event.type;if(c in this._l)c=this._l[c];var b={};for(var e in f)b[e]=this.getData(f[e]);var h=new JX.Event().setRawEvent(event).setType(c).setTarget(k).setData(b).setNodes(f).setPath(g.reverse());return this._o(h);},_o:function(i){var k=this._d[i.getType()];if(!k)return i;var h=i.getPath();var f=h.length;var c={};var g;for(var j=-1;j<f;++j){if(j==-1){g=k[this._i];}else g=k[h[j]];if(!g)continue;for(var d=0;d<g.length;++d)c[g[d]]=(c[g[d]]||0)+1;}var a=[];for(var e in c)if(c[e]==this._f[e]){var b=this._e[e];if(b)a.push(b);}this._k.push({handlers:a,event:i,cursor:0});this.pass();this._k.pop();return i;},pass:function(){var a=this._k[this._k.length-1];while(a.cursor<a.handlers.length){var b=a.cursor;++a.cursor;(a.handlers[b]||JX.bag)(a.event);if(a.event.getStopped())break;}return a.event.getStopped()||a.event.getPrevented();},context:function(){var a=this._k.length;if(!a)return null;return this._k[a-1].event;},mergeData:function(a,b){this._j[a]=b;if(a==0){JX.Stratcom.ready=true;JX.__rawEventQueue({type:'start-queue'});}},sigilize:function(b,c,a){if(a)JX.Stratcom._p(b,a);b.className='FN_'+c+' '+b.className;},hasSigil:function(a,b){if(!a.className)return false;return (a.className.match(this._g)||[])[1]==b;},getData:function(d){var c=(d.className||'').match(this._h);if(c){var a=this._j[c[1]];var b=c[2];if(a&&(b in a))return a[b];}return JX.Stratcom._p(d,{});},allocateMetadataBlock:function(){return this._m++;},_p:function(b,a){if(!this._j[1])this._j[1]={};this._j[1][this._n]=a;b.className='FD_1_'+(this._n++)+' '+b.className;return a;}}});JX.behavior=function(b,a){JX.behavior._q[b]=a;};JX.initBehaviors=function(c){for(var d in c){var a=c[d];if(!a.length)if(d in JX.behavior._r){continue;}else a=[null];for(var b=0;b<a.length;b++)JX.behavior._q[d](a[b]);JX.behavior._r[d]=true;}};!function(a){a.behavior._q={};a.behavior._r={};}(JX);JX.install('Request',{construct:function(b,a){this.setURI(b);if(a)this.listen('done',a);},events:['send','done','error','finally'],members:{_s:null,_t:null,_u:false,_v:null,send:function(){var f=null;try{try{f=new XMLHttpRequest();}catch(e){f=new ActiveXObject("Msxml2.XMLHTTP");}}catch(e){f=new ActiveXObject("Microsoft.XMLHTTP");}this._t=f;this._s=JX.Request._w.length;JX.Request._w.push(this);f.onreadystatechange=JX.bind(this,this._x);var a=this.getData()||{};a.__ajax__=true;this._v=JX.Stratcom.allocateMetadataBlock();a.__metablock__=this._v;var c=(this.getDataSerializer()||JX.Request.defaultDataSerializer)(a);var d=this.getURI();var b=this.getMethod().toUpperCase();if(b=='GET')d+=((d.indexOf('?')===-1)?'?':'&')+c;this.invoke('send',this);if(this.getTimeout())this._y=JX.defer(JX.bind(this,this._z,JX.Request.ERROR_TIMEOUT),this.getTimeout());f.open(b,d,true);if(b=='POST'){if(this.getFile()){f.send(this.getFile());}else{f.setRequestHeader('Content-Type','application/x-www-form-urlencoded');f.send(c);}}else f.send(null);},abort:function(){this._za();},_x:function(){var xport=this._t;try{if(this._u)return;if(xport.readyState!=4)return;if(xport.status<200||xport.status>=300){this._z();return;}var text=xport.responseText.substring('for (;;);'.length);var response=eval('('+text+')');}catch(exception){this._z();return;}try{if(response.error){this._z(response.error);}else{JX.Stratcom.mergeData(this._v,response.javelin_metadata||{});this._zb(response);JX.initBehaviors(response.javelin_behaviors||{});}}catch(exception){JX.defer(function(){throw exception;});}},_z:function(a){this._za();this.invoke('error',a,this);this.invoke('finally');},_zb:function(b){this._za();if(b.onload)for(var a=0;a<b.onload.length;a++)(new Function(b.onload[a]))();this.invoke('done',this.getRaw()?b:b.payload,this);this.invoke('finally');},_za:function(){this._u=true;delete JX.Request._w[this._s];this._y&&this._y.stop();this._t.abort();}},statics:{_w:[],shutdown:function(){for(var a=0;a<JX.Request._w.length;a++)try{JX.Request._w[a]&&JX.Request._w[a].abort();}catch(b){}JX.Request._w=[];},ERROR_TIMEOUT:-9000,defaultDataSerializer:function(a){var c=[];for(var b in a)c.push(encodeURIComponent(b)+'='+encodeURIComponent(a[b]));return c.join('&');}},properties:{URI:null,data:null,dataSerializer:null,method:'POST',file:null,raw:false,timeout:null},initialize:function(){JX.Stratcom.listen('unload','tag:window',JX.Request.shutdown);}});JX.install('$V',{construct:function(a,b){if(this==JX||this==window)return new JX.$V(a,b);if(typeof b=='undefined')return JX.$V.getPos(a);this.x=parseFloat(a);this.y=parseFloat(b);},canCallAsFunction:true,members:{x:null,y:null,setPos:function(a){a.style.left=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.top=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},setDim:function(a){a.style.width=(this.x===null)?'':(parseInt(this.x,10)+'px');a.style.height=(this.y===null)?'':(parseInt(this.y,10)+'px');return this;},add:function(a,b){if(a instanceof JX.$V)return this.add(a.x,a.y);return JX.$V(this.x+parseFloat(a),this.y+parseFloat(b));}},statics:{_zc:null,getPos:function(b){JX.Event&&(b instanceof JX.Event)&&(b=b.getRawEvent());if(('pageX' in b)||('clientX' in b)){var a=JX.$V._zc;return JX.$V(b.pageX||(b.clientX+a.scrollLeft),b.pageY||(b.clientY+a.scrollTop));}var c=b.offsetLeft;var d=b.offsetTop;while(b.offsetParent&&(b.offsetParent!=document.body)){b=b.offsetParent;c+=b.offsetLeft;d+=b.offsetTop;}return JX.$V(c,d);},getDim:function(a){return JX.$V(a.offsetWidth,a.offsetHeight);},getScroll:function(){var a=document.body;var b=document.documentElement;return JX.$V(a.scrollLeft||b.scrollLeft,a.scrollTop||b.scrollTop);},getViewport:function(){var a=JX.$V._zc;var b=window;return JX.$V(b.innerWidth||a.clientWidth||0,b.innerHeight||a.clientHeight||0);},getDocument:function(){var a=JX.$V._zc;return JX.$V(a.scrollWidth||0,a.scrollHeight||0);}},initialize:function(){var a=((a=document)&&(a=a.documentElement))||((a=document)&&(a=a.body));JX.$V._zc=a;}});JX.$=function(a){var b=document.getElementById(a);if(!b||(b.id!=a))throw JX.$.NotFound;return b;};JX.$.NotFound={};JX.install('HTML',{construct:function(a){if(this==JX||this==window)return new JX.HTML(a);this._zd=a;},canCallAsFunction:true,members:{_zd:null,getFragment:function(){var b=JX.$N('div');b.innerHTML=this._zd;var a=document.createDocumentFragment();while(b.firstChild)a.appendChild(b.removeChild(b.firstChild));return a;}}});JX.$N=function(d,a,b){if(typeof b=='undefined'&&(typeof a!='object'||a instanceof JX.HTML)){b=a;a={};}var c=document.createElement(d);if(a.style){JX.copy(c.style,a.style);delete a.style;}if(a.sigil){JX.Stratcom.sigilize(c,a.sigil,a.meta);delete a.sigil;delete a.meta;}if(a.className){JX.DOM.alterClass(c,a.className,true);delete a.className;}JX.copy(c,a);if(b)JX.DOM.setContent(c,b);return c;};JX.install('DOM',{statics:{_ze:0,_zf:{},setContent:function(b,a){while(b.firstChild)JX.DOM.remove(b.firstChild);JX.DOM.appendContent(b,a);},prependContent:function(b,a){this._zg(b,a,this._zh);},appendContent:function(b,a){this._zg(b,a,this._zi);},_zh:function(b,a){b.insertBefore(a,b.firstChild);},_zi:function(b,a){b.appendChild(a);},_zg:function(e,b,d){if(b===null||typeof b=='undefined')return;if(b instanceof JX.HTML)b=b.getFragment();if(b instanceof Array){for(var c=0;c<b.length;c++){var a=(typeof b[c]=='string')?document.createTextNode(b[c]):b[c];d(e,a);}}else if(b.nodeType){d(e,b);}else d(e,document.createTextNode(b));},remove:function(a){a.parentNode&&JX.DOM.replace(a,null);return a;},replace:function(b,d){var a;if(b.nextSibling){a=JX.bind(b.nextSibling,function(f,e){f.insertBefore(e,this);});}else a=this._zi;var c=b.parentNode;b.parentNode.removeChild(b);this._zg(c,d,a);return b;},nearest:function(a,b){while(a&&!JX.Stratcom.hasSigil(a,b))a=a.parentNode;return a;},serialize:function(c){var b=c.getElementsByTagName('*');var a={};for(var d=0;d<b.length;++d){if(!b[d].name)continue;var f=b[d].type;var e=b[d].tagName;if((f in {radio:1,checkbox:1}&&b[d].checked)||f in {text:1,hidden:1,password:1}||e in {TEXTAREA:1,SELECT:1})a[b[d].name]=b[d].value;}return a;},isNode:function(a){return !!(a&&a.nodeName&&(a!==window));},isType:function(b,c){b=(''+(b.nodeName||'')).toUpperCase();c=JX.$AX(c);for(var a=0;a<c.length;++a)if(c[a].toUpperCase()==b)return true;return false;},listen:function(b,d,c,a){return JX.Stratcom.listen(d,['id:'+JX.DOM.uniqID(b)].concat(JX.$AX(c||[])),a);},uniqID:function(a){if(!a.id)a.id='autoid_'+(++JX.DOM._ze);return a.id;},alterClass:function(d,b,a){var c=((' '+d.className+' ').indexOf(' '+b+' ')>-1);if(a&&!c){d.className+=' '+b;}else if(c&&!a)d.className=d.className.replace(new RegExp('(^|\\s)'+b+'(?:\\s|$)','g'),' ');},htmlize:function(a){return (''+a).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');},show:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='';},hide:function(){for(var a=0;a<arguments.length;++a)arguments[a].style.display='none';},textMetrics:function(c,e,f){if(!this._zf[e]){var b=JX.$N('var',{className:e});this._zf[e]=b;}var d=this._zf[e];document.body.appendChild(d);d.style.width=f?(f+'px'):'';JX.DOM.setContent(d,JX.HTML(JX.DOM.htmlize(c.value).replace(/\n/g,'<br />')));var a=JX.$V.getDim(d);document.body.removeChild(d);return a;},scry:function(d,f,e){var b=d.getElementsByTagName(f);if(!e)return JX.$A(b);var c=[];for(var a=0;a<b.length;a++)if(JX.Stratcom.hasSigil(b[a],e))c.push(b[a]);return c;},find:function(b,d,c){var a=JX.DOM.scry(b,d,c);if(!a.length)throw JX.$.NotFound;return a[0];},focus:function(b){try{b.focus();}catch(a){}},scrollTo:function(a){window.scrollTo(0,JX.$V(a).y);}}});JX.install('JSON',{statics:{serialize:function(a){return JX.JSON._zj(a);},_zj:function(d){var c=[];if(d===null){return 'null';}else if(d.push&&d.pop){for(var a=0;a<d.length;a++)if(typeof d[a]!='undefined')c.push(JX.JSON._zj(d[a]));return '['+c.join(',')+']';}else if(d===true){return 'true';}else if(d===false){return 'false';}else if(typeof d=='string'){return JX.JSON._zk(d);}else if(typeof d=='number'){return d;}else{for(var b in d)c.push(JX.JSON._zk(b)+':'+JX.JSON._zj(d[b]));return '{'+c.join(',')+'}';}},_zk:function(a){return '"'+a.replace(/\\/g,'\\\\').replace(/"/g,'\\"')+'"';}}});
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 15, 9:31 PM (5 h, 38 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
338443
Default Alt Text
(117 KB)

Event Timeline