diff --git a/webroot/rsrc/externals/javelin/core/__tests__/stratcom.js b/webroot/rsrc/externals/javelin/core/__tests__/stratcom.js index 6638ef1fc0..2bdbae8d78 100644 --- a/webroot/rsrc/externals/javelin/core/__tests__/stratcom.js +++ b/webroot/rsrc/externals/javelin/core/__tests__/stratcom.js @@ -1,184 +1,184 @@ /** * @requires javelin-stratcom * javelin-dom */ describe('Stratcom Tests', function() { - node1 = document.createElement('div'); + var node1 = document.createElement('div'); JX.Stratcom.addSigil(node1, 'what'); - node2 = document; - node3 = document.createElement('div'); + var node2 = document; + var node3 = document.createElement('div'); node3.className = 'what'; it('should disallow document', function() { ensure__DEV__(true, function() { expect(function() { JX.Stratcom.listen('click', 'tag:#document', function() {}); }).toThrow(); }); }); it('should disallow window', function() { ensure__DEV__(true, function() { expect(function() { JX.Stratcom.listen('click', 'tag:window', function() {}); }).toThrow(); }); }); it('should test nodes for hasSigil', function() { expect(JX.Stratcom.hasSigil(node1, 'what')).toBe(true); expect(JX.Stratcom.hasSigil(node3, 'what')).toBe(false); ensure__DEV__(true, function() { expect(function() { JX.Stratcom.hasSigil(node2, 'what'); }).toThrow(); }); }); it('should be able to add sigils', function() { var node = document.createElement('div'); JX.Stratcom.addSigil(node, 'my-sigil'); expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true); expect(JX.Stratcom.hasSigil(node, 'i-dont-haz')).toBe(false); JX.Stratcom.addSigil(node, 'javelin-rocks'); expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true); expect(JX.Stratcom.hasSigil(node, 'javelin-rocks')).toBe(true); // Should not arbitrarily take away other sigils JX.Stratcom.addSigil(node, 'javelin-rocks'); expect(JX.Stratcom.hasSigil(node, 'my-sigil')).toBe(true); expect(JX.Stratcom.hasSigil(node, 'javelin-rocks')).toBe(true); }); it('should test dataPersistence', function() { var n, d; n = JX.$N('div'); d = JX.Stratcom.getData(n); expect(d).toEqual({}); d.noise = 'quack'; expect(JX.Stratcom.getData(n).noise).toEqual('quack'); n = JX.$N('div'); JX.Stratcom.addSigil(n, 'oink'); d = JX.Stratcom.getData(n); expect(JX.Stratcom.getData(n)).toEqual({}); d.noise = 'quack'; expect(JX.Stratcom.getData(n).noise).toEqual('quack'); ensure__DEV__(true, function(){ var bad_values = [false, null, undefined, 'quack']; for (var ii = 0; ii < bad_values.length; ii++) { n = JX.$N('div'); expect(function() { JX.Stratcom.addSigil(n, 'oink'); JX.Stratcom.addData(n, bad_values[ii]); }).toThrow(); } }); }); it('should allow the merge of additional data', function() { ensure__DEV__(true, function() { var clown = JX.$N('div'); clown.setAttribute('data-meta', '0_0'); JX.Stratcom.mergeData('0', {'0' : 'clown'}); expect(JX.Stratcom.getData(clown)).toEqual('clown'); var town = JX.$N('div'); town.setAttribute('data-meta', '0_1'); JX.Stratcom.mergeData('0', {'1' : 'town'}); expect(JX.Stratcom.getData(clown)).toEqual('clown'); expect(JX.Stratcom.getData(town)).toEqual('town'); expect(function() { JX.Stratcom.mergeData('0', {'0' : 'oops'}); }).toThrow(); }); }); it('all listeners should be called', function() { ensure__DEV__(true, function() { var callback_count = 0; JX.Stratcom.listen('custom:eventA', null, function() { callback_count++; }); JX.Stratcom.listen('custom:eventA', null, function() { callback_count++; }); expect(callback_count).toEqual(0); JX.Stratcom.invoke('custom:eventA'); expect(callback_count).toEqual(2); }); }); it('removed listeners should not be called', function() { ensure__DEV__(true, function() { var callback_count = 0; var listeners = []; var remove_listeners = function() { while (listeners.length) { listeners.pop().remove(); } }; listeners.push( JX.Stratcom.listen('custom:eventB', null, function() { callback_count++; remove_listeners(); }) ); listeners.push( JX.Stratcom.listen('custom:eventB', null, function() { callback_count++; remove_listeners(); }) ); expect(callback_count).toEqual(0); JX.Stratcom.invoke('custom:eventB'); expect(listeners.length).toEqual(0); expect(callback_count).toEqual(1); }); }); it('should throw when accessing data in an unloaded block', function() { ensure__DEV__(true, function() { var n = JX.$N('div'); n.setAttribute('data-meta', '9999999_9999999'); var caught; try { JX.Stratcom.getData(n); } catch (error) { caught = error; } expect(caught instanceof Error).toEqual(true); }); }); // it('can set data serializer', function() { // var uri = new JX.URI('http://www.facebook.com/home.php?key=value'); // uri.setQuerySerializer(JX.PHPQuerySerializer.serialize); // uri.setQueryParam('obj', { // num : 1, // obj : { // str : 'abc', // i : 123 // } // }); // expect(decodeURIComponent(uri.toString())).toEqual( // 'http://www.facebook.com/home.php?key=value&' + // 'obj[num]=1&obj[obj][str]=abc&obj[obj][i]=123'); // }); }); diff --git a/webroot/rsrc/externals/javelin/core/__tests__/util.js b/webroot/rsrc/externals/javelin/core/__tests__/util.js index b2f72c6747..0f7f861a19 100644 --- a/webroot/rsrc/externals/javelin/core/__tests__/util.js +++ b/webroot/rsrc/externals/javelin/core/__tests__/util.js @@ -1,85 +1,85 @@ /** * @requires javelin-util */ describe('JX.isArray', function() { it('should correctly identify an array', function() { expect(JX.isArray([1, 2, 3])).toBe(true); expect(JX.isArray([])).toBe(true); }); it('should return false on anything that is not an array', function() { expect(JX.isArray(1)).toBe(false); expect(JX.isArray('a string')).toBe(false); expect(JX.isArray(true)).toBe(false); expect(JX.isArray(/regex/)).toBe(false); expect(JX.isArray(new String('a super string'))).toBe(false); expect(JX.isArray(new Number(42))).toBe(false); expect(JX.isArray(new Boolean(false))).toBe(false); expect(JX.isArray({})).toBe(false); expect(JX.isArray({'0': 1, '1': 2, length: 2})).toBe(false); expect(JX.isArray((function(){ return arguments; })('I', 'want', 'to', 'trick', 'you'))).toBe(false); }); it('should identify an array from another context as an array', function() { var iframe = document.createElement('iframe'); - var name = iframe.name = 'javelin-iframe-test'; + iframe.name = 'javelin-iframe-test'; iframe.style.display = 'none'; document.body.insertBefore(iframe, document.body.firstChild); var doc = iframe.contentWindow.document; doc.write( '' ); var array = MaybeArray(1, 2, 3); var array2 = new MaybeArray(1); array2[0] = 5; expect(JX.isArray(array)).toBe(true); expect(JX.isArray(array2)).toBe(true); }); }); describe('JX.bind', function() { it('should bind a function to a context', function() { var object = {a: 5, b: 3}; JX.bind(object, function() { object.b = 1; })(); expect(object).toEqual({a: 5, b: 1}); }); it('should bind a function without context', function() { var called; JX.bind(null, function() { called = true; })(); expect(called).toBe(true); }); it('should bind with arguments', function() { var list = []; JX.bind(null, function() { list.push.apply(list, JX.$A(arguments)); }, 'a', 2, 'c', 4)(); expect(list).toEqual(['a', 2, 'c', 4]); }); it('should allow to pass additional arguments', function() { var list = []; JX.bind(null, function() { list.push.apply(list, JX.$A(arguments)); }, 'a', 2)('c', 4); expect(list).toEqual(['a', 2, 'c', 4]); }); }); diff --git a/webroot/rsrc/externals/javelin/core/install.js b/webroot/rsrc/externals/javelin/core/install.js index 284d529be4..c56e6489a0 100644 --- a/webroot/rsrc/externals/javelin/core/install.js +++ b/webroot/rsrc/externals/javelin/core/install.js @@ -1,455 +1,455 @@ /** * @requires javelin-util * javelin-magical-init * @provides javelin-install * * @javelin-installs JX.install * @javelin-installs JX.createClass * * @javelin */ /** * Install a class into the Javelin ("JX") namespace. The first argument is the * name of the class you want to install, and the second is a map of these * attributes (all of which are optional): * * - ##construct## //(function)// Class constructor. If you don't provide one, * one will be created for you (but it will be very boring). * - ##extend## //(string)// The name of another JX-namespaced class to extend * via prototypal inheritance. * - ##members## //(map)// A map of instance methods and properties. * - ##statics## //(map)// A map of static methods and properties. * - ##initialize## //(function)// A function which will be run once, after * this class has been installed. * - ##properties## //(map)// A map of properties that should have instance * getters and setters automatically generated for them. The key is the * property name and the value is its default value. For instance, if you * provide the property "size", the installed class will have the methods * "getSize()" and "setSize()". It will **NOT** have a property ".size" * and no guarantees are made about where install is actually chosing to * store the data. The motivation here is to let you cheaply define a * stable interface and refine it later as necessary. * - ##events## //(list)// List of event types this class is capable of * emitting. * * For example: * * JX.install('Dog', { * construct : function(name) { * this.setName(name); * }, * members : { * bark : function() { * // ... * } * }, * properites : { * name : null, * } * }); * * This creates a new ##Dog## class in the ##JX## namespace: * * var d = new JX.Dog(); * d.bark(); * * Javelin classes are normal Javascript functions and generally behave in * the expected way. Some properties and methods are automatically added to * all classes: * * - ##instance.__id__## Globally unique identifier attached to each instance. * - ##prototype.__class__## Reference to the class constructor. * - ##constructor.__path__## List of path tokens used emit events. It is * probably never useful to access this directly. * - ##constructor.__readable__## Readable class name. You could use this * for introspection. * - ##constructor.__events__## //DEV ONLY!// List of events supported by * this class. * - ##constructor.listen()## Listen to all instances of this class. See * @{JX.Base}. * - ##instance.listen()## Listen to one instance of this class. See * @{JX.Base}. * - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}. * * * @param string Name of the class to install. It will appear in the JX * "namespace" (e.g., JX.Pancake). * @param map Map of properties, see method documentation. * @return void */ JX.install = function(new_name, new_junk) { // If we've already installed this, something is up. if (new_name in JX) { if (__DEV__) { JX.$E( 'JX.install("' + new_name + '", ...): ' + 'trying to reinstall something that has already been installed.'); } return; } if (__DEV__) { if ('name' in new_junk) { JX.$E( 'JX.install("' + new_name + '", {"name": ...}): ' + 'trying to install with "name" property.' + 'Either remove it or call JX.createClass directly.'); } } // Since we may end up loading things out of order (e.g., Dog extends Animal // but we load Dog first) we need to keep a list of things that we've been // asked to install but haven't yet been able to install around. (JX.install._queue || (JX.install._queue = [])).push([new_name, new_junk]); var name; do { var junk; var initialize; name = null; for (var ii = 0; ii < JX.install._queue.length; ++ii) { junk = JX.install._queue[ii][1]; if (junk.extend && !JX[junk.extend]) { // We need to extend something that we haven't been able to install // yet, so just keep this in queue. continue; } // Install time! First, get this out of the queue. name = JX.install._queue.splice(ii, 1)[0][0]; --ii; if (junk.extend) { junk.extend = JX[junk.extend]; } initialize = junk.initialize; delete junk.initialize; junk.name = 'JX.' + name; JX[name] = JX.createClass(junk); if (initialize) { if (JX['Stratcom'] && JX['Stratcom'].ready) { initialize.apply(null); } else { // This is a holding queue, defined in init.js. JX['install-init'](initialize); } } } // In effect, this exits the loop as soon as we didn't make any progress // installing things, which means we've installed everything we have the // dependencies for. } while (name); }; /** * Creates a class from a map of attributes. Requires ##extend## property to * be an actual Class object and not a "String". Supports ##name## property * to give the created Class a readable name. * * @see JX.install for description of supported attributes. * * @param junk Map of properties, see method documentation. * @return function Constructor of a class created */ JX.createClass = function(junk) { var name = junk.name || ''; var k; var ii; if (__DEV__) { var valid = { construct : 1, statics : 1, members : 1, extend : 1, properties : 1, events : 1, name : 1 }; for (k in junk) { if (!(k in valid)) { JX.$E( 'JX.createClass("' + name + '", {"' + k + '": ...}): ' + 'trying to create unknown property `' + k + '`.'); } } if (junk.constructor !== {}.constructor) { JX.$E( 'JX.createClass("' + name + '", {"constructor": ...}): ' + 'property `constructor` should be called `construct`.'); } } // First, build the constructor. If construct is just a function, this // won't change its behavior (unless you have provided a really awesome // function, in which case it will correctly punish you for your attempt // at creativity). var Class = (function(name, junk) { var result = function() { this.__id__ = '__obj__' + (++JX.install._nextObjectID); return (junk.construct || junk.extend || JX.bag).apply(this, arguments); // TODO: Allow mixins to initialize here? // TODO: Also, build mixins? }; if (__DEV__) { var inner = result; result = function() { if (this == window || this == JX) { JX.$E( '<' + Class.__readable__ + '>: ' + 'Tried to construct an instance without the "new" operator.'); } return inner.apply(this, arguments); }; } return result; })(name, junk); Class.__readable__ = name; // Copy in all the static methods and properties. for (k in junk.statics) { // Can't use JX.copy() here yet since it may not have loaded. Class[k] = junk.statics[k]; } var proto; if (junk.extend) { var Inheritance = function() {}; Inheritance.prototype = junk.extend.prototype; proto = Class.prototype = new Inheritance(); } else { proto = Class.prototype = {}; } proto.__class__ = Class; var setter = function(prop) { return function(v) { this[prop] = v; return this; }; }; var getter = function(prop) { - return function(v) { + return function() { return this[prop]; }; }; // Build getters and setters from the `prop' map. for (k in (junk.properties || {})) { var base = k.charAt(0).toUpperCase() + k.substr(1); var prop = '__auto__' + k; proto[prop] = junk.properties[k]; proto['set' + base] = setter(prop); proto['get' + base] = getter(prop); } if (__DEV__) { // Check for aliasing in default values of members. If we don't do this, // you can run into a problem like this: // // JX.install('List', { members : { stuff : [] }}); // // var i_love = new JX.List(); // var i_hate = new JX.List(); // // i_love.stuff.push('Psyduck'); // I love psyduck! // JX.log(i_hate.stuff); // Show stuff I hate. // // This logs ["Psyduck"] because the push operation modifies // JX.List.prototype.stuff, which is what both i_love.stuff and // i_hate.stuff resolve to. To avoid this, set the default value to // null (or any other scalar) and do "this.stuff = [];" in the // constructor. for (var member_name in junk.members) { if (junk.extend && member_name[0] == '_') { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" must not be named with ' + 'a leading underscore because it is in a subclass. Variables ' + 'are analyzed and crushed one file at a time, and crushed ' + 'member variables in subclasses alias crushed member variables ' + 'in superclasses. Remove the underscore, refactor the class so ' + 'it does not extend anything, or fix the minifier to be ' + 'capable of safely crushing subclasses.'); } var member_value = junk.members[member_name]; if (typeof member_value == 'object' && member_value !== null) { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" is not a scalar or ' + 'function. Prototypal inheritance in Javascript aliases object ' + 'references across instances so all instances are initialized ' + 'to point at the exact same object. This is almost certainly ' + 'not what you intended. Make this member static to share it ' + 'across instances, or initialize it in the constructor to ' + 'prevent reference aliasing and give each instance its own ' + 'copy of the value.'); } } } // This execution order intentionally allows you to override methods // generated from the "properties" initializer. for (k in junk.members) { proto[k] = junk.members[k]; } // IE does not enumerate some properties on objects var enumerables = JX.install._enumerables; if (junk.members && enumerables) { ii = enumerables.length; while (ii--){ var property = enumerables[ii]; if (junk.members[property]) { proto[property] = junk.members[property]; } } } // Build this ridiculous event model thing. Basically, this defines // two instance methods, invoke() and listen(), and one static method, // listen(). If you listen to an instance you get events for that // instance; if you listen to a class you get events for all instances // of that class (including instances of classes which extend it). // // This is rigged up through Stratcom. Each class has a path component // like "class:Dog", and each object has a path component like // "obj:23". When you invoke on an object, it emits an event with // a path that includes its class, all parent classes, and its object // ID. // // Calling listen() on an instance listens for just the object ID. // Calling listen() on a class listens for that class's name. This // has the effect of working properly, but installing them is pretty // messy. var parent = junk.extend || {}; var old_events = parent.__events__; var new_events = junk.events || []; var has_events = old_events || new_events.length; if (has_events) { var valid_events = {}; // If we're in dev, we build up a list of valid events (for this class // and our parent class), and then check them on listen and invoke. if (__DEV__) { for (var key in old_events || {}) { valid_events[key] = true; } for (ii = 0; ii < new_events.length; ++ii) { valid_events[junk.events[ii]] = true; } } Class.__events__ = valid_events; // Build the class name chain. Class.__name__ = 'class:' + name; var ancestry = parent.__path__ || []; Class.__path__ = ancestry.concat([Class.__name__]); proto.invoke = function(type) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.invoke("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } // Here and below, this nonstandard access notation is used to mask // these callsites from the static analyzer. JX.Stratcom is always // available by the time we hit these execution points. return JX['Stratcom'].invoke( 'obj:' + type, this.__class__.__path__.concat([this.__id__]), {args : JX.$A(arguments).slice(1)}); }; proto.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__id__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; Class.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__events__)) { JX.$E( this.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__name__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; } else if (__DEV__) { var error_message = 'class does not define any events. Pass an "events" property to ' + 'JX.createClass() to define events.'; Class.listen = Class.listen || function() { JX.$E( this.__readable__ + '.listen(...): ' + error_message); }; Class.invoke = Class.invoke || function() { JX.$E( this.__readable__ + '.invoke(...): ' + error_message); }; proto.listen = proto.listen || function() { JX.$E( this.__class__.__readable__ + '.listen(...): ' + error_message); }; proto.invoke = proto.invoke || function() { JX.$E( this.__class__.__readable__ + '.invoke(...): ' + error_message); }; } return Class; }; JX.install._nextObjectID = 0; JX.flushHoldingQueue('install', JX.install); (function() { // IE does not enter this loop. for (var i in {toString: 1}) { return; } JX.install._enumerables = [ 'toString', 'hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'constructor' ]; })(); diff --git a/webroot/rsrc/externals/javelin/core/util.js b/webroot/rsrc/externals/javelin/core/util.js index be43b73faa..e09171d0d6 100644 --- a/webroot/rsrc/externals/javelin/core/util.js +++ b/webroot/rsrc/externals/javelin/core/util.js @@ -1,345 +1,345 @@ /** * Javelin utility functions. * * @provides javelin-util * * @javelin-installs JX.$E * @javelin-installs JX.$A * @javelin-installs JX.$AX * @javelin-installs JX.isArray * @javelin-installs JX.copy * @javelin-installs JX.bind * @javelin-installs JX.bag * @javelin-installs JX.keys * @javelin-installs JX.log * @javelin-installs JX.id * @javelin-installs JX.now * * @javelin */ /** * Throw an exception and attach the caller data in the exception. * * @param string Exception message. */ JX.$E = function(message) { var e = new Error(message); var caller_fn = JX.$E.caller; if (caller_fn) { e.caller_fn = caller_fn.caller; } throw e; }; /** * Convert an array-like object (usually ##arguments##) into a real Array. An * "array-like object" is something with a ##length## property and numerical * keys. The most common use for this is to let you call Array functions on the * magical ##arguments## object. * * JX.$A(arguments).slice(1); * * @param obj Array, or array-like object. * @return Array Actual array. */ JX.$A = function(object) { // IE8 throws "JScript object expected" when trying to call // Array.prototype.slice on a NodeList, so just copy items one by one here. var r = []; for (var ii = 0; ii < object.length; ii++) { r.push(object[ii]); } return r; }; /** * Cast a value into an array, by wrapping scalars into singletons. If the * argument is an array, it is returned unmodified. If it is a scalar, an array * with a single element is returned. For example: * * JX.$AX([3]); // Returns [3]. * JX.$AX(3); // Returns [3]. * * Note that this function uses a @{function:JX.isArray} check whether or not * the argument is an array, so you may need to convert array-like objects (such * as ##arguments##) into real arrays with @{function:JX.$A}. * * This function is mostly useful to create methods which accept either a * value or a list of values. * * @param wild Scalar or Array. * @return Array If the argument was a scalar, an Array with the argument as * its only element. Otherwise, the original Array. */ JX.$AX = function(maybe_scalar) { return JX.isArray(maybe_scalar) ? maybe_scalar : [maybe_scalar]; }; /** * Checks whether a value is an array. * * JX.isArray(['an', 'array']); // Returns true. * JX.isArray('Not an Array'); // Returns false. * * @param wild Any value. * @return bool true if the argument is an array, false otherwise. */ JX.isArray = Array.isArray || function(maybe_array) { return Object.prototype.toString.call(maybe_array) == '[object Array]'; }; /** * Copy properties from one object to another. If properties already exist, they * are overwritten. * * var cat = { * ears: 'clean', * paws: 'clean', * nose: 'DIRTY OH NOES' * }; * var more = { * nose: 'clean', * tail: 'clean' * }; * * JX.copy(cat, more); * * // cat is now: * // { * // ears: 'clean', * // paws: 'clean', * // nose: 'clean', * // tail: 'clean' * // } * * NOTE: This function does not copy the ##toString## property or anything else * which isn't enumerable or is somehow magic or just doesn't work. But it's * usually what you want. * * @param obj Destination object, which properties should be copied to. * @param obj Source object, which properties should be copied from. * @return obj Modified destination object. */ JX.copy = function(copy_dst, copy_src) { for (var k in copy_src) { copy_dst[k] = copy_src[k]; } return copy_dst; }; /** * Create a function which invokes another function with a bound context and * arguments (i.e., partial function application) when called; king of all * functions. * * Bind performs context binding (letting you select what the value of ##this## * will be when a function is invoked) and partial function application (letting * you create some function which calls another one with bound arguments). * * = Context Binding = * * Normally, when you call ##obj.method()##, the magic ##this## object will be * the ##obj## you invoked the method from. This can be undesirable when you * need to pass a callback to another function. For instance: * * COUNTEREXAMPLE * var dog = new JX.Dog(); * dog.barkNow(); // Makes the dog bark. * * JX.Stratcom.listen('click', 'bark', dog.barkNow); // Does not work! * * This doesn't work because ##this## is ##window## when the function is * later invoked; @{method:JX.Stratcom.listen} does not know about the context * object ##dog##. The solution is to pass a function with a bound context * object: * * var dog = new JX.Dog(); * var bound_function = JX.bind(dog, dog.barkNow); * * JX.Stratcom.listen('click', 'bark', bound_function); * * ##bound_function## is a function with ##dog## bound as ##this##; ##this## * will always be ##dog## when the function is called, no matter what * property chain it is invoked from. * * You can also pass ##null## as the context argument to implicitly bind * ##window##. * * = Partial Function Application = * * @{function:JX.bind} also performs partial function application, which allows * you to bind one or more arguments to a function. For instance, if we have a * simple function which adds two numbers: * * function add(a, b) { return a + b; } * add(3, 4); // 7 * * Suppose we want a new function, like this: * * function add3(b) { return 3 + b; } * add3(4); // 7 * * Instead of doing this, we can define ##add3()## in terms of ##add()## by * binding the value ##3## to the ##a## argument: * * var add3_bound = JX.bind(null, add, 3); * add3_bound(4); // 7 * * Zero or more arguments may be bound in this way. This is particularly useful * when using closures in a loop: * * COUNTEREXAMPLE * for (var ii = 0; ii < button_list.length; ii++) { * button_list[ii].onclick = function() { * JX.log('You clicked button number '+ii+'!'); // Fails! * }; * } * * This doesn't work; all the buttons report the highest number when clicked. * This is because the local ##ii## is captured by the closure. Instead, bind * the current value of ##ii##: * * var func = function(button_num) { * JX.log('You clicked button number '+button_num+'!'); * } * for (var ii = 0; ii < button_list.length; ii++) { * button_list[ii].onclick = JX.bind(null, func, ii); * } * * @param obj|null Context object to bind as ##this##. * @param function Function to bind context and arguments to. * @param ... Zero or more arguments to bind. * @return function New function which invokes the original function with * bound context and arguments when called. */ JX.bind = function(context, func, more) { if (__DEV__) { if (typeof func != 'function') { JX.$E( 'JX.bind(context, , ...): '+ 'Attempting to bind something that is not a function.'); } } var bound = JX.$A(arguments).slice(2); if (func.bind) { return func.bind.apply(func, [context].concat(bound)); } return function() { return func.apply(context || window, bound.concat(JX.$A(arguments))); }; }; /** * "Bag of holding"; function that does nothing. Primarily, it's used as a * placeholder when you want something to be callable but don't want it to * actually have an effect. * * @return void */ 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 = Object.keys || function(obj) { var r = []; for (var k in obj) { r.push(k); } return r; }; /** * Identity function; returns the argument unmodified. This is primarily useful * as a placeholder for some callback which may transform its argument. * * @param wild Any value. * @return wild The passed argument. */ JX.id = function(any) { return any; }; 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) { }}; + window.console = {log: function() {}}; } } /** * Print a message to the browser debugging console (like Firebug). * * @param string Message to print to the browser debugging console. * @return void */ JX.log = function(message) { window.console.log(message); }; if (__DEV__) { window.alert = (function(native_alert) { var recent_alerts = []; var in_alert = false; return function(msg) { if (in_alert) { JX.log( 'alert(...): '+ 'discarded reentrant alert.'); return; } in_alert = true; recent_alerts.push(JX.now()); if (recent_alerts.length > 3) { recent_alerts.splice(0, recent_alerts.length - 3); } if (recent_alerts.length >= 3 && (recent_alerts[recent_alerts.length - 1] - recent_alerts[0]) < 5000) { if (window.confirm(msg + "\n\nLots of alert()s recently. Kill them?")) { window.alert = JX.bag; } } else { // Note that we can't .apply() the IE6 version of this "function". native_alert(msg); } in_alert = false; }; })(window.alert); } /** * Date.now is the fastest timestamp function, but isn't supported by every * browser. This gives the fastest version the environment can support. * The wrapper function makes the getTime call even slower, but benchmarking * shows it to be a marginal perf loss. Considering how small of a perf * difference this makes overall, it's not really a big deal. The primary * reason for this is to avoid hacky "just think of the byte savings" JS * like +new Date() that has an unclear outcome for the unexposed. * * @return Int A Unix timestamp of the current time on the local machine */ JX.now = (Date.now || function() { return new Date().getTime(); }); diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js b/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js index a40d864dc8..3af21b14b5 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/Reactor.js @@ -1,89 +1,89 @@ /** * @provides javelin-reactor * @requires javelin-install * javelin-util * @javelin */ JX.install('Reactor', { statics : { /** * Return this value from a ReactorNode transformer to indicate that * its listeners should not be activated. */ DoNotPropagate : {}, /** * For internal use by the Reactor system. */ propagatePulse : function(start_pulse, start_node) { var reverse_post_order = JX.Reactor._postOrder(start_node).reverse(); start_node.primeValue(start_pulse); for (var ix = 0; ix < reverse_post_order.length; ix++) { var node = reverse_post_order[ix]; var pulse = node.getNextPulse(); if (pulse === JX.Reactor.DoNotPropagate) { continue; } var next_pulse = node.getTransformer()(pulse); var sends_to = node.getListeners(); for (var jx = 0; jx < sends_to.length; jx++) { sends_to[jx].primeValue(next_pulse); } } }, /** * For internal use by the Reactor system. */ _postOrder : function(node, result, pending) { - if (typeof result === "undefined") { + if (typeof result === 'undefined') { result = []; pending = {}; } pending[node.getGraphID()] = true; var nexts = node.getListeners(); for (var ix = 0; ix < nexts.length; ix++) { var next = nexts[ix]; if (pending[next.getGraphID()]) { continue; } JX.Reactor._postOrder(next, result, pending); } result.push(node); return result; }, // Helper for lift. _valueNow : function(fn, dynvals) { var values = []; for (var ix = 0; ix < dynvals.length; ix++) { values.push(dynvals[ix].getValueNow()); } return fn.apply(null, values); }, /** * Lift a function over normal values to be a function over dynvals. * @param fn A function expecting normal values * @param dynvals Array of DynVals whose instaneous values will be passed * to fn. * @return A DynVal representing the changing value of fn applies to dynvals * over time. */ lift : function(fn, dynvals) { var valueNow = JX.bind(null, JX.Reactor._valueNow, fn, dynvals); var streams = []; for (var ix = 0; ix < dynvals.length; ix++) { streams.push(dynvals[ix].getChanges()); } var result = new JX['ReactorNode'](streams, valueNow); return new JX['DynVal'](result, valueNow()); } } }); diff --git a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js index 02c60f9c41..c500ce4c33 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/core/ReactorNode.js @@ -1,96 +1,96 @@ /** * @provides javelin-reactornode * @requires javelin-install * javelin-reactor * javelin-util * javelin-reactor-node-calmer * @javelin */ JX.install('ReactorNode', { members : { _transformer : null, _sendsTo : null, _nextPulse : null, _graphID : null, getGraphID : function() { return this._graphID || this.__id__; }, setGraphID : function(id) { this._graphID = id; return this; }, setTransformer : function(fn) { this._transformer = fn; return this; }, /** * Set up dest as a listener to this. */ listen : function(dest) { this._sendsTo[dest.__id__] = dest; return { remove : JX.bind(null, this._removeListener, dest) }; }, /** * Helper for listen. */ _removeListener : function(dest) { delete this._sendsTo[dest.__id__]; }, /** * For internal use by the Reactor system */ primeValue : function(value) { this._nextPulse = value; }, getListeners : function() { var result = []; for (var k in this._sendsTo) { result.push(this._sendsTo[k]); } return result; }, /** * For internal use by the Reactor system */ - getNextPulse : function(pulse) { + getNextPulse : function() { return this._nextPulse; }, getTransformer : function() { return this._transformer; }, forceSendValue : function(pulse) { JX.Reactor.propagatePulse(pulse, this); }, // fn should return JX.Reactor.DoNotPropagate to indicate a value that // should not be retransmitted. transform : function(fn) { return new JX.ReactorNode([this], fn); }, /** * Suppress events to happen at most once per min_interval. * The last event that fires within an interval will fire at the end * of the interval. Events that are sandwiched between other events * within an interval are dropped. */ calm : function(min_interval) { var result = new JX.ReactorNode([this], JX.id); var transformer = new JX.ReactorNodeCalmer(result, min_interval); result.setTransformer(JX.bind(transformer, transformer.onPulse)); return result; } }, construct : function(source_streams, transformer) { this._nextPulse = JX.Reactor.DoNotPropagate; this._transformer = transformer; this._sendsTo = {}; for (var ix = 0; ix < source_streams.length; ix++) { source_streams[ix].listen(this); } } }); diff --git a/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js b/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js index f34907f27f..8bdbefd07b 100644 --- a/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js +++ b/webroot/rsrc/externals/javelin/ext/reactor/dom/RDOM.js @@ -1,405 +1,405 @@ /** * Javelin Reactive functions to work with the DOM. * @provides javelin-reactor-dom * @requires javelin-dom * javelin-dynval * javelin-reactor * javelin-reactornode * javelin-install * javelin-util * @javelin */ JX.install('RDOM', { statics : { _time : null, /** * DynVal of the current time in milliseconds. */ time : function() { if (JX.RDOM._time === null) { var time = new JX.ReactorNode([], JX.id); window.setInterval(function() { time.forceSendValue(JX.now()); }, 100); JX.RDOM._time = new JX.DynVal(time, JX.now()); } return JX.RDOM._time; }, /** * Given a DynVal[String], return a DOM text node whose value tracks it. */ $DT : function(dyn_string) { var node = document.createTextNode(dyn_string.getValueNow()); dyn_string.transform(function(s) { node.data = s; }); return node; }, _recvEventPulses : function(node, event) { var reactor_node = new JX.ReactorNode([], JX.id); var no_path = null; JX.DOM.listen( node, event, no_path, JX.bind(reactor_node, reactor_node.forceSendValue) ); reactor_node.setGraphID(JX.DOM.uniqID(node)); return reactor_node; }, _recvChangePulses : function(node) { return JX.RDOM._recvEventPulses(node, 'change').transform(function() { return node.value; }); }, /** * Sets up a bidirectional DynVal for a node. * @param node :: DOM Node * @param inPulsesFn :: DOM Node -> ReactorNode * @param inDynValFn :: DOM Node -> ReactorNode -> DynVal * @param outFn :: ReactorNode -> DOM Node */ _bidi : function(node, inPulsesFn, inDynValFn, outFn) { var inPulses = inPulsesFn(node); var inDynVal = inDynValFn(node, inPulses); outFn(inDynVal.getChanges(), node); inDynVal.getChanges().listen(inPulses); return inDynVal; }, /** * ReactorNode[String] of the incoming values of a radio group. * @param Array of DOM elements, all the radio buttons in a group. */ _recvRadioPulses : function(buttons) { var ins = []; for (var ii = 0; ii < buttons.length; ii++) { ins.push(JX.RDOM._recvChangePulses(buttons[ii])); } return new JX.ReactorNode(ins, JX.id); }, /** * DynVal[String] of the incoming values of a radio group. * pulses is a ReactorNode[String] of the incoming values of the group */ _recvRadio : function(buttons, pulses) { var init = ''; for (var ii = 0; ii < buttons.length; ii++) { if (buttons[ii].checked) { init = buttons[ii].value; break; } } return new JX.DynVal(pulses, init); }, /** * Send the pulses from the ReactorNode[String] to the radio group. * Sending an invalid value will result in a log message in __DEV__. */ _sendRadioPulses : function(rnode, buttons) { return rnode.transform(function(val) { var found; if (__DEV__) { found = false; } for (var ii = 0; ii < buttons.length; ii++) { if (buttons[ii].value == val) { buttons[ii].checked = true; if (__DEV__) { found = true; } } } if (__DEV__) { if (!found) { - throw new Error("Mismatched radio button value"); + throw new Error('Mismatched radio button value'); } } }); }, /** * Bidirectional DynVal[String] for a radio group. * Sending an invalid value will result in a log message in __DEV__. */ radio : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvRadioPulses, JX.RDOM._recvRadio, JX.RDOM._sendRadioPulses ); }, /** * ReactorNode[Boolean] of the values of the checkbox when it changes. */ _recvCheckboxPulses : function(checkbox) { return JX.RDOM._recvChangePulses(checkbox).transform(function(val) { return Boolean(val); }); }, /** * DynVal[Boolean] of the value of a checkbox. */ _recvCheckbox : function(checkbox, pulses) { return new JX.DynVal(pulses, Boolean(checkbox.checked)); }, /** * Send the pulses from the ReactorNode[Boolean] to the checkbox */ _sendCheckboxPulses : function(rnode, checkbox) { return rnode.transform(function(val) { if (__DEV__) { if (!(val === true || val === false)) { - throw new Error("Send boolean values to checkboxes."); + throw new Error('Send boolean values to checkboxes.'); } } checkbox.checked = val; }); }, /** * Bidirectional DynVal[Boolean] for a checkbox. */ checkbox : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvCheckboxPulses, JX.RDOM._recvCheckbox, JX.RDOM._sendCheckboxPulses ); }, /** * ReactorNode[String] of the changing values of a text input. */ _recvInputPulses : function(input) { // This misses advanced changes like paste events. var live_changes = [ JX.RDOM._recvChangePulses(input), JX.RDOM._recvEventPulses(input, 'keyup'), JX.RDOM._recvEventPulses(input, 'keypress'), JX.RDOM._recvEventPulses(input, 'keydown') ]; return new JX.ReactorNode(live_changes, function() { return input.value; }); }, /** * DynVal[String] of the value of a text input. */ _recvInput : function(input, pulses) { return new JX.DynVal(pulses, input.value); }, /** * Send the pulses from the ReactorNode[String] to the input */ _sendInputPulses : function(rnode, input) { var result = rnode.transform(function(val) { input.value = val; }); result.setGraphID(JX.DOM.uniqID(input)); return result; }, /** * Bidirectional DynVal[String] for a text input. */ input : function(input) { return JX.RDOM._bidi( input, JX.RDOM._recvInputPulses, JX.RDOM._recvInput, JX.RDOM._sendInputPulses ); }, /** * ReactorNode[String] of the incoming changes in value of a select element. */ _recvSelectPulses : function(select) { return JX.RDOM._recvChangePulses(select); }, /** * DynVal[String] of the value of a select element. */ _recvSelect : function(select, pulses) { return new JX.DynVal(pulses, select.value); }, /** * Send the pulses from the ReactorNode[String] to the select. * Sending an invalid value will result in a log message in __DEV__. */ _sendSelectPulses : function(rnode, select) { return rnode.transform(function(val) { select.value = val; if (__DEV__) { if (select.value !== val) { - throw new Error("Mismatched select value"); + throw new Error('Mismatched select value'); } } }); }, /** * Bidirectional DynVal[String] for the value of a select. */ select : function(select) { return JX.RDOM._bidi( select, JX.RDOM._recvSelectPulses, JX.RDOM._recvSelect, JX.RDOM._sendSelectPulses ); }, /** * ReactorNode[undefined] that fires when a button is clicked. */ clickPulses : function(button) { return JX.RDOM._recvEventPulses(button, 'click').transform(function() { return null; }); }, /** * ReactorNode[Boolean] of whether the mouse is over a target. */ _recvIsMouseOverPulses : function(target) { var mouseovers = JX.RDOM._recvEventPulses(target, 'mouseover').transform( function() { return true; }); var mouseouts = JX.RDOM._recvEventPulses(target, 'mouseout').transform( function() { return false; }); return new JX.ReactorNode([mouseovers, mouseouts], JX.id); }, /** * DynVal[Boolean] of whether the mouse is over a target. */ isMouseOver : function(target) { // Not worth it to initialize this properly. return new JX.DynVal(JX.RDOM._recvIsMouseOverPulses(target), false); }, /** * ReactorNode[Boolean] of whether an element has the focus. */ _recvHasFocusPulses : function(target) { var focuses = JX.RDOM._recvEventPulses(target, 'focus').transform( function() { return true; }); var blurs = JX.RDOM._recvEventPulses(target, 'blur').transform( function() { return false; }); return new JX.ReactorNode([focuses, blurs], JX.id); }, /** * DynVal[Boolean] of whether an element has the focus. */ _recvHasFocus : function(target) { var is_focused_now = (target === document.activeElement); return new JX.DynVal(JX.RDOM._recvHasFocusPulses(target), is_focused_now); }, _sendHasFocusPulses : function(rnode, target) { rnode.transform(function(should_focus) { if (should_focus) { target.focus(); } else { target.blur(); } return should_focus; }); }, /** * Bidirectional DynVal[Boolean] of whether an element has the focus. */ hasFocus : function(target) { return JX.RDOM._bidi( target, JX.RDOM._recvHasFocusPulses, JX.RDOM._recvHasFocus, JX.RDOM._sendHasFocusPulses ); }, /** * Send a CSS class from a DynVal to a node */ sendClass : function(dynval, node, className) { return dynval.transform(function(add) { JX.DOM.alterClass(node, className, add); }); }, /** * Dynamically attach a set of DynVals to a DOM node's properties as * specified by props. * props: {left: someDynVal, style: {backgroundColor: someOtherDynVal}} */ sendProps : function(node, props) { var dynvals = []; var keys = []; var style_keys = []; for (var key in props) { keys.push(key); if (key === 'style') { for (var style_key in props[key]) { style_keys.push(style_key); dynvals.push(props[key][style_key]); node.style[style_key] = props[key][style_key].getValueNow(); } } else { dynvals.push(props[key]); node[key] = props[key].getValueNow(); } } return JX.Reactor.lift(JX.bind(null, function(keys, style_keys, node) { var args = JX.$A(arguments).slice(3); for (var ii = 0; ii < args.length; ii++) { if (keys[ii] === 'style') { for (var jj = 0; jj < style_keys.length; jj++) { node.style[style_keys[jj]] = args[ii]; ii++; } ii--; } else { node[keys[ii]] = args[ii]; } } }, keys, style_keys, node), dynvals); } } }); diff --git a/webroot/rsrc/externals/javelin/ext/view/HTMLView.js b/webroot/rsrc/externals/javelin/ext/view/HTMLView.js index 244b252e05..c9eff564b8 100644 --- a/webroot/rsrc/externals/javelin/ext/view/HTMLView.js +++ b/webroot/rsrc/externals/javelin/ext/view/HTMLView.js @@ -1,137 +1,137 @@ /** * Dumb HTML views. Mostly to demonstrate how the visitor pattern over these * views works, as driven by validation. I'm not convinced it's actually a good * idea to do validation. * * @provides javelin-view-html * @requires javelin-install * javelin-dom * javelin-view-visitor * javelin-util */ JX.install('HTMLView', { extend: 'View', members : { render: function(rendered_children) { return JX.$N(this.getName(), this.getAllAttributes(), rendered_children); }, validate: function() { this.accept(JX.HTMLView.getValidatingVisitor()); } }, statics: { getValidatingVisitor: function() { return new JX.ViewVisitor(JX.HTMLView.validate); }, - validate: function(view, children) { + validate: function(view) { var spec = this._getHTMLSpec(); if (!(view.getName() in spec)) { - throw new Error("invalid tag"); + throw new Error('invalid tag'); } var tag_spec = spec[view.getName()]; var attrs = view.getAllAttributes(); for (var attr in attrs) { if (!(attr in tag_spec)) { - throw new Error("invalid attr"); + throw new Error('invalid attr'); } var validator = tag_spec[attr]; - if (typeof validator === "function") { + if (typeof validator === 'function') { return validator(attrs[attr]); } } return true; }, _validateRel: function(target) { return target in { - "_blank": 1, - "_self": 1, - "_parent": 1, - "_top": 1 + '_blank': 1, + '_self': 1, + '_parent': 1, + '_top': 1 }; }, _getHTMLSpec: function() { var attrs_any_can_have = { className: 1, id: 1, sigil: 1 }; var form_elem_attrs = { name: 1, value: 1 }; var spec = { a: { href: 1, target: JX.HTMLView._validateRel }, b: {}, blockquote: {}, br: {}, button: JX.copy({}, form_elem_attrs), canvas: {}, code: {}, dd: {}, div: {}, dl: {}, dt: {}, em: {}, embed: {}, fieldset: {}, form: { type: 1 }, h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, h6: {}, hr: {}, i: {}, iframe: { src: 1 }, img: { src: 1, alt: 1 }, input: JX.copy({}, form_elem_attrs), label: {'for': 1}, li: {}, ol: {}, optgroup: {}, option: JX.copy({}, form_elem_attrs), p: {}, pre: {}, q: {}, select: {}, span: {}, strong: {}, sub: {}, sup: {}, table: {}, tbody: {}, td: {}, textarea: {}, tfoot: {}, th: {}, thead: {}, tr: {}, ul: {} }; for (var k in spec) { JX.copy(spec[k], attrs_any_can_have); } return spec; }, registerToInterpreter: function(view_interpreter) { var spec = this._getHTMLSpec(); for (var tag in spec) { view_interpreter.register(tag, JX.HTMLView); } return view_interpreter; } } }); diff --git a/webroot/rsrc/externals/javelin/ext/view/ViewInterpreter.js b/webroot/rsrc/externals/javelin/ext/view/ViewInterpreter.js index 743071e101..bb2a5b0bc7 100644 --- a/webroot/rsrc/externals/javelin/ext/view/ViewInterpreter.js +++ b/webroot/rsrc/externals/javelin/ext/view/ViewInterpreter.js @@ -1,71 +1,71 @@ /** * Experimental interpreter for nice views. * This is CoffeeScript: * * d = declare * selectable: false * boxOrientation: Orientation.HORIZONTAL * additionalClasses: ['some-css-class'] * MultiAvatar ref: 'avatars' * div * flex: 1 * div( * span className: 'some-css-class', ref: 'actorTargetLine' * span className: 'message-css', ref: 'message' * ) * * div * boxOrientation: Orientation.HORIZONTAL * className: 'attachment-css-class' * div * className: 'attachment-image-css-class' * ref: 'attachmentImageContainer' * boxOrientation: Orientation.HORIZONTAL * div className: 'inline attachment-text', ref: 'attachmentText', * div * className: 'attachment-title' * ref: 'attachmentTitle' * flex: 1 * div * className: 'attachment-subtitle' * ref: 'attachmentSubtitle' * flex: 1 * div className: 'clear' * MiniUfi ref: 'miniUfi' * FeedbackFlyout ref: 'feedbackFlyout' * * It renders to nested function calls of the form: * view({....options...}, child1, child2, ...); * * This view interpreter is meant to make it work. * * @provides javelin-view-interpreter * @requires javelin-view * javelin-install * javelin-dom */ JX.install('ViewInterpreter', { members : { register : function(name, view_cls) { this[name] = function(/* [properties, ]children... */) { var properties = arguments[0] || {}; var children = Array.prototype.slice.call(arguments, 1); // Passing properties is optional if (properties instanceof JX.View || properties instanceof JX.HTML || properties.nodeType || - typeof properties === "string") { + typeof properties === 'string') { children.unshift(properties); properties = {}; } var result = new view_cls(properties).setName(name); result.addChildren(children); return result; }; } } }); diff --git a/webroot/rsrc/externals/javelin/ext/view/ViewPlaceholder.js b/webroot/rsrc/externals/javelin/ext/view/ViewPlaceholder.js index 7a2b79cfc2..6439e5c796 100644 --- a/webroot/rsrc/externals/javelin/ext/view/ViewPlaceholder.js +++ b/webroot/rsrc/externals/javelin/ext/view/ViewPlaceholder.js @@ -1,105 +1,105 @@ /** * Initialize a client-side view from the server. The main idea here is to * give server-side views a way to integrate with client-side views. * * The idea is that a client-side view will have an accompanying * thin server-side component. The server-side component renders a placeholder * element in the document, and then it will invoke this behavior to initialize * the view into the placeholder. * * Because server-side views may be nested, we need to add complexity to * handle nesting properly. * * Assuming a server-side view design that looks like hierarchical views, * we have to handle structures like * * * * * * * * * * * This leads to a problem: Client component 1 needs to initialize the behavior * with its children, which includes client component 2. So client component * 2 must be rendered first. When client component 2 is rendered, it will also * initialize a copy of this behavior. If behaviors are run in the order they * are initialized, the child component will run before the parent, and its * placeholder won't exist yet. * * To solve this problem, placeholder behaviors are initialized with the token * of a containing view that must be rendered first (if any) and a token * representing it for its own children to depend on. This means the server code * is free to call initBehavior in any order. * * In Phabricator, AphrontJavelinView demonstrates how to handle this correctly. * * config: { * id: Node id to replace. * view: class of view, without the 'JX.' prefix. * params: view parameters * children: messy and loud, cute when drunk * trigger_id: id of containing view that must be rendered first * } * * @provides javelin-behavior-view-placeholder * @requires javelin-behavior * javelin-dom * javelin-view-renderer * javelin-install */ -JX.behavior('view-placeholder', function(config, statics) { +JX.behavior('view-placeholder', function(config) { JX.ViewPlaceholder.register(config.trigger_id, config.id, function() { var replace = JX.$(config.id); var children = config.children; - if (typeof children === "string") { + if (typeof children === 'string') { children = JX.$H(children); } var view = new JX[config.view](config.params, children); var rendered = JX.ViewRenderer.render(view); JX.DOM.replace(replace, rendered); }); }); JX.install('ViewPlaceholder', { statics: { register: function(wait_on_token, token, cb) { var ready_q = []; var waiting; if (!wait_on_token || wait_on_token in JX.ViewPlaceholder.ready) { ready_q.push({token: token, cb: cb}); } else { waiting = JX.ViewPlaceholder.waiting; waiting[wait_on_token] = waiting[wait_on_token] || []; waiting[wait_on_token].push({token: token, cb: cb}); } while(ready_q.length) { var ready = ready_q.shift(); waiting = JX.ViewPlaceholder.waiting[ready.token]; if (waiting) { for (var ii = 0; ii < waiting.length; ii++) { ready_q.push(waiting[ii]); } delete JX.ViewPlaceholder.waiting[ready.token]; } ready.cb(); JX.ViewPlaceholder.ready[token] = true; } }, ready: {}, waiting: {} } }); diff --git a/webroot/rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js b/webroot/rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js index 1e8729e5c4..2247611f31 100644 --- a/webroot/rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js +++ b/webroot/rsrc/externals/javelin/ext/view/__tests__/ViewRenderer.js @@ -1,25 +1,25 @@ /** * @requires javelin-view-renderer * javelin-view */ describe('JX.ViewRenderer', function() { it('should render children then parent', function() { var child_rendered = false; var child_rendered_first = false; var child = new JX.View({}); var parent = new JX.View({}); parent.addChild(child); - child.render = function(_) { + child.render = function() { child_rendered = true; }; - parent.render = function(rendered_children) { + parent.render = function() { child_rendered_first = child_rendered; }; JX.ViewRenderer.render(parent); expect(child_rendered_first).toBe(true); }); }); diff --git a/webroot/rsrc/externals/javelin/lib/Leader.js b/webroot/rsrc/externals/javelin/lib/Leader.js index f259d79105..d6a7ebd5fa 100644 --- a/webroot/rsrc/externals/javelin/lib/Leader.js +++ b/webroot/rsrc/externals/javelin/lib/Leader.js @@ -1,308 +1,307 @@ /** * @requires javelin-install * @provides javelin-leader * @javelin */ /** * Synchronize multiple tabs over LocalStorage. * * This class elects one tab as the "Leader". It remains the leader until it * is closed. * * Tabs can conditionally call a function if they are the leader using * @{method:callIfLeader}. This will trigger leader election, and call the * function if the current tab is elected. This can be used to keep one * websocket open across a group of tabs, or play a sound only once in response * to a server state change. * * Tabs can broadcast messages to other tabs using @{method:broadcast}. Each * message has an optional ID. When a tab receives multiple copies of a message * with the same ID, copies after the first copy are discarded. This can be * used in conjunction with @{method:callIfLeader} to allow multiple event * responders to trigger a reaction to an event (like a sound) and ensure that * it is played only once (not once for each notification), and by only one * tab (not once for each tab). * * Finally, tabs can register a callback which will run if they become the * leading tab, by listening for `onBecomeLeader`. */ JX.install('Leader', { events: ['onBecomeLeader', 'onReceiveBroadcast'], statics: { _interval: null, _broadcastKey: 'JX.Leader.broadcast', _leaderKey: 'JX.Leader.id', /** * Tracks leadership state. Since leadership election is asynchronous, * we can't expose this directly without inconsistent behavior. */ _isLeader: false, /** * Keeps track of message IDs we've seen, so we send each message only * once. */ _seen: {}, /** * Helps keep the list of seen message IDs from growing without bound. */ _seenList: [], /** * Elect a leader, triggering leadership callbacks if they are registered. */ start: function() { var self = JX.Leader; self.callIfLeader(JX.bag); }, /** * Call a method if this tab is the leader. * * This is asynchronous because leadership election is asynchronous. If * the current tab is not the leader after any election takes place, the * callback will not be invoked. */ callIfLeader: function(callback) { JX.Leader._callIf(callback, JX.bag); }, /** * Call a method after leader election. * * This is asynchronous because leadership election is asynchronous. The * callback will be invoked after election takes place. * * This method is useful if you want to invoke a callback no matter what, * but the callback behavior depends on whether this is the leader or * not. */ call: function(callback) { JX.Leader._callIf(callback, callback); }, /** * Elect a leader, then invoke either a leader callback or a follower * callback. */ _callIf: function(leader_callback, follower_callback) { if (!window.localStorage) { // If we don't have localStorage, pretend we're the only tab. self._becomeLeader(); leader_callback(); return; } var self = JX.Leader; // If we don't have an ID for this tab yet, generate one and register // event listeners. if (!self._id) { self._id = 1 + parseInt(Math.random() * 1000000000, 10); JX.Stratcom.listen('pagehide', null, self._pagehide); JX.Stratcom.listen('storage', null, self._storage); } // Read the current leadership lease. var lease = self._read(); // If the lease is good, we're all set. var now = +new Date(); if (lease.until > now) { if (lease.id === self._id) { // If we haven't installed an update timer yet, do so now. This will // renew our lease every 5 seconds, making sure we hold it until the // tab is closed. if (!self._interval && lease.until > now + 10000) { self._interval = window.setInterval(self._write, 5000); } self._becomeLeader(); leader_callback(); } else { follower_callback(); } return; } // If the lease isn't good, try to become the leader. We don't have // proper locking primitives for this, but can do a relatively good // job. The algorithm here is: // // - Write our ID, trying to acquire the lease. // - Delay for much longer than a write "could possibly" take. // - Read the key back. // - If nothing else overwrote the key, we become the leader. // // This avoids a race where our reads and writes could otherwise // interleave with another tab's reads and writes, electing both or // neither as the leader. // // This approximately follows an algorithm attributed to Fischer in // "A Fast Mutual Exclusion Algorithm" (Leslie Lamport, 1985). That // paper also describes a faster (but more complex) algorithm, but // it's not problematic to add a significant delay here because // leader election is not especially performance-sensitive. self._write(); window.setTimeout( JX.bind(null, self._callIf, leader_callback, follower_callback), 50); }, /** * Send a message to all open tabs. * * Tabs can receive messages by listening to `onReceiveBroadcast`. * * @param string|null Message ID. If provided, subsequent messages with * the same ID will be discarded. * @param wild The message to send. */ broadcast: function(id, message) { var self = JX.Leader; if (id !== null) { if (id in self._seen) { return; } self._markSeen(id); } if (window.localStorage) { var json = JX.JSON.stringify( { id: id, message: message, // LocalStorage only emits events if the value changes. Include // a random component to make sure that broadcasts are never // eaten. Although this is probably not often useful in a // production system, it makes testing easier and more predictable. uniq: parseInt(Math.random() * 1000000, 10) }); window.localStorage.setItem(self._broadcastKey, json); } self._receiveBroadcast(message); }, /** * Write a lease which names us as the leader. */ _write: function() { var self = JX.Leader; var str = [self._id, ((+new Date()) + 16000)].join(':'); window.localStorage.setItem(self._leaderKey, str); }, /** * Read the current lease. */ _read: function() { var self = JX.Leader; - leader = window.localStorage.getItem(self._leaderKey) || '0:0'; + var leader = window.localStorage.getItem(self._leaderKey) || '0:0'; leader = leader.split(':'); return { id: parseInt(leader[0], 10), until: parseInt(leader[1], 10) }; }, /** * When the tab is closed, if we're the leader, release leadership. * * This will trigger a new election if there are other tabs open. */ _pagehide: function() { var self = JX.Leader; if (self._read().id === self._id) { window.localStorage.removeItem(self._leaderKey); } }, /** * React to a storage update. */ _storage: function(e) { var self = JX.Leader; var key = e.getRawEvent().key; var new_value = e.getRawEvent().newValue; switch (key) { case self._broadcastKey: new_value = JX.JSON.parse(new_value); if (new_value.id !== null) { if (new_value.id in self._seen) { return; } self._markSeen(new_value.id); } self._receiveBroadcast(new_value.message); break; case self._leaderKey: // If the leader tab closed, elect a new leader. if (new_value === null) { self.callIfLeader(JX.bag); } break; } }, _receiveBroadcast: function(message) { var self = JX.Leader; new JX.Leader().invoke('onReceiveBroadcast', message, self._isLeader); }, _becomeLeader: function() { var self = JX.Leader; if (self._isLeader) { return; } self._isLeader = true; new JX.Leader().invoke('onBecomeLeader'); }, /** * Mark a message as seen. * * We keep a fixed-sized list of recent messages, and let old ones fall * off the end after a while. */ _markSeen: function(id) { var self = JX.Leader; self._seen[id] = true; self._seenList.push(id); while (self._seenList.length > 128) { delete self._seen[self._seenList[0]]; self._seenList.splice(0, 1); } } } }); - diff --git a/webroot/rsrc/externals/javelin/lib/Request.js b/webroot/rsrc/externals/javelin/lib/Request.js index 14d13905d0..50e044b038 100644 --- a/webroot/rsrc/externals/javelin/lib/Request.js +++ b/webroot/rsrc/externals/javelin/lib/Request.js @@ -1,482 +1,482 @@ /** * @requires javelin-install * javelin-stratcom * javelin-util * javelin-behavior * javelin-json * javelin-dom * javelin-resource * javelin-routable * @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 : ['start', 'open', 'send', 'statechange', 'done', 'error', 'finally', 'uploadprogress'], members : { _xhrkey : null, _transport : null, _sent : false, _finished : false, _block : null, _data : null, _getSameOriginTransport : function() { try { try { return new XMLHttpRequest(); } catch (x) { - return new ActiveXObject("Msxml2.XMLHTTP"); + return new ActiveXObject('Msxml2.XMLHTTP'); } } catch (x) { - return new ActiveXObject("Microsoft.XMLHTTP"); + return new ActiveXObject('Microsoft.XMLHTTP'); } }, _getCORSTransport : function() { try { var xport = new XMLHttpRequest(); if ('withCredentials' in xport) { // XHR supports CORS } else if (typeof XDomainRequest != 'undefined') { xport = new XDomainRequest(); } return xport; } catch (x) { return new XDomainRequest(); } }, getTransport : function() { if (!this._transport) { this._transport = this.getCORS() ? this._getCORSTransport() : this._getSameOriginTransport(); } return this._transport; }, getRoutable: function() { var routable = new JX.Routable(); routable.listen('start', JX.bind(this, function() { // Pass the event to allow other listeners to "start" to configure this // request before it fires. JX.Stratcom.pass(JX.Stratcom.context()); this.send(); })); this.listen('finally', JX.bind(routable, routable.done)); return routable; }, send : function() { if (this._sent || this._finished) { if (__DEV__) { if (this._sent) { JX.$E( 'JX.Request.send(): ' + 'attempting to send a Request that has already been sent.'); } if (this._finished) { JX.$E( 'JX.Request.send(): ' + 'attempting to send a Request that has finished or aborted.'); } } return; } // Fire the "start" event before doing anything. A listener may // perform pre-processing or validation on this request this.invoke('start', this); if (this._finished) { return; } var xport = this.getTransport(); xport.onreadystatechange = JX.bind(this, this._onreadystatechange); if (xport.upload) { xport.upload.onprogress = JX.bind(this, this._onuploadprogress); } var method = this.getMethod().toUpperCase(); if (__DEV__) { if (this.getRawData()) { if (method != 'POST') { JX.$E( 'JX.Request.send(): ' + 'attempting to send post data over GET. You must use POST.'); } } } var list_of_pairs = this._data || []; list_of_pairs.push(['__ajax__', true]); this._block = JX.Stratcom.allocateMetadataBlock(); list_of_pairs.push(['__metablock__', this._block]); var q = (this.getDataSerializer() || JX.Request.defaultDataSerializer)(list_of_pairs); var uri = this.getURI(); // If we're sending a file, submit the metadata via the URI instead of // via the request body, because the entire post body will be consumed by // the file content. if (method == 'GET' || this.getRawData()) { uri += ((uri.indexOf('?') === -1) ? '?' : '&') + q; } if (this.getTimeout()) { this._timer = setTimeout( JX.bind( this, this._fail, JX.Request.ERROR_TIMEOUT), this.getTimeout()); } xport.open(method, uri, true); // Must happen after xport.open so that listeners can modify the transport // Some transport properties can only be set after the transport is open this.invoke('open', this); if (this._finished) { return; } this.invoke('send', this); if (this._finished) { return; } if (method == 'POST') { if (this.getRawData()) { xport.send(this.getRawData()); } else { xport.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded'); xport.send(q); } } else { xport.send(null); } this._sent = true; }, abort : function() { this._cleanup(); }, _onuploadprogress : function(progress) { this.invoke('uploadprogress', progress); }, _onreadystatechange : function() { var xport = this.getTransport(); var response; try { this.invoke('statechange', this); if (this._finished) { return; } if (xport.readyState != 4) { return; } // XHR requests to 'file:///' domains return 0 for success, which is why // we treat it as a good result in addition to HTTP 2XX responses. if (xport.status !== 0 && (xport.status < 200 || xport.status >= 300)) { this._fail(); return; } if (__DEV__) { var expect_guard = this.getExpectCSRFGuard(); if (!xport.responseText.length) { JX.$E( 'JX.Request("'+this.getURI()+'", ...): '+ 'server returned an empty response.'); } if (expect_guard && xport.responseText.indexOf('for (;;);') !== 0) { JX.$E( 'JX.Request("'+this.getURI()+'", ...): '+ 'server returned an invalid response.'); } if (expect_guard && xport.responseText == 'for (;;);') { JX.$E( 'JX.Request("'+this.getURI()+'", ...): '+ 'server returned an empty response.'); } } response = this._extractResponse(xport); if (!response) { JX.$E( 'JX.Request("'+this.getURI()+'", ...): '+ 'server returned an invalid response.'); } } catch (exception) { if (__DEV__) { JX.log( 'JX.Request("'+this.getURI()+'", ...): '+ 'caught exception processing response: '+exception); } this._fail(); return; } try { this._handleResponse(response); this._cleanup(); } catch (exception) { // In Firefox+Firebug, at least, something eats these. :/ setTimeout(function() { throw exception; }, 0); } }, _extractResponse : function(xport) { var text = xport.responseText; if (this.getExpectCSRFGuard()) { text = text.substring('for (;;);'.length); } var type = this.getResponseType().toUpperCase(); if (type == 'TEXT') { return text; } else if (type == 'JSON' || type == 'JAVELIN') { return JX.JSON.parse(text); } else if (type == 'XML') { var doc; try { if (typeof DOMParser != 'undefined') { var parser = new DOMParser(); - doc = parser.parseFromString(text, "text/xml"); + doc = parser.parseFromString(text, 'text/xml'); } else { // IE // an XDomainRequest - doc = new ActiveXObject("Microsoft.XMLDOM"); + doc = new ActiveXObject('Microsoft.XMLDOM'); doc.async = false; doc.loadXML(xport.responseText); } return doc.documentElement; } catch (exception) { if (__DEV__) { JX.log( 'JX.Request("'+this.getURI()+'", ...): '+ 'caught exception extracting response: '+exception); } this._fail(); return null; } } if (__DEV__) { JX.$E( 'JX.Request("'+this.getURI()+'", ...): '+ 'unrecognized response type.'); } return null; }, _fail : function(error) { this._cleanup(); this.invoke('error', error, this); this.invoke('finally'); }, _done : function(response) { this._cleanup(); if (response.onload) { for (var ii = 0; ii < response.onload.length; ii++) { (new Function(response.onload[ii]))(); } } var payload; if (this.getRaw()) { payload = response; } else { payload = response.payload; JX.Request._parseResponsePayload(payload); } this.invoke('done', payload, this); this.invoke('finally'); }, _cleanup : function() { this._finished = true; clearTimeout(this._timer); this._timer = null; // Should not abort the transport request if it has already completed // Otherwise, we may see an "HTTP request aborted" error in the console // despite it possibly having succeeded. if (this._transport && this._transport.readyState != 4) { this._transport.abort(); } }, setData : function(dictionary) { this._data = null; this.addData(dictionary); return this; }, addData : function(dictionary) { if (!this._data) { this._data = []; } for (var k in dictionary) { this._data.push([k, dictionary[k]]); } return this; }, setDataWithListOfPairs : function(list_of_pairs) { this._data = list_of_pairs; return this; }, _handleResponse : function(response) { if (this.getResponseType().toUpperCase() == 'JAVELIN') { if (response.error) { this._fail(response.error); } else { JX.Stratcom.mergeData( this._block, response.javelin_metadata || {}); var when_complete = JX.bind(this, function() { this._done(response); JX.initBehaviors(response.javelin_behaviors || {}); }); if (response.javelin_resources) { JX.Resource.load(response.javelin_resources, when_complete); } else { when_complete(); } } } else { this._cleanup(); this.invoke('done', response, this); this.invoke('finally'); } } }, statics : { ERROR_TIMEOUT : -9000, defaultDataSerializer : function(list_of_pairs) { var uri = []; for (var ii = 0; ii < list_of_pairs.length; ii++) { var pair = list_of_pairs[ii]; var name = encodeURIComponent(pair[0]); var value = encodeURIComponent(pair[1]); uri.push(name + '=' + value); } return uri.join('&'); }, /** * When we receive a JSON blob, parse it to introduce meaningful objects * where there are magic keys for placeholders. * * Objects with the magic key '__html' are translated into JX.HTML objects. * * This function destructively modifies its input. */ _parseResponsePayload: function(parent, index) { var recurse = JX.Request._parseResponsePayload; var obj = (typeof index !== 'undefined') ? parent[index] : parent; if (JX.isArray(obj)) { for (var ii = 0; ii < obj.length; ii++) { recurse(obj, ii); } } else if (obj && typeof obj == 'object') { if (('__html' in obj) && (obj.__html !== null)) { parent[index] = JX.$H(obj.__html); } else { for (var key in obj) { recurse(obj, key); } } } } }, properties : { URI : null, dataSerializer : null, /** * Configure which HTTP method to use for the request. Permissible values * are "POST" (default) or "GET". * * @param string HTTP method, one of "POST" or "GET". */ method : 'POST', /** * Set the data parameter of transport.send. Useful if you want to send a * file or FormData. Not that you cannot send raw data and data at the same * time. * * @param Data, argument to transport.send */ rawData: null, raw : false, /** * Configure a timeout, in milliseconds. If the request has not resolved * (either with success or with an error) within the provided timeframe, * it will automatically fail with error JX.Request.ERROR_TIMEOUT. * * @param int Timeout, in milliseconds (e.g. 3000 = 3 seconds). */ timeout : null, /** * Whether or not we should expect the CSRF guard in the response. * * @param bool */ expectCSRFGuard : true, /** * Whether it should be a CORS (Cross-Origin Resource Sharing) request to * a third party domain other than the current site. * * @param bool */ CORS : false, /** * Type of the response. * * @param enum 'JAVELIN', 'JSON', 'XML', 'TEXT' */ responseType : 'JAVELIN' } }); diff --git a/webroot/rsrc/externals/javelin/lib/Resource.js b/webroot/rsrc/externals/javelin/lib/Resource.js index c5133ab6a8..4e7d528740 100644 --- a/webroot/rsrc/externals/javelin/lib/Resource.js +++ b/webroot/rsrc/externals/javelin/lib/Resource.js @@ -1,185 +1,185 @@ /** * @provides javelin-resource * @requires javelin-util * javelin-uri * javelin-install * * @javelin */ JX.install('Resource', { statics: { _loading: {}, _loaded: {}, _links: [], _callbacks: [], /** * Loads one or many static resources (JavaScript or CSS) and executes a * callback once these resources have finished loading. * * @param string|array static resource or list of resources to be loaded * @param function callback when resources have finished loading */ load: function(list, callback) { var resources = {}, - uri, resource, path, type; + uri, resource, path; list = JX.$AX(list); // In the event there are no resources to wait on, call the callback and // exit. NOTE: it's better to do this check outside this function and not // call through JX.Resource, but it's not always easy/possible to do so if (!list.length) { setTimeout(callback, 0); return; } for (var ii = 0; ii < list.length; ii++) { uri = new JX.URI(list[ii]); resource = uri.toString(); path = uri.getPath(); resources[resource] = true; if (JX.Resource._loaded[resource]) { setTimeout(JX.bind(JX.Resource, JX.Resource._complete, resource), 0); } else if (!JX.Resource._loading[resource]) { JX.Resource._loading[resource] = true; if (path.indexOf('.css') == path.length - 4) { JX.Resource._loadCSS(resource); } else { JX.Resource._loadJS(resource); } } } JX.Resource._callbacks.push({ resources: resources, callback: callback }); }, _loadJS: function(uri) { var script = document.createElement('script'); var load_callback = function() { JX.Resource._complete(uri); }; var error_callback = function() { JX.$E('Resource: JS file download failure: ' + uri); }; JX.copy(script, { type: 'text/javascript', src: uri }); script.onload = load_callback; script.onerror = error_callback; script.onreadystatechange = function() { var state = this.readyState; if (state == 'complete' || state == 'loaded') { load_callback(); } }; document.getElementsByTagName('head')[0].appendChild(script); }, _loadCSS: function(uri) { var link = JX.copy(document.createElement('link'), { type: 'text/css', rel: 'stylesheet', href: uri, 'data-href': uri // don't trust href }); document.getElementsByTagName('head')[0].appendChild(link); JX.Resource._links.push(link); if (!JX.Resource._timer) { JX.Resource._timer = setInterval(JX.Resource._poll, 20); } }, _poll: function() { var sheets = document.styleSheets, ii = sheets.length, links = JX.Resource._links; // Cross Origin CSS loading // http://yearofmoo.com/2011/03/cross-browser-stylesheet-preloading/ while (ii--) { var link = sheets[ii], owner = link.ownerNode || link.owningElement, jj = links.length; if (owner) { while (jj--) { if (owner == links[jj]) { JX.Resource._complete(links[jj]['data-href']); links.splice(jj, 1); } } } } if (!links.length) { clearInterval(JX.Resource._timer); JX.Resource._timer = null; } }, _complete: function(uri) { var list = JX.Resource._callbacks, current, ii; delete JX.Resource._loading[uri]; JX.Resource._loaded[uri] = true; var errors = []; for (ii = 0; ii < list.length; ii++) { current = list[ii]; delete current.resources[uri]; if (!JX.Resource._hasResources(current.resources)) { try { current.callback(); } catch (error) { errors.push(error); } list.splice(ii--, 1); } } if (errors.length) { throw errors[0]; } }, _hasResources: function(resources) { for (var hasResources in resources) { return true; } return false; } }, initialize: function() { var list = JX.$A(document.getElementsByTagName('link')), ii = list.length, node; while ((node = list[--ii])) { if (node.type == 'text/css' && node.href) { JX.Resource._loaded[(new JX.URI(node.href)).toString()] = true; } } list = JX.$A(document.getElementsByTagName('script')); ii = list.length; while ((node = list[--ii])) { if (node.type == 'text/javascript' && node.src) { JX.Resource._loaded[(new JX.URI(node.src)).toString()] = true; } } } }); diff --git a/webroot/rsrc/externals/javelin/lib/WebSocket.js b/webroot/rsrc/externals/javelin/lib/WebSocket.js index a7f5d1a864..8fd426f4bc 100644 --- a/webroot/rsrc/externals/javelin/lib/WebSocket.js +++ b/webroot/rsrc/externals/javelin/lib/WebSocket.js @@ -1,178 +1,178 @@ /** * @requires javelin-install * @provides javelin-websocket * @javelin */ /** * Wraps a WebSocket. */ JX.install('WebSocket', { construct: function(uri) { this.setURI(uri); }, properties: { URI: null, /** * Called when a connection is established or re-established after an * interruption. */ openHandler: null, /** * Called when a message is received. */ messageHandler: null, /** * Called when the connection is closed. * * You can return `true` to prevent the socket from reconnecting. */ closeHandler: null }, members: { /** * The underlying WebSocket. */ _socket: null, /** * Is the socket connected? */ _isOpen: false, /** * Has the caller asked us to close? * * By default, we reconnect when the connection is interrupted. * This stops us from reconnecting if @{method:close} was called. */ _shouldClose: false, /** * Number of milliseconds to wait after a connection failure before * attempting to reconnect. */ _delayUntilReconnect: null, /** * Open the connection. */ open: function() { if (!window.WebSocket) { return; } this._shouldClose = false; this._resetDelay(); this._socket = new WebSocket(this.getURI()); this._socket.onopen = JX.bind(this, this._onopen); this._socket.onmessage = JX.bind(this, this._onmessage); this._socket.onclose = JX.bind(this, this._onclose); }, /** * Send a message. * * If the connection is not currently open, this method has no effect and * the messages vanishes into the ether. */ send: function(message) { if (this._isOpen) { this._socket.send(message); } }, /** * Close the connection. */ close: function() { if (!this._isOpen) { return; } this._shouldClose = true; this._socket.close(); }, /** * Callback for connection open. */ - _onopen: function(e) { + _onopen: function() { this._isOpen = true; // Reset the reconnect delay, since we connected successfully. this._resetDelay(); var handler = this.getOpenHandler(); if (handler) { handler(); } }, /** * Reset the reconnect delay to its base value. */ _resetDelay: function() { this._delayUntilReconnect = 2000; }, /** * Callback for message received. */ _onmessage: function(e) { var data = e.data; var handler = this.getMessageHandler(); if (handler) { handler(data); } }, /** * Callback for connection close. */ - _onclose: function(e) { + _onclose: function() { this._isOpen = false; var done = false; var handler = this.getCloseHandler(); if (handler) { done = handler(); } // If we didn't explicitly see a close() call and the close handler // did not return `true` to stop the reconnect, wait a little while // and try to reconnect. if (!done && !this._shouldClose) { setTimeout(JX.bind(this, this._reconnect), this._delayUntilReconnect); } }, /** * Reconnect an interrupted socket. */ _reconnect: function() { // Increase the reconnect delay by a factor of 2. If we fail to open the // connection, the close handler will send us back here. We'll reconnect // more and more slowly until we eventually get a valid connection. this._delayUntilReconnect = this._delayUntilReconnect * 2; this.open(); } } }); diff --git a/webroot/rsrc/externals/javelin/lib/__tests__/JSON.js b/webroot/rsrc/externals/javelin/lib/__tests__/JSON.js index 539a41610e..05b236307d 100644 --- a/webroot/rsrc/externals/javelin/lib/__tests__/JSON.js +++ b/webroot/rsrc/externals/javelin/lib/__tests__/JSON.js @@ -1,36 +1,36 @@ /** * @requires javelin-json */ describe('JSON', function() { it('should encode and decode an object', function() { var object = { a: [0, 1, 2], - s: "Javelin Stuffs", + s: 'Javelin Stuffs', u: '\x01', n: 1, f: 3.14, b: false, nil: null, o: { a: 1, b: [1, 2], c: { a: 2, b: 3 } } }; expect(JX.JSON.parse(JX.JSON.stringify(object))).toEqual(object); }); it('should encode undefined array indices as null', function() { var a = []; a.length = 2; var o = { x : a }; expect(JX.JSON.stringify(o)).toEqual('{"x":[null,null]}'); }); }); diff --git a/webroot/rsrc/externals/javelin/lib/__tests__/URI.js b/webroot/rsrc/externals/javelin/lib/__tests__/URI.js index a162ad59b8..382ba3ca7e 100644 --- a/webroot/rsrc/externals/javelin/lib/__tests__/URI.js +++ b/webroot/rsrc/externals/javelin/lib/__tests__/URI.js @@ -1,302 +1,302 @@ /** * @requires javelin-uri javelin-php-serializer */ describe('Javelin URI', function() { it('should understand parts of a uri', function() { var uri = JX.$U('http://www.facebook.com:123/home.php?key=value#fragment'); expect(uri.getProtocol()).toEqual('http'); expect(uri.getDomain()).toEqual('www.facebook.com'); expect(uri.getPort()).toEqual('123'); expect(uri.getPath()).toEqual('/home.php'); expect(uri.getQueryParams()).toEqual({'key' : 'value'}); expect(uri.getFragment()).toEqual('fragment'); }); it('can accept null as uri string', function() { var uri = JX.$U(null); expect(uri.getProtocol()).toEqual(undefined); expect(uri.getDomain()).toEqual(undefined); expect(uri.getPath()).toEqual(undefined); expect(uri.getQueryParams()).toEqual({}); expect(uri.getFragment()).toEqual(undefined); expect(uri.toString()).toEqual(''); }); it('can accept empty string as uri string', function() { var uri = JX.$U(''); expect(uri.getProtocol()).toEqual(undefined); expect(uri.getDomain()).toEqual(undefined); expect(uri.getPath()).toEqual(undefined); expect(uri.getQueryParams()).toEqual({}); expect(uri.getFragment()).toEqual(undefined); expect(uri.toString()).toEqual(''); }); it('should understand relative uri', function() { var uri = JX.$U('/home.php?key=value#fragment'); expect(uri.getProtocol()).toEqual(undefined); expect(uri.getDomain()).toEqual(undefined); expect(uri.getPath()).toEqual('/home.php'); expect(uri.getQueryParams()).toEqual({'key' : 'value'}); expect(uri.getFragment()).toEqual('fragment'); }); function charRange(from, to) { - res = ''; + var res = ''; for (var i = from.charCodeAt(0); i <= to.charCodeAt(0); i++) { res += String.fromCharCode(i); } return res; } it('should reject unsafe domains', function() { var unsafe_chars = '\x00;\\%\u2047\u2048\ufe56\ufe5f\uff03\uff0f\uff1f' + charRange('\ufdd0', '\ufdef') + charRange('\ufff0', '\uffff'); for (var i = 0; i < unsafe_chars.length; i++) { expect(function() { JX.$U('http://foo' + unsafe_chars.charAt(i) + 'bar'); }).toThrow(); } }); it('should allow safe domains', function() { var safe_chars = '-._' + charRange('a', 'z') + charRange('A', 'Z') + charRange('0', '9') + '\u2046\u2049\ufdcf\ufdf0\uffef'; for (var i = 0; i < safe_chars.length; i++) { var domain = 'foo' + safe_chars.charAt(i) + 'bar'; var uri = JX.$U('http://' + domain); expect(uri.getDomain()).toEqual(domain); } }); it('should set slash as the default path', function() { var uri = JX.$U('http://www.facebook.com'); expect(uri.getPath()).toEqual('/'); }); it('should set empty map as the default query data', function() { var uri = JX.$U('http://www.facebook.com/'); expect(uri.getQueryParams()).toEqual({}); }); it('should set undefined as the default fragment', function() { var uri = JX.$U('http://www.facebook.com/'); expect(uri.getFragment()).toEqual(undefined); }); it('should understand uri with no path', function() { var uri = JX.$U('http://www.facebook.com?key=value'); expect(uri.getPath()).toEqual('/'); expect(uri.getQueryParams()).toEqual({'key' : 'value'}); }); it('should understand multiple query keys', function() { var uri = JX.$U('/?clown=town&herp=derp'); expect(uri.getQueryParams()).toEqual({ 'clown' : 'town', 'herp' : 'derp' }); }); it('does not set keys for nonexistant data', function() { var uri = JX.$U('/?clown=town'); expect(uri.getQueryParams().herp).toEqual(undefined); }); it('does not parse different types of query data', function() { var uri = JX.$U('/?str=string&int=123&bool=true&badbool=false&raw'); expect(uri.getQueryParams()).toEqual({ 'str' : 'string', 'int' : '123', 'bool' : 'true', 'badbool' : 'false', 'raw' : '' }); }); it('should act as string', function() { var string = 'http://www.facebook.com/home.php?key=value'; var uri = JX.$U(string); expect(uri.toString()).toEqual(string); expect('' + uri).toEqual(string); }); it('can remove path', function() { var uri = JX.$U('http://www.facebook.com/home.php?key=value'); uri.setPath(undefined); expect(uri.getPath()).toEqual(undefined); expect(uri.toString()).toEqual('http://www.facebook.com/?key=value'); }); it('can remove queryData by undefining it', function() { var uri = JX.$U('http://www.facebook.com/home.php?key=value'); uri.setQueryParams(undefined); expect(uri.getQueryParams()).toEqual(undefined); expect(uri.toString()).toEqual('http://www.facebook.com/home.php'); }); it('can remove queryData by replacing it', function() { var uri = JX.$U('http://www.facebook.com/home.php?key=value'); uri.setQueryParams({}); expect(uri.getQueryParams()).toEqual({}); expect(uri.toString()).toEqual('http://www.facebook.com/home.php'); }); it('can amend to removed queryData', function() { var uri = JX.$U('http://www.facebook.com/home.php?key=value'); uri.setQueryParams({}); expect(uri.getQueryParams()).toEqual({}); uri.addQueryParams({'herp' : 'derp'}); expect(uri.getQueryParams()).toEqual({'herp' : 'derp'}); expect(uri.toString()).toEqual( 'http://www.facebook.com/home.php?herp=derp'); }); it('should properly decode entities', function() { var uri = JX.$U('/?from=clown+town&to=cloud%20city&pass=cloud%2Bcountry'); expect(uri.getQueryParams()).toEqual({ 'from' : 'clown town', 'to' : 'cloud city', 'pass' : 'cloud+country' }); expect(uri.toString()).toEqual( '/?from=clown%20town&to=cloud%20city&pass=cloud%2Bcountry'); }); it('can add query data', function() { var uri = JX.$U('http://www.facebook.com/'); uri.addQueryParams({'key' : 'value'}); expect(uri.getQueryParams()).toEqual({'key' : 'value'}); expect(uri.toString()).toEqual('http://www.facebook.com/?key=value'); uri.setQueryParam('key', 'lock'); expect(uri.getQueryParams()).toEqual({'key' : 'lock'}); expect(uri.toString()).toEqual('http://www.facebook.com/?key=lock'); }); it('can add different types of query data', function() { var uri = new JX.URI(); uri.setQueryParams({ 'str' : 'string', 'int' : 123, 'bool' : true, 'badbool' : false, 'raw' : '' }); expect(uri.toString()).toEqual( '?str=string&int=123&bool=true&badbool=false&raw'); }); it('should properly encode entities in added query data', function() { var uri = new JX.URI(); uri.addQueryParams({'key' : 'two words'}); expect(uri.getQueryParams()).toEqual({'key' : 'two words'}); expect(uri.toString()).toEqual('?key=two%20words'); }); it('can add multiple query data', function() { var uri = JX.$U('http://www.facebook.com/'); uri.addQueryParams({ 'clown' : 'town', 'herp' : 'derp' }); expect(uri.getQueryParams()).toEqual({ 'clown' : 'town', 'herp' : 'derp' }); expect(uri.toString()).toEqual( 'http://www.facebook.com/?clown=town&herp=derp'); }); it('can append to existing query data', function() { var uri = JX.$U('/?key=value'); uri.addQueryParams({'clown' : 'town'}); expect(uri.getQueryParams()).toEqual({ 'key' : 'value', 'clown' : 'town' }); expect(uri.toString()).toEqual('/?key=value&clown=town'); }); it('can merge with existing query data', function() { var uri = JX.$U('/?key=value&clown=town'); uri.addQueryParams({ 'clown' : 'ville', 'herp' : 'derp' }); expect(uri.getQueryParams()).toEqual({ 'key' : 'value', 'clown' : 'ville', 'herp' : 'derp' }); expect(uri.toString()).toEqual('/?key=value&clown=ville&herp=derp'); }); it('can replace query data', function() { var uri = JX.$U('/?key=value&clown=town'); uri.setQueryParams({'herp' : 'derp'}); expect(uri.getQueryParams()).toEqual({'herp' : 'derp'}); expect(uri.toString()).toEqual('/?herp=derp'); }); it('can remove query data', function() { var uri = JX.$U('/?key=value&clown=town'); uri.addQueryParams({'key' : null}); expect(uri.getQueryParams()).toEqual({ 'clown' : 'town', 'key' : null }); expect(uri.toString()).toEqual('/?clown=town'); }); it('can remove multiple query data', function() { var uri = JX.$U('/?key=value&clown=town&herp=derp'); uri.addQueryParams({'key' : null, 'herp' : undefined}); expect(uri.getQueryParams()).toEqual({ 'clown' : 'town', 'key' : null, 'herp' : undefined }); expect(uri.toString()).toEqual('/?clown=town'); }); it('can remove non existent query data', function() { var uri = JX.$U('/?key=value'); uri.addQueryParams({'magic' : null}); expect(uri.getQueryParams()).toEqual({ 'key' : 'value', 'magic' : null }); expect(uri.toString()).toEqual('/?key=value'); }); it('can build uri from scratch', function() { var uri = new JX.URI(); uri.setProtocol('http'); uri.setDomain('www.facebook.com'); uri.setPath('/home.php'); uri.setQueryParams({'key' : 'value'}); uri.setFragment('fragment'); expect(uri.toString()).toEqual( 'http://www.facebook.com/home.php?key=value#fragment'); }); it('no global state interference', function() { var uri1 = JX.$U('/?key=value'); var uri2 = JX.$U(); expect(uri2.getQueryParams()).not.toEqual({'key' : 'value'}); }); it('should not loop indefinitely when parsing empty params', function() { expect(JX.$U('/?&key=value').getQueryParams()).toEqual({'key' : 'value'}); expect(JX.$U('/?&&&key=value').getQueryParams()).toEqual({'key' : 'value'}); expect(JX.$U('/?&&').getQueryParams()).toEqual({}); }); it('should parse values with =', function() { expect(JX.$U('/?x=1=1').getQueryParams()).toEqual({'x' : '1=1'}); }); }); diff --git a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js index dc516f3de5..fd5aae35d8 100644 --- a/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js +++ b/webroot/rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js @@ -1,439 +1,435 @@ /** * @requires javelin-dom * javelin-util * javelin-stratcom * javelin-install * @provides javelin-tokenizer * @javelin */ /** * A tokenizer is a UI component similar to a text input, except that it * allows the user to input a list of items ("tokens"), generally from a fixed * set of results. A familiar example of this UI is the "To:" field of most * email clients, where the control autocompletes addresses from the user's * address book. * * @{JX.Tokenizer} is built on top of @{JX.Typeahead}, and primarily adds the * ability to choose multiple items. * * To build a @{JX.Tokenizer}, you need to do four things: * * 1. Construct it, padding a DOM node for it to attach to. See the constructor * for more information. * 2. Build a {@JX.Typeahead} and configure it with setTypeahead(). * 3. Configure any special options you want. * 4. Call start(). * * If you do this correctly, the input should suggest items and enter them as * tokens as the user types. * * When the tokenizer is focused, the CSS class `jx-tokenizer-container-focused` * is added to the container node. */ JX.install('Tokenizer', { construct : function(containerNode) { this._containerNode = containerNode; }, events : [ /** * Emitted when the value of the tokenizer changes, similar to an 'onchange' * from a ##. The typeahead's dropdown suggestions will be appended to the * hardpoint in the DOM. Basically, this is the bare minimum requirement: * * LANG=HTML *
* *
* * Then get a reference to the ##
## and pass it as 'hardpoint', and pass * the #### as 'control'. This will enhance your boring old * #### with amazing typeahead powers. * * On the Facebook/Tools stack, #### can build * this for you. * * @param Node "Hardpoint", basically an anchorpoint in the document which * the typeahead can append its suggestion menu to. * @param Node? Actual #### to use; if not provided, the typeahead * will just look for a (solitary) input inside the hardpoint. * @task build */ construct : function(hardpoint, control) { this._hardpoint = hardpoint; this._control = control || JX.DOM.find(hardpoint, 'input'); this._root = JX.$N( 'div', {className: 'jx-typeahead-results'}); this._display = []; this._listener = JX.DOM.listen( this._control, ['focus', 'blur', 'keypress', 'keydown', 'input'], null, JX.bind(this, this.handleEvent)); JX.DOM.listen( this._root, ['mouseover', 'mouseout'], null, JX.bind(this, this._onmouse)); JX.DOM.listen( this._root, 'mousedown', 'tag:a', JX.bind(this, function(e) { if (!e.isRightButton()) { this._choose(e.getNode('tag:a')); } })); }, events : ['choose', 'query', 'start', 'change', 'show'], properties : { /** * Boolean. If true (default), the user is permitted to submit the typeahead * with a custom or empty selection. This is a good behavior if the * typeahead is attached to something like a search input, where the user * might type a freeform query or select from a list of suggestions. * However, sometimes you require a specific input (e.g., choosing which * user owns something), in which case you can prevent null selections. * * @task config */ allowNullSelection : true }, members : { _root : null, _control : null, _hardpoint : null, _listener : null, _value : null, _stop : false, _focus : -1, _focused : false, _placeholderVisible : false, _placeholder : null, _display : null, _datasource : null, _waitingListener : null, _readyListener : null, _completeListener : null, /** * Activate your properly configured typeahead. It won't do anything until * you call this method! * * @task start * @return void */ start : function() { this.invoke('start'); if (__DEV__) { if (!this._datasource) { throw new Error( - "JX.Typeahead.start(): " + - "No datasource configured. Create a datasource and call " + - "setDatasource()."); + 'JX.Typeahead.start(): ' + + 'No datasource configured. Create a datasource and call ' + + 'setDatasource().'); } } this.updatePlaceholder(); }, /** * Configure a datasource, which is where the Typeahead gets suggestions * from. See @{JX.TypeaheadDatasource} for more information. You must * provide exactly one datasource. * * @task datasource * @param JX.TypeaheadDatasource The datasource which the typeahead will * draw from. */ setDatasource : function(datasource) { if (this._datasource) { this._datasource.unbindFromTypeahead(); this._waitingListener.remove(); this._readyListener.remove(); this._completeListener.remove(); } this._waitingListener = datasource.listen( 'waiting', JX.bind(this, this.waitForResults)); this._readyListener = datasource.listen( 'resultsready', JX.bind(this, this.showResults)); this._completeListener = datasource.listen( 'complete', JX.bind(this, this.doneWaitingForResults)); datasource.bindToTypeahead(this); this._datasource = datasource; }, getDatasource : function() { return this._datasource; }, /** * Override the selected in the constructor with some other input. * This is primarily useful when building a control on top of the typeahead, * like @{JX.Tokenizer}. * * @task config * @param node An node to use as the primary control. */ setInputNode : function(input) { this._control = input; return this; }, /** * Hide the typeahead's dropdown suggestion menu. * * @task control * @return void */ hide : function() { this._changeFocus(Number.NEGATIVE_INFINITY); this._display = []; this._moused = false; JX.DOM.hide(this._root); }, /** * Show a given result set in the typeahead's dropdown suggestion menu. * Normally, you don't call this method directly. Usually it gets called * in response to events from the datasource you have configured. * * @task control * @param list List of #### tags to show as suggestions/results. * @param string The query this result list corresponds to. * @return void */ showResults : function(results, value) { if (value != this._value) { // This result list is for an old query, and no longer represents // the input state of the typeahead. // For example, the user may have typed "dog", and then they delete // their query and type "cat", and then the "dog" results arrive from // the source. // Another case is that the user made a selection in a tokenizer, // and then results returned. However, the typeahead is now empty, and // we don't want to pop it back open. // In all cases, just throw these results away. They are no longer // relevant. return; } var obj = {show: results}; var e = this.invoke('show', obj); // If the user has an element focused, store the value before we redraw. // After we redraw, try to select the same element if it still exists in // the list. This prevents redraws from disrupting keyboard element // selection. var old_focus = null; if (this._focus >= 0 && this._display[this._focus]) { old_focus = this._display[this._focus].name; } // Note that the results list may have been update by the "show" event // listener. Non-result node (e.g. divider or label) may have been // inserted. JX.DOM.setContent(this._root, results); this._display = JX.DOM.scry(this._root, 'a', 'typeahead-result'); if (this._display.length && !e.getPrevented()) { this._changeFocus(Number.NEGATIVE_INFINITY); var d = JX.Vector.getDim(this._hardpoint); d.x = 0; d.setPos(this._root); if (this._root.parentNode !== this._hardpoint) { this._hardpoint.appendChild(this._root); } JX.DOM.show(this._root); // If we had a node focused before, look for a node with the same value // and focus it. if (old_focus !== null) { for (var ii = 0; ii < this._display.length; ii++) { if (this._display[ii].name == old_focus) { this._focus = ii; this._drawFocus(); break; } } } } else { this.hide(); JX.DOM.setContent(this._root, null); } }, refresh : function() { if (this._stop) { return; } this._value = this._control.value; this.invoke('change', this._value); }, /** * Show a "waiting for results" UI. We may be showing a partial result set * at this time, if the user is extending a query we already have results * for. * * @task control * @return void */ waitForResults : function() { JX.DOM.alterClass(this._hardpoint, 'jx-typeahead-waiting', true); }, /** * Hide the "waiting for results" UI. * * @task control * @return void */ doneWaitingForResults : function() { JX.DOM.alterClass(this._hardpoint, 'jx-typeahead-waiting', false); }, /** * @task internal */ _onmouse : function(event) { this._moused = (event.getType() == 'mouseover'); this._drawFocus(); }, /** * @task internal */ _changeFocus : function(d) { var n = Math.min(Math.max(-1, this._focus + d), this._display.length - 1); if (!this.getAllowNullSelection()) { n = Math.max(0, n); } if (this._focus >= 0 && this._focus < this._display.length) { JX.DOM.alterClass(this._display[this._focus], 'focused', false); } this._focus = n; this._drawFocus(); return true; }, /** * @task internal */ _drawFocus : function() { var f = this._display[this._focus]; if (f) { JX.DOM.alterClass(f, 'focused', !this._moused); } }, /** * @task internal */ _choose : function(target) { var result = this.invoke('choose', target); if (result.getPrevented()) { return; } this._control.value = target.name; this.hide(); }, /** * @task control */ clear : function() { this._control.value = ''; this._value = ''; this.hide(); }, /** * @task control */ enable : function() { this._control.disabled = false; this._stop = false; }, /** * @task control */ disable : function() { this._control.blur(); this._control.disabled = true; this._stop = true; }, /** * @task control */ submit : function() { if (this._focus >= 0 && this._display[this._focus]) { this._choose(this._display[this._focus]); return true; } else { - result = this.invoke('query', this._control.value); + var result = this.invoke('query', this._control.value); if (result.getPrevented()) { return true; } } return false; }, setValue : function(value) { this._control.value = value; }, getValue : function() { return this._control.value; }, /** * @task internal */ _update : function(event) { if (event.getType() == 'focus') { this._focused = true; this.updatePlaceholder(); } var k = event.getSpecialKey(); if (k && event.getType() == 'keydown') { switch (k) { case 'up': if (this._display.length && this._changeFocus(-1)) { event.prevent(); } break; case 'down': if (this._display.length && this._changeFocus(1)) { event.prevent(); } break; case 'return': if (this.submit()) { event.prevent(); return; } break; case 'esc': if (this._display.length && this.getAllowNullSelection()) { this.hide(); event.prevent(); } break; case 'tab': // If the user tabs out of the field, don't refresh. return; } } // We need to defer because the keystroke won't be present in the input's // value field yet. setTimeout(JX.bind(this, function() { if (this._value == this._control.value) { // The typeahead value hasn't changed. return; } this.refresh(); }), 0); }, /** * This method is pretty much internal but @{JX.Tokenizer} needs access to * it for delegation. You might also need to delegate events here if you * build some kind of meta-control. * * Reacts to user events in accordance to configuration. * * @task internal * @param JX.Event User event, like a click or keypress. * @return void */ handleEvent : function(e) { if (this._stop || e.getPrevented()) { return; } var type = e.getType(); if (type == 'blur') { this._focused = false; this.updatePlaceholder(); this.hide(); } else { this._update(e); } }, removeListener : function() { if (this._listener) { this._listener.remove(); } }, /** * Set a string to display in the control when it is not focused, like * "Type a user's name...". This string hints to the user how to use the * control. * * When the string is displayed, the input will have class * "jx-typeahead-placeholder". * * @param string Placeholder string, or null for no placeholder. * @return this * * @task config */ setPlaceholder : function(string) { this._placeholder = string; this.updatePlaceholder(); return this; }, /** * Update the control to either show or hide the placeholder text as * necessary. * * @return void * @task internal */ updatePlaceholder : function() { if (this._placeholderVisible) { // If the placeholder is visible, we want to hide if the control has // been focused or the placeholder has been removed. if (this._focused || !this._placeholder) { this._placeholderVisible = false; this._control.value = ''; } } else if (!this._focused) { // If the placeholder is not visible, we want to show it if the control // has benen blurred. if (this._placeholder && !this._control.value) { this._placeholderVisible = true; } } if (this._placeholderVisible) { // We need to resist the Tokenizer wiping the input on blur. this._control.value = this._placeholder; } JX.DOM.alterClass( this._control, 'jx-typeahead-placeholder', this._placeholderVisible); } } }); diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js index 3ce355a2e0..31b9866d46 100644 --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadSource.js @@ -1,371 +1,371 @@ /** * @requires javelin-install * javelin-util * javelin-dom * javelin-typeahead-normalizer * @provides javelin-typeahead-source * @javelin */ JX.install('TypeaheadSource', { construct : function() { this._raw = {}; this._lookup = {}; this.setNormalizer(JX.TypeaheadNormalizer.normalize); this._excludeIDs = {}; }, events : ['waiting', 'resultsready', 'complete'], properties : { /** * Allows you to specify a function which will be used to normalize strings. * Strings are normalized before being tokenized, and before being sent to * the server. The purpose of normalization is to strip out irrelevant data, * like uppercase/lowercase, extra spaces, or punctuation. By default, * the @{JX.TypeaheadNormalizer} is used to normalize strings, but you may * want to provide a different normalizer, particularly if there are * special characters with semantic meaning in your object names. * * @param function */ normalizer : null, /** * If a typeahead query should be processed before being normalized and * tokenized, specify a queryExtractor. * * @param function */ queryExtractor : null, /** * Transformers convert data from a wire format to a runtime format. The * transformation mechanism allows you to choose an efficient wire format * and then expand it on the client side, rather than duplicating data * over the wire. The transformation is applied to objects passed to * addResult(). It should accept whatever sort of object you ship over the * wire, and produce a dictionary with these keys: * * - **id**: a unique id for each object. * - **name**: the string used for matching against user input. * - **uri**: the URI corresponding with the object (must be present * but need not be meaningful) * * You can also give: * - **display**: the text or nodes to show in the DOM. Usually just the * same as ##name##. * - **tokenizable**: if you want to tokenize something other than the * ##name##, for the typeahead to complete on, specify it here. A * selected entry from the typeahead will still insert the ##name## * into the input, but the ##tokenizable## field lets you complete on * non-name things. * * The default transformer expects a three element list with elements * [name, uri, id]. It assigns the first element to both ##name## and * ##display##. * * @param function */ transformer : null, /** * Configures the maximum number of suggestions shown in the typeahead * dropdown. * * @param int */ maximumResultCount : 5, /** * Optional function which is used to sort results. Inputs are the input * string, the list of matches, and a default comparator. The function * should sort the list for display. This is the minimum useful * implementation: * * function(value, list, comparator) { * list.sort(comparator); * } * * Alternatively, you may pursue more creative implementations. * * The `value` is a raw string; you can bind the datasource into the * function and use normalize() or tokenize() to parse it. * * The `list` is a list of objects returned from the transformer function, * see the `transformer` property. These are the objects in the list which * match the value. * * The `comparator` is a sort callback which implements sensible default * sorting rules (e.g., alphabetic order), which you can use as a fallback * if you just want to tweak the results (e.g., put some items at the top). * * The function is called after the user types some text, immediately before * the possible completion results are displayed to the user. * * @param function */ sortHandler : null, /** * Optional function which is used to filter results before display. Inputs * are the input string and a list of matches. The function should * return a list of matches to display. This is the minimum useful * implementation: * * function(value, list) { * return list; * } * * @param function */ filterHandler : null }, members : { _raw : null, _lookup : null, _excludeIDs : null, _changeListener : null, _startListener : null, bindToTypeahead : function(typeahead) { this._changeListener = typeahead.listen( 'change', JX.bind(this, this.didChange) ); this._startListener = typeahead.listen( 'start', JX.bind(this, this.didStart) ); }, unbindFromTypeahead : function() { this._changeListener.remove(); this._startListener.remove(); }, - didChange : function(value) { + didChange : function() { return; }, didStart : function() { return; }, clearCache : function() { this._raw = {}; this._lookup = {}; }, addExcludeID : function(id) { if (id) { this._excludeIDs[id] = true; } }, removeExcludeID : function (id) { if (id) { delete this._excludeIDs[id]; } }, addResult : function(obj) { obj = (this.getTransformer() || this._defaultTransformer)(obj); if (obj.id in this._raw) { // We're already aware of this result. This will happen if someone // searches for "zeb" and then for "zebra" with a // TypeaheadRequestSource, for example, or the datasource just doesn't // dedupe things properly. Whatever the case, just ignore it. return; } if (__DEV__) { for (var k in {name : 1, id : 1, display : 1, uri : 1}) { if (!(k in obj)) { throw new Error( "JX.TypeaheadSource.addResult(): " + "result must have properties 'name', 'id', 'uri' and 'display'."); } } } this._raw[obj.id] = obj; var t = this.tokenize(obj.tokenizable || obj.name); for (var jj = 0; jj < t.length; ++jj) { if (!this._lookup.hasOwnProperty(t[jj])) { this._lookup[t[jj]] = []; } this._lookup[t[jj]].push(obj.id); } }, waitForResults : function() { this.invoke('waiting'); return this; }, /** * Get the raw state of a result by its ID. A number of other events and * mechanisms give a list of result IDs and limited additional data; if you * need to act on the full result data you can look it up here. * * @param scalar Result ID. * @return dict Corresponding raw result. */ getResult : function(id) { return this._raw[id]; }, matchResults : function(value, partial) { // This table keeps track of the number of tokens each potential match // has actually matched. When we're done, the real matches are those // which have matched every token (so the value is equal to the token // list length). var match_count = {}; // This keeps track of distinct matches. If the user searches for // something like "Chris C" against "Chris Cox", the "C" will match // both fragments. We need to make sure we only count distinct matches. var match_fragments = {}; var matched = {}; var seen = {}; var query_extractor = this.getQueryExtractor(); if (query_extractor) { value = query_extractor(value); } var t = this.tokenize(value); // Sort tokens by longest-first. We match each name fragment with at // most one token. t.sort(function(u, v) { return v.length - u.length; }); for (var ii = 0; ii < t.length; ++ii) { // Do something reasonable if the user types the same token twice; this // is sort of stupid so maybe kill it? if (t[ii] in seen) { t.splice(ii--, 1); continue; } seen[t[ii]] = true; var fragment = t[ii]; for (var name_fragment in this._lookup) { if (name_fragment.substr(0, fragment.length) === fragment) { if (!(name_fragment in matched)) { matched[name_fragment] = true; } else { continue; } var l = this._lookup[name_fragment]; for (var jj = 0; jj < l.length; ++jj) { var match_id = l[jj]; if (!match_fragments[match_id]) { match_fragments[match_id] = {}; } if (!(fragment in match_fragments[match_id])) { match_fragments[match_id][fragment] = true; match_count[match_id] = (match_count[match_id] || 0) + 1; } } } } } var hits = []; for (var k in match_count) { if (match_count[k] == t.length && !this._excludeIDs[k]) { hits.push(k); } } this.filterAndSortHits(value, hits); var nodes = this.renderNodes(value, hits); this.invoke('resultsready', nodes, value); if (!partial) { this.invoke('complete'); } }, filterAndSortHits : function(value, hits) { var objs = []; var ii; for (ii = 0; ii < hits.length; ii++) { objs.push(this._raw[hits[ii]]); } var default_comparator = function(u, v) { var key_u = u.sort || u.name; var key_v = v.sort || v.name; return key_u.localeCompare(key_v); }; var filter_handler = this.getFilterHandler() || function(value, list) { return list; }; objs = filter_handler(value, objs); var sort_handler = this.getSortHandler() || function(value, list, cmp) { list.sort(cmp); }; sort_handler(value, objs, default_comparator); hits.splice(0, hits.length); for (ii = 0; ii < objs.length; ii++) { hits.push(objs[ii].id); } }, renderNodes : function(value, hits) { var n = Math.min(this.getMaximumResultCount(), hits.length); var nodes = []; for (var kk = 0; kk < n; kk++) { nodes.push(this.createNode(this._raw[hits[kk]])); } return nodes; }, createNode : function(data) { return JX.$N( 'a', { sigil: 'typeahead-result', href: data.uri, name: data.name, rel: data.id, className: 'jx-result' }, data.display ); }, normalize : function(str) { return this.getNormalizer()(str); }, tokenize : function(str) { str = this.normalize(str); if (!str.length) { return []; } return str.split(/\s/g); }, _defaultTransformer : function(object) { return { name : object[0], display : object[0], uri : object[1], id : object[2] }; } } });