/** * 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) { }}; } } /** * 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 (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(); });