/** @provides javelin-lib-dev */ /** * 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