src/util.js
/*globals crypto, WebKitBlobBuilder, Blob, URL */
/*globals webkitURL, Uint8Array, Uint16Array, ArrayBuffer */
/*jslint indent:2,white:true,browser:true,node:true,sloppy:true */
/**
* Utility method used within the freedom Library.
* @class util
* @static
*/
var util = {};
/**
* Helper function for iterating over an array backwards. If the func
* returns a true value, it will break out of the loop.
* @method eachReverse
* @static
*/
util.eachReverse = function(ary, func) {
if (ary) {
var i;
for (i = ary.length - 1; i > -1; i -= 1) {
if (ary[i] && func(ary[i], i, ary)) {
break;
}
}
}
};
/**
* @method hasProp
* @static
*/
util.hasProp = function(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
/**
* Cycles over properties in an object and calls a function for each
* property value. If the function returns a truthy value, then the
* iteration is stopped.
* @method eachProp
* @static
*/
util.eachProp = function(obj, func) {
var prop;
for (prop in obj) {
if (obj.hasOwnProperty(prop)) {
if (func(obj[prop], prop)) {
break;
}
}
}
};
/**
* Simple function to mix in properties from source into target,
* but only if target does not already have a property of the same name.
* This is not robust in IE for transferring methods that match
* Object.prototype names, but the uses of mixin here seem unlikely to
* trigger a problem related to that.
* @method mixin
* @static
*/
util.mixin = function(target, source, force) {
if (source) {
util.eachProp(source, function (value, prop) {
if (force || !util.hasProp(target, prop)) {
target[prop] = value;
}
});
}
return target;
};
/**
* Get a unique ID.
* @method getId
* @static
*/
util.getId = function() {
var guid = 'guid',
domain = 12,
buffer;
// Chrome / Firefox.
if (typeof crypto === 'object' && crypto.getRandomValues) {
buffer = new Uint8Array(domain);
crypto.getRandomValues(buffer);
util.eachReverse(buffer, function(n) {
guid += '-' + n;
});
// Node
} else if (typeof crypto === 'object' && crypto.randomBytes) {
buffer = crypto.randomBytes(domain);
util.eachReverse(buffer, function(n) {
guid += '-' + n;
});
} else {
while (domain > 0) {
guid += '-' + Math.ceil(255 * Math.random());
domain -= 1;
}
}
return guid;
};
/**
* Encode a string into a binary array buffer, by treating each character as a
* utf16 encoded character - the native javascript encoding.
* @method str2ab
* @static
* @param {String} str The string to encode.
* @returns {ArrayBuffer} The encoded string.
*/
util.str2ab = function(str) {
var length = str.length,
buffer = new ArrayBuffer(length * 2), // 2 bytes for each char
bufferView = new Uint16Array(buffer),
i;
for (i = 0; i < length; i += 1) {
bufferView[i] = str.charCodeAt(i);
}
return buffer;
};
/**
* Convert an array buffer containing an encoded string back into a string.
* @method ab2str
* @static
* @param {ArrayBuffer} buffer The buffer to unwrap.
* @returns {String} The decoded buffer.
*/
util.ab2str = function(buffer) {
var str = '';
var a = new Uint16Array(buffer);
for (var i = 0; i < a.length; i++) {
str += String.fromCharCode(a[i]);
}
return str;
};
/**
* Add 'on' and 'emit' methods to an object, which act as a light weight
* event handling structure.
* @class handleEvents
* @static
*/
util.handleEvents = function(obj) {
var eventState = {
DEBUG_BACKREF: obj,
multiple: {},
maybemultiple: [],
single: {},
maybesingle: []
}, filter, push;
/**
* Filter a list based on a predicate. The list is filtered in place, with
* selected items removed and returned by the function.
* @method
* @param {Array} list The list to filter
* @param {Function} predicate The method to run on each item.
* @returns {Array} Selected items
*/
filter = function(list, predicate) {
var ret = [], i;
if (!list || !list.length) {
return [];
}
for (i = list.length - 1; i >= 0; i -= 1) {
if (predicate(list[i])) {
ret.push(list.splice(i, 1));
}
}
return ret;
};
/**
* Enqueue a handler for a specific type.
* @method
* @param {String} to The queue ('single' or 'multiple') to queue on.
* @param {String} type The type of event to wait for.
* @param {Function} handler The handler to enqueue.
*/
push = function(to, type, handler) {
if (typeof type === 'function') {
this['maybe' + to].push([type, handler]);
} else if (this[to][type]) {
this[to][type].push(handler);
} else {
this[to][type] = [handler];
}
};
/**
* Register a method to be executed when an event of a specific type occurs.
* @method on
* @param {String|Function} type The type of event to register against.
* @param {Function} handler The handler to run when the event occurs.
*/
obj.on = push.bind(eventState, 'multiple');
/**
* Register a method to be execute the next time an event occurs.
* @method once
* @param {String|Function} type The type of event to wait for.
* @param {Function} handler The handler to run the next time a matching event
* is raised.
*/
obj.once = push.bind(eventState, 'single');
/**
* Emit an event on this object.
* @method emit
* @param {String} type The type of event to raise.
* @param {Object} data The payload of the event.
*/
obj.emit = function(type, data) {
var i, queue;
// Note that registered handlers may stop events on the object, by calling
// this.off(). As such, the presence of these keys must be checked on each
// iteration of the relevant loops.
for (i = 0; this.multiple[type] &&
i < this.multiple[type].length; i += 1) {
if (this.multiple[type][i](data) === false) {
return;
}
}
if (this.single[type]) {
queue = this.single[type];
this.single[type] = [];
for (i = 0; i < queue.length; i += 1) {
queue[i](data);
}
}
for (i = 0; i < this.maybemultiple.length; i += 1) {
if (this.maybemultiple[i][0](type, data)) {
this.maybemultiple[i][1](data);
}
}
for (i = this.maybesingle.length - 1; i >= 0; i -= 1) {
if (this.maybesingle[i][0](type, data)) {
queue = this.maybesingle.splice(i, 1);
queue[0][1](data);
}
}
}.bind(eventState);
/**
* Remove an event handler
* @method off
* @param {String} type The type of event to remove.
* @param {Function?} handler The handler to remove.
*/
obj.off = function(type, handler) {
if (!type) {
delete this.DEBUG_BACKREF;
this.multiple = {};
this.maybemultiple = [];
this.single = {};
this.maybesingle = [];
return;
}
if (typeof type === 'function') {
filter(this.maybesingle, function(item) {
return item[0] === type && (!handler || item[1] === handler);
});
filter(this.maybemultiple, function(item) {
return item[0] === type && (!handler || item[1] === handler);
});
}
if (!handler) {
delete this.multiple[type];
delete this.single[type];
} else {
filter(this.multiple[type], function(item) {
return item === handler;
});
filter(this.single[type], function(item) {
return item === handler;
});
}
}.bind(eventState);
};
/**
* When run without a window, or specifically requested.
* Note: Declaration can be redefined in forceModuleContext below.
* @method isModuleContext
* @for util
* @static
*/
/*!@preserve StartModuleContextDeclaration*/
util.isModuleContext = function() {
return (typeof document === 'undefined');
};
/**
* Get a Blob object of a string.
* Polyfills implementations which don't have a current Blob constructor, like
* phantomjs.
* @method getBlob
* @static
*/
util.getBlob = function(data, type) {
if (typeof Blob !== 'function' && typeof WebKitBlobBuilder !== 'undefined') {
var builder = new WebKitBlobBuilder();
builder.append(data);
return builder.getBlob(type);
} else {
return new Blob([data], {type: type});
}
};
/**
* Find all scripts on the given page.
* @method scripts
* @static
*/
util.scripts = function(global) {
return global.document.getElementsByTagName('script');
};
module.exports = util;