/** @function autoExpand @desc Used as a callback on keydown/press in textareas, resizes them to accommodate full height of text while typing. @arg {Element|jQuery} $element - Textarea to resize. @arg {Function} callback - Function to call with new height after resizing. */ function autoExpand($element, callback) { element = jUnwrap($element); callback = callback || function () {}; //element.style.height = (element.scrollHeight > element.clientHeight) ? (element.scrollHeight) + 'px' : '1.5rem'; element.style.height = '0px'; setTimeout(function () { var height = element.style.height = (element.scrollHeight) + 'px'; callback.apply(element, [height]); }, 1); } /** @function convertToLocalTime @desc Adds local timezone offset to a numeric UTC timestamp. @arg {Number} timeUTC - UTC timestamp. @returns {Object} New Date instance. */ function convertToLocalTime(timeUTC) { var offset = parseInt(new Date().getTimezoneOffset()), timeLocal = new Date(timeUTC.getTime() - (offset * T_MINUTE)); return timeLocal; } /** @function dataArrayToObject @desc Transforms a list of data objects into an object of data objects. Each key in the containing object corresponds to a specific property value of each data object. Each data object must contain the property used for indexing. Useful for fast lookups without looping. @arg {Array} data - List of data to transform. @returns {String} indexed - Property within each data object whose value to use as key in indexed object. @returns {Object} Indexed data object. @see {@link dataObjectToArray} */ function dataArrayToObject(data, indexed) { var funcName = 'window.dataArrayToObject', type = dltypeof(data), indexed = dltypeof(indexed) === 'string' ? indexed : false, dataNew = {}; // validate if (!indexed) { log(L_ERROR, funcName, 'invalid indexing key'); return false; } if (type !== 'array') { log(L_ERROR, funcName, 'data is not an array'); return false; } if (!data.length) { log(L_INFO , funcName, 'data is empty'); return dataNew; } // convert for (var i = 0; i < data.length; i++) { var item = data[i], key = item[indexed]; if (typeof key !== 'undefined') { dataNew[key] = item; } else { log(L_ERROR, funcName, 'data index {0} lacks key "{1}"'.format(i, indexed)); return false; } } return dataNew; } /** @function dataObjectToArray @desc Transform indexed data object into plain list of data objects, optionally populating each with value formerly used as keys. @arg {Object} data - Indexed data object. @arg {String} [indexed] - Property within each data object to restore using containing object's keys as values. @returns {Array} List of data objects. @see {@link dataArrayToObject} @see {@link Object#toArray} */ function dataObjectToArray(data, indexed) { var funcName = 'window.dataObjectToArray', type = dltypeof(data), indexed = dltypeof(indexed) === 'string' ? indexed : false, dataNew = []; // validate if (!~$.inArray(type, ['object', 'jsobject'])) { log(L_ERROR, funcName, 'data is not an object'); return false; } // convert for (var key in data) { if (data.hasOwnProperty(key)) { var item = data[key]; if (!~$.inArray(dltypeof(item), ['object', 'jsobject'])) { log(L_ERROR, funcName, 'data key "{0}" is not an object'.format(key)); return false; } // add index value to item if asked indexed && typeof item[indexed] === 'undefined' && (item[indexed] = key); dataNew.push(item); }} if (!dataNew.length) { log(L_INFO , funcName, 'data is empty'); } return dataNew; } /** @function decodeURIComponentSafe @desc A version of decodeURIComponent that catches exceptions, allowing code to continue execution. @arg {String} encoded - String to decode safely. @returns {String} Decoded string. */ function decodeURIComponentSafe(encoded) { var decoded; try { decoded = decodeURIComponent(encoded); } catch (e) { var caller = arguments.callee.caller.toString() // the responsible function body .replace(/[\r\n]/i, '') // strip line breaks .substr(0, 100); // truncate decoded = encoded; log ( L_ERROR, 'window.decodeURIComponentSafe', 'found an unsafe string: "{0}"'.format(encoded), 'called by: "{0}"'.format(caller) ); } return decoded; } /** @function detectStyles @desc Searches DOM for unprocessed less styleheets, and processes them. @returns {Number} Count of stylesheets detected and processed. */ function detectStyles() { var timer = new TimerClass(), found = 0, $lessStyles = $('link[type="text/less"]'); if ($lessStyles.length) { $lessStyles.each(function () { var $link = $(this); if (!$link.hasClass('processed')) { $link.addClass('processed'); less.sheets.push(this); found++; } $link.hasClass('processed') && $link.remove(); }); less.refresh(); } log(L_INFO, 'window.detectStyles', '{0} in {1}'.format(found, timer.elapsed(1))); return found; } /** @function detectTemplates @desc Searches document for unprocessed templates, compiles them, and saves generator functions as both properties in the globally accessible {@link templates} object, and partials usable by id within other templates. @arg {String} prefix - Used to filter what template ids are to be processed. @arg {Boolean} [force=false] - Whether to replace saved generators if a duplicate is found. @returns {Number} Count of templates detected and processed. */ function detectTemplates(prefix, force) { var timer = new TimerClass(), prefix = prefix || '', force = force || false, pattern = new RegExp('^' + prefix + '(.+)$', 'i'), matches , id , source , generator , found = 0; $('script[type*="handlebars"]').each(function () { if ((matches = this.id.match(pattern)) !== null) { id = matches[1]; if (!force && (id in window.templates)) { return true; } // continue var $element = $(this); log(L_INFO, 'window.detectTemplates', '#' + $element.attr('id')); source = $.trim($element.html()); generator = Handlebars.compile(source); window.templates[id] = generator; // template Handlebars.registerPartial(id, source); // partial $element.remove(); found++; } }); log(L_INFO, 'window.detectTemplates', found + ' in ' + timer.elapsed(1)); return found; }; /** @function deval @desc As close as possible to the opposite of eval. Like JSON.stringify without type restrictions. @arg {Mixed} thing - Any value. @returns {String} Representation of value suitable for direct eval. */ function deval(thing) { var self = arguments.callee; // static typeof self.whitespace === 'undefined' && (self.whitespace = true); typeof self.comments === 'undefined' && (self.comments = true); // private var s = self.whitespace ? ' ' : '', // space? separator = ',' + s, type = dltypeof(thing) || 'jsobject'; // if dltypeof says undefined, it's probably an object switch (type) // what is this thing? { case 'array' : var values = [], thingLength = thing.length; for (var i = 0; i < thingLength; i++) { values.push(self(thing[i])); } return '[' + values.join(self.separator) + ']'; case 'object' : case 'jsobject' : case 'arguments' : var values = []; for (key in thing) { thing.hasOwnProperty(key) && values.push('"' + key + '":' + s + self(thing[key])); } return '{' + values.join(self.separator) + '}'; case 'boolean' : case 'number' : case 'funciton' : return thing.toString(); case 'string' : return '"' + thing.replace(/\"/g, '\\"') + '"'; case 'domelement' : return (s && '/* element */ ') + '$("' + html(thing).replace(/\"/g, '\\"') + '")[0]'; case 'date' : return 'new Date(' + thing.getTimestamp() + ')'; case 'math' : return 'Math'; case 'window' : return 'window'; case 'undefined' : return 'undefined'; case 'null' : return 'null'; case 'error' : return '"[Error]"'; case 'domcollection' : return '"[DOM Collection]"'; case 'mimetypecollection': return '"[Mime Type Collection]"'; case 'plugincollection' : return '"[Plugin Collection]"'; case 'textnode' : return '"[Text Node]"'; case 'textrange' : return '"[Text Range]"'; case 'regexp' : return (self.comments ? '/*'+s+'regexp'+s+'*/'+s : '') + '(function'+s+'(i)'+s+'{var x'+s+'='+s+'/' + thing.source + '/' + (thing.global ? 'g' : '') + (thing.ignoreCase ? 'i' : '') + (thing.multiline ? 'm' : '') + (thing.sticky ? 'y' : '') + ';'+s+'x.lastIndex'+s+'='+s+'i;'+s+'return x;'+s+'})(' + thing.lastIndex + ')'; default : return (/^(html|element)/i.test(type)) // html-ifiable? ? (self.comments ? '/*'+s+'element'+s+'*/'+s : '') + '$("' + html(thing).replace(/\"/g, '\\"') + '")[0]' : (self.comments ? '/*'+s+ type +s+'*/' : '') + 'null'; } } // in development function diff(before, after) { var beforeType = dltypeof(before), afterType = dltypeof(after); if (beforeType !== afterType) { log(L_ERROR, 'window.diff', 'types do not match'); return false; } switch (beforeType) { case 'object' : case 'jsobject': var output = { 'added' : {}, 'modified': {}, 'removed' : {} }; for (var prop in before) { if (before.hasOwnProperty(prop)) { }} return; case 'array': var output = { 'added' : after .filter(function (item, index) { return before.indexOf(item) < 0; }), 'removed': before.filter(function (item, index) { return after .indexOf(item) < 0; }) }; return output; default: log(L_ERROR, 'window.diff', 'unsupported type "{0}"'.format(typeA)); return false; } } // in development function diffDataArray(a, b) { var aType = dltypeof(a), bType = dltypeof(b), output = {'added': [], 'modified': [], 'deleted': []}; if (aType !== bType) { log(L_ERROR, 'window.diff', 'types do not match'); return output; } if (aType !== 'array') { log(L_ERROR, 'window.diff', 'types are not arrays'); return output; } var idsGet = function (item, index) { return item.id; }, ids = [], idsA = a.map(idsGet), idsB = a.map(idsGet); $.extend(ids, idsA, idsB); a = dataArrayToObject(a, 'id'); b = dataArrayToObject(b, 'id'); for (var id in ids) { if (typeof a[id] === 'undefined') { output.added.push(b[id]); } else if (typeof b[id] === 'undefined') { output.deleted.push(a[id]); } else if (diffDataObject(a[id], b[id])) { output.modified.push(b[id]); } } return output; } // in development function diffDataObject(a, b) { var keys = [], keysA = [], keysB = [], modified = false; for (var key in a) { a.hasOwnProperty(key) && keysA.push(key); } for (var key in b) { b.hasOwnProperty(key) && keysB.push(key); } $.extend(keys, keysA, keysB); $.each(keys, function (index, value) { if (a[key] !== b[key]) { modified = true; return false; } }); return modified; } /** @function exists @desc Checks if a value isn't undefined. Detects false, null, and NaN. @arg {Mixed} thing - Any value. @returns {Boolean} Whether thing exists. */ function exists(thing) { return typeof thing !== 'undefined'; } /** @function fork @desc Faux-forks a function call via setTimeout. This allows procedural execution to continue while the function executes in semi-parallel (round-robin). Useful for allowing layout/reflow/repaint so up to date values can be queried from the DOM. @arg {Function} func - Function to call, less-than-procedurally. @arg {Number} [delay=0] - @returns {Number} Positive if f was actually a function and was forked. */ function fork(func, delay) { return typeof func === 'function' ? setTimeout(func, Number(delay) || 0) : 0; } /** @function format_mysql_date @desc Transforms a timestamp into a mysql-style formatted date string. @deprecated since version 1.9 @arg {Number} date - Integer UTC timestamp. @arg {String} [day_offset='0'] - Number of days to offset. Actually only responds to '0' vs any other value. @returns {String} Formatted date. */ function format_mysql_date(date, day_offset) { if (date === '0') { return date; } var offset = parseInt(new Date().getTimezoneOffset()) formatted_date = new Date(date) local_time = new Date ( formatted_date.getTime() + offset * T_MINUTE + (day_offset === '0' ? 0 : T_DAY) ), month = parseInt(local_time.getMonth()) + 1, mysql_date = local_time.getFullYear() + '-' + month + '-' + local_time.getDate() + ' ' + local_time.getHours() + ':' + local_time.getMinutes() +' :' + local_time.getSeconds(); return mysql_date; } /** @function getAttributes @desc Reads element attributes into an object. @arg {jQuery|Element} $element - Source of attributes. @returns {Object} Populated object. */ function getAttributes($element) { var keys = jWrap($element)[0].attributes, keysLength = keys.length; attributes = {}; for (var i = 0; i < keysLength; i++) { var key = keys[i]; attributes[key.name] = key.value; } return attributes; } /** @function html @desc Transforms element into representative markup. @arg {jQuery|Element} element - Element to transform. @returns {String} Representative HTML. */ function html(element) { return $('<div></div>').append(jWrap(element).clone()).html().replace(/\s{2,}/g, ' '); } /** @function htmlDecode @desc Unescapes HTML entities within a string. @arg {String} html - String to decode. @returns {String} Decoded string. */ function htmlDecode(html) { return $('<div></div>').html(html).text(); } /** @function htmlEncode @desc Escapes HTML-unsafe characters within a string. @arg {String} text - String to encode. @returns {String} Encoded string. */ function htmlEncode(text) { return $('<div></div>').text(text).html(); } /** @function isExternal @desc Tries to determine if a given URL points within the scope of the current page. @arg {String} url - Location to analyze. @returns {Boolean} Whether url is external. */ function isExternal(url) { var match = url.match(/^([^:\/?#]+:)?(?:\/\/([^\/?#]*))?([^?#]+)?(\?[^#]*)?(#.*)?/); if ( typeof match[1] === "string" && match[1].length > 0 && match[1].toLowerCase() !== location.protocol ) { return true; } if ( typeof match[2] === "string" && match[2].length > 0 && match[2].replace(new RegExp(":("+{"http:":80,"https:":443}[location.protocol]+")?$"), "") !== location.host ) { return true; } return false; } /** @function jUnwrap @desc Removes jQuery wrapper from element, if any. @arg {jQuery|Element} $element - Reference to strip of jQuery wrapper. @returns {Element} Guaranteed bare element. */ function jUnwrap($element) { return $element instanceof $ ? $element[0] : $element; } /** @function jWrap @desc Adds jQuery wrapper to element if not already wrapped. @arg {jQuery|Element} $element - Reference to wrap with jQuery. @returns {Element} Guaranteed wrapped element. */ function jWrap(element) { return element instanceof $ ? element : $(element); } /** @function killEvent @desc Shorthand way of stopping propagation and preventing default action for a given event. @arg {Object} event - Event to neutralize. */ function killEvent(event) { event.preventDefault(); event.stopPropagation(); } /** @function linkCallback @desc Wraps a function reference in a global uniquely named function, and returns that name. Useful for passing arbitrary or anonymous functions into Framework methods, which only accept strings. @arg {Function} callback - Function to represent via string. @arg {Object} [context=window] - <this> value when calling wrapped function. @arg {Boolean} [persist=false] - Whether the wrapper should persist after one call, or destruct itself. @arg {Object} [deferred] - Deferred object to resolve in addition to calling wrapped function. @returns {String} Global callback wrapper function name. @property {Number} counter - How many wrapper functions have been generated. @property {Number} expire - How long before non-persisting functions automatically destruct themselves. @see {@link linkCallback(2)} @see {@link unlinkCallback} */ /** @function linkCallback(2) @desc Logs current settings (static properties). @returns {Boolean} false @property {Number} counter - How many wrapper functions have been generated. @property {Number} expire - How long before non-persisting functions automatically destruct themselves. @see {@link linkCallback} @see {@link unlinkCallback} */ function linkCallback(callback, context, persist, deferred) { var self = arguments.callee, argumentsLenth = arguments.length; // static typeof self.counter == 'undefined' && (self.counter = 0); typeof self.expire == 'undefined' && (self.expire = T_MINUTE * 15); var context = context || window, persist = persist || false, uniqueName = 'callback_' + ++self.counter; if (!argumentsLenth) // display settings? { log(L_ALWAYS, 'window.linkCallback', 'settings', { 'counter': self.counter, 'expire' : self.expire }); return false; } // wrapper log(L_DEBUG, 'window.linkCallback', 'linking ' + uniqueName + ' (persist: ' + persist + ')'); window[uniqueName] = function () { //log(L_DEBUG, 'window.' + uniqueName, persist ? 'persisting' : 'unlinking', arguments); var self = arguments.callee; callback && callback.apply(context, arguments); deferred && deferred.resolve.apply(deferred, arguments); !self.persist && unlinkCallback(uniqueName); }; // wrapper static window[uniqueName].callback = callback; window[uniqueName].persist = persist; window[uniqueName].timeout = persist ? null : setTimeout(function () { log(L_DEBUG, 'window.' + uniqueName, 'auto expire') && unlinkCallback(uniqueName); }, self.expire); return uniqueName; } /** @function log @desc Send stringified values to console or file for debugging or analytics. Will always output regardless of verbosity and squelch settings. @arg {...Mixed} value - Any number of any value. @returns {Boolean} true @property {Object} verbosity - Range of message levels to output. @property {Number} verbosity.min - Minimum depth of message levels to log, set with logging constants. @property {Number} verbosity.max - Maximum depth of message levels to log, set with logging constants. @property {Object} squelch - Range of message levels to {@link deval} arguments/html/xml. @property {Number} squelch.min - Minimum depth of message levels to pass squelch, set with logging constants. @property {Number} squelch.max - Maximum depth of message levels to pass squelch, set with logging constants. @property {Object} truncate - Max length of stringified arguments or output, 0 = unlimited. @property {Number} truncate.argument - Max length of each argument logged. @property {Number} truncate.output - Max length of total output. @property {Boolean} compress - Whether to LZ compress data portion of log line, requires LZString. @property {Array.<String>} names - Custom identifiers for message levels, pad to same length to align log lines. @property {String} separator - Characters to delineate values within a line. @property {Boolean} console - Whether to redirect messages to the console instead of file. @property {Boolean} ms - Whether to display millisecond timestamp. @property {Number} msPad - How many places to pad timestamps. @property {Object} timer - Timer instance for generating timestamps. @property {String} identifier - String used to indicate which WebView emitted a message, defaults to {@link Starlet.identifier} or 'connection'. @property {Number} leaderLength - Used to adaptively pad the leader of each line to align data into columns for readability. @property {Array.<RegExp>} exclude - List of patterns to test on final output, logs nothing if any match. @see {@link log(2)} @see {@link deval} */ /** @function log(2) @desc Send stringified values to console or file for debugging or analytics. @arg {Number} level - Message level of line, use logging constants. @arg {...Mixed} value - Any number of any value. @returns {Boolean} true @property {Object} verbosity - Range of message levels to output. @property {Number} verbosity.min - Minimum depth of message levels to log, set with logging constants. @property {Number} verbosity.max - Maximum depth of message levels to log, set with logging constants. @property {Object} squelch - Range of message levels to {@link deval} arguments/html/xml. @property {Number} squelch.min - Minimum depth of message levels to pass squelch, set with logging constants. @property {Number} squelch.max - Maximum depth of message levels to pass squelch, set with logging constants. @property {Object} truncate - Max length of stringified arguments or output, 0 = unlimited. @property {Number} truncate.argument - Max length of each argument logged. @property {Number} truncate.output - Max length of total output. @property {Boolean} compress - Whether to LZ compress data portion of log line, requires LZString. @property {Array.<String>} names - Custom identifiers for message levels, pad to same length to align log lines. @property {String} separator - Characters to delineate values within a line. @property {Boolean} console - Whether to redirect messages to the console instead of file. @property {Boolean} ms - Whether to display millisecond timestamp. @property {Number} msPad - How many places to pad timestamps. @property {Object} timer - Timer instance for generating timestamps. @property {String} identifier - String used to indicate which WebView emitted a message, defaults to {@link Starlet.identifier} or 'connection'. @property {Number} leaderLength - Used to adaptively pad the leader of each line to align data into columns for readability. @property {Array.<RegExp>} exclude - List of patterns to test on final output, logs nothing if any match. @see {@link log} @see {@link deval} */ function log() { var self = arguments.callee, argumentsLength = arguments.length, level = 0, type = '', message = [], fwLevelsText = ['DEBUG', 'INFO', 'WARN', 'ERROR'], fwLevels = /* framework d i w e e n a r b f r r u o n o g r */ [ 1, // always s 3, // error c 2, // warn r 1, // main i 1, // func p 1, // info t 0 // debug ]; // static !exists(self.verbosity ) && (self.verbosity = {'min' : L_ALWAYS, 'max' : L_MAIN}); !exists(self.squelch ) && (self.squelch = {'min' : L_ALWAYS, 'max' : L_MAIN}); !exists(self.truncate ) && (self.truncate = {'argument': 0 , 'output': 300}); !exists(self.compress ) && (self.compress = false); !exists(self.names ) && (self.names = ['always ', 'error ', 'warn ', 'main ', 'function', 'info ', 'debug ']); !exists(self.separator ) && (self.separator = ', '); !exists(self.console ) && (self.console = false); !exists(self.ms ) && (self.ms = false); !exists(self.msPad ) && (self.msPad = (T_MONTH * 12).toString().length); !exists(self.timer ) && (self.timer = new TimerClass()); !exists(self.identifier ) && (self.identifier = pad(typeof Starlet !== 'undefined' ? Starlet.identifier : 'connection', 20)); !exists(self.leaderLength) && (self.leaderLength = 'yyyymmdd hh:mm:ss.mmmuuu (DEBUG) : [{0}] : '.format ( typeof Starlet !== 'undefined' ? Starlet.name : 'connection_starlet' ) .length ); !exists(self.exclude ) && (self.exclude = []); // each arg for (var i = 0; i < argumentsLength; i++) { var arg = arguments[i]; // first arg is message level? if (!i && typeof arg === 'number') { level = arg; if (level >= self.verbosity.min && level <= self.verbosity.max) // in range { var text = []; //text.push(self.identifier); // source webview self.ms && text.push(pad(self.timer.elapsed(), self.msPad, '0')); // time since first call text.push(self.names[level]); // message level message.push('[' + text.join(':') + ']'); // final info string continue; } else { return true; /* courtesy */ } // not in range } // squelch appropriately switch (type = dltypeof(arg)) { case 'array' : case 'boolean' : case 'date' : case 'domcollection' : case 'error' : case 'funciton' : case 'jsobject' : case 'math' : case 'mimetypecollection': case 'null' : case 'number' : case 'object' : case 'plugincollection' : case 'regexp' : case 'string' : case 'textnode' : case 'textrange' : case 'undefined' : case 'window' : break; case 'arguments' : case 'domelement' : if (level < self.squelch.min || level > self.squelch.max) { message.push('/* ' + type + ' */'); continue; } break; default : if (/^(html|element)/i.test(type)) // html-like? { if (level < self.squelch.min || level > self.squelch.max) { message.push('/* ' + type + ' */'); continue; } break; } else { break; } } // finalize arg arg = deval(arg); self.truncate.argument && arg.length > self.truncate.argument && (arg.substr(0, self.truncate.argument - 4) + ' ...'); message.push(arg); } // no args? stop if (!message.length) { return true; /* courtesy */ } // finalize output message = message.join(self.separator); self.truncate.output && message.length + self.leaderLength + 4 > self.truncate.output && (message = message.substr(0, self.truncate.output - self.leaderLength - 4) + ' ...'); // exclusion for (var i = 0; i < self.exclude.length; i++) { if (self.exclude[i].test(message)) { return true; } } // compress? if (self.compress && window.LZString) { var startLength = message.length, startTime = (new Date()).getTime(); message = LZString.compressToUTF16(message); var endTime = (new Date()).getTime(), endLength = message.length; message = '{0}% {1}ms ~ {2}'.format(Math.round(endLength / startLength * 100), endTime - startTime, message); } // output ISORION && !self.console ? OrionSystem.logToConsole(fwLevels[level], message + "\n") : console.log(message); //Bus.ask('logs', {'action': 'create', 'message': message}); return true; // courtesy } /** @function pad @readonly @desc Prepends characters to a value. @arg {Number|String} value - Item to pad. @arg {Number} places - Desired minimum length. @arg {String} character - Item to pad with. @returns {String} Padded string of length >= places. */ function pad(value, places, character) { value = value.toString(); character = character || ' '; while (value.length < places) { value = character + value; } return value; } // in development function sanitizeJSON(input) { var self = arguments.callee, output = null; switch (dltypeof(input)) { case 'arguments': input = [].slice.call(input); case 'object' : case 'jsobject' : output = {}, returned = null; for (var key in input) { input.hasOwnProperty(key) && (returned = self(input[key])) !== null && (output[key] = returned); } //console.log('obj', output); break; case 'array': output = []; for (var index = 0; index < input.length; index++) { (returned = self(input[index])) !== null && (output[index] = returned); } //console.log('obj', output); break; case 'string' : case 'number' : case 'boolean': case 'null' : output = input; break; default: break; } return output || null; } /** @function sanitizeParams @readonly @desc Converts properties to strings for safer assembly of URLs. @arg {Object} params - Parameters object to sanitize. @returns {Object} Sanitized parameters object */ function sanitizeParams(params) { for (var key in params) { typeof params[key] === 'number' && (params[key] = String(params[key])); }; return params; } /** @function touch @readonly @desc Creates a property on an object if it doesn't exist, defaults to value given, and returns the reference. @arg {Object} obj - Container object. @arg {String} property - Key to create if undefined. @arg {Mixed} [value] - Item to set key to, otherwise null. @returns {Mixed} Reference to obj.property. */ function touch(obj, property, value) { !obj.hasOwnProperty(property) && (obj[property] = value || null); return obj[property]; } /** @function unlinkCallback @readonly @desc Removes a callback wrapper generated with {@link linkCallback}. @arg {String|Function} callback - Generated function name or original callback. @returns {Boolean} Success or failure. @seec {@link linkCallback} */ function unlinkCallback(callback) { switch (typeof callback) { case 'string': log(L_DEBUG, 'window.unlinkCallback', callback, 'by name'); clearTimeout(window[callback].timeout); delete window[callback]; return true; case 'function': var pattern = new RegExp('^callback_'); for (key in window) { if (pattern.test(key) && window[key] === callback) { log(L_DEBUG, 'window.unlinkCallback', key, 'by reference'); clearTimeout(window[key].timeout); delete window[key]; return true; } } default: return false; } }