From fed4c583c5929869c3994dacac97b5fc7781d28d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 25 Jan 2011 08:18:27 -0800 Subject: [PATCH] Add Javelin. --- .../standard/PhabricatorStandardPageView.php | 14 +- webroot/rsrc/js/javelin/init.dev.js | 179 + webroot/rsrc/js/javelin/init.min.js | 1 + webroot/rsrc/js/javelin/javelin.dev.js | 2934 +++++++++++++++++ webroot/rsrc/js/javelin/javelin.min.js | 1 + 5 files changed, 3127 insertions(+), 2 deletions(-) create mode 100644 webroot/rsrc/js/javelin/init.dev.js create mode 100644 webroot/rsrc/js/javelin/init.min.js create mode 100644 webroot/rsrc/js/javelin/javelin.dev.js create mode 100644 webroot/rsrc/js/javelin/javelin.min.js 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 @@ -55,7 +55,10 @@ class PhabricatorStandardPageView extends AphrontPageView { protected function getHead() { return ''. - ''; + ''. + ''. + ''; } public function setGlyph($glyph) { @@ -105,7 +108,14 @@ class PhabricatorStandardPageView extends AphrontPageView { } protected function getTail() { - return ''; + return + ''. + ''; + + ; } } 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( + '<\/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<\/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, , ...): '+ + '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 + * @task info + */ + data : null, + + /** + * Sigil path this event was activated from. TODO: explain this + * + * @type list + * @task info + */ + path : [], + + /** + * True if propagation of the event has been stopped. See stop(). + * + * @type bool + * @task stop + */ + stopped : false, + + /** + * True if default behavior of the event has been prevented. See prevent(). + * + * @type bool + * @task stop + */ + prevented : false, + + /** + * @task info + */ + nodes : {} + }, + + /** + * @{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 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(, ' + 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(..., ..., ): ' + + '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(): ' + + '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+'", ): '+ + 'initialization function is required.'); + } + if (typeof control_function != 'function') { + throw new Error( + 'JX.behavior("'+name+'", ): '+ + '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 + *
...
+ * + * 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, is allowed but or are not.'); + } + + var really_evil = /..."): '+ + 'call initializes an HTML object with an embedded script tag! '+ + 'Are you crazy?! Do NOT do this!!!'); + } + + var wont_work = /..."): '+ + 'call initializes an HTML object with an embedded tag. IE '+ + 'will not do the right thing with this.'); + } + + // TODO(epriestley): May need to deny