463 lines
11 KiB
JavaScript
463 lines
11 KiB
JavaScript
/**
|
||
* Utopia: A JavaScript util library that assumes modern standards support and doesn't fix any browser bugs
|
||
* @author Lea Verou (http://lea.verou.me)
|
||
* MIT license (http://www.opensource.org/licenses/mit-license.php)
|
||
* Last update: 2012-4-29
|
||
*/
|
||
|
||
function $(expr, con) {
|
||
return typeof expr === 'string'? (con || document).querySelector(expr) : expr;
|
||
}
|
||
|
||
function $$(expr, con) {
|
||
var elements = (con || document).querySelectorAll(expr);
|
||
|
||
try {
|
||
return Array.prototype.slice.call(elements);
|
||
}
|
||
catch(e) {
|
||
var arr = Array(elements.length);
|
||
|
||
for (var i = elements.length; i-- > 0;) {
|
||
arr[i] = elements[i];
|
||
}
|
||
|
||
return arr;
|
||
}
|
||
}
|
||
|
||
if (!Array.prototype.forEach) {
|
||
Array.prototype.forEach = function(fn, scope) {
|
||
for (var i = 0, len = this.length; i < len; ++i) {
|
||
fn.call(scope || this, this[i], i, this);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Make each ID a global variable
|
||
// Many browsers do this anyway (it’s in the HTML5 spec), so it ensures consistency
|
||
$$('[id]').forEach(function(element) { window[element.id] = element; });
|
||
|
||
// Array#splice but for strings
|
||
String.prototype.splice = function(i, remove, add) {
|
||
remove = +remove || 0;
|
||
add = add || '';
|
||
|
||
return this.slice(0,i) + add + this.slice(i + remove);
|
||
};
|
||
|
||
(function(){
|
||
|
||
var _ = window.Utopia = {
|
||
/**
|
||
* Returns the [[Class]] of an object in lowercase (eg. array, date, regexp, string etc)
|
||
* Caution: Results for DOM elements and collections aren't reliable.
|
||
* @param {Object} obj
|
||
*
|
||
* @return {String}
|
||
*/
|
||
type: function(obj) {
|
||
if(obj === null) { return 'null'; }
|
||
|
||
if(obj === undefined) { return 'undefined'; }
|
||
|
||
var ret = Object.prototype.toString.call(obj).match(/^\[object\s+(.*?)\]$/)[1];
|
||
|
||
ret = ret? ret.toLowerCase() : '';
|
||
|
||
if(ret == 'number' && isNaN(obj)) {
|
||
return 'NaN';
|
||
}
|
||
|
||
return ret;
|
||
},
|
||
|
||
/**
|
||
* Iterate over the properties of an object. Checks whether the properties actually belong to it.
|
||
* Can be stopped if the function explicitly returns a value that isn't null, undefined or NaN.
|
||
*
|
||
* @param obj {Object} The object to iterate over
|
||
* @param func {Function} The function used in the iteration. Can accept 2 parameters: one of the
|
||
* value of the object and one for its name.
|
||
* @param context {Object} Context for the above function. Default is the object being iterated.
|
||
*
|
||
* @return {Object} Null or the return value of func, if it broke the loop at some point.
|
||
*/
|
||
each: function(obj, func, context) {
|
||
if(!_.type(func) == 'function') {
|
||
throw Error('The second argument in Utopia.each() must be a function');
|
||
};
|
||
|
||
context = context || obj;
|
||
|
||
for (var i in obj) {
|
||
if(obj.hasOwnProperty && obj.hasOwnProperty(i)) {
|
||
var ret = func.call(context, obj[i], i);
|
||
|
||
if(!!ret || ret === 0 || ret === '') {
|
||
return ret;
|
||
}
|
||
}
|
||
}
|
||
|
||
return null;
|
||
},
|
||
|
||
/**
|
||
* Copies the properties of one object onto another.
|
||
* When there is a collision, the later one wins
|
||
*
|
||
* @return {Object} destination object
|
||
*/
|
||
merge: function(objects) {
|
||
var ret = {};
|
||
|
||
for(var i=0; i<arguments.length; i++) {
|
||
var o = arguments[i];
|
||
|
||
for(var j in o) {
|
||
ret[j] = o[j];
|
||
}
|
||
}
|
||
|
||
return ret;
|
||
},
|
||
|
||
/**
|
||
* Copies the properties of one or more objects onto the first one
|
||
* When there is a collision, the first object wins
|
||
*/
|
||
attach: function(object, objects) {
|
||
for(var i=0; i<arguments.length; i++) {
|
||
var o = arguments[i];
|
||
|
||
for(var j in o) {
|
||
if(!(j in object)) {
|
||
object[j] = o[j];
|
||
}
|
||
}
|
||
}
|
||
|
||
return object;
|
||
},
|
||
|
||
element: {
|
||
/**
|
||
* Creates a new DOM element
|
||
* @param options {Object} A set of key/value pairs for attributes, properties, contents, placement in the DOM etc
|
||
* @return The new DOM element
|
||
*/
|
||
create: function() {
|
||
var options;
|
||
|
||
if(_.type(arguments[0]) === 'string') {
|
||
if(_.type(arguments[1]) === 'object') {
|
||
// Utopia.element.create('div', { ... });
|
||
options = arguments[1];
|
||
options.tag = arguments[0];
|
||
}
|
||
else {
|
||
// Utopia.element.create('div', ...);
|
||
options = {
|
||
tag: arguments[0]
|
||
};
|
||
|
||
// Utopia.element.create('div', [contents]);
|
||
if(_.type(arguments[1]) === 'array') {
|
||
options.contents = arguments[1];
|
||
}
|
||
// Utopia.element.create('div', 'Text contents');
|
||
else if(_.type(arguments[1]) === 'string' || _.type(arguments[1]) === 'number') {
|
||
options.contents = ['' + arguments[1]];
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
options = arguments[0];
|
||
}
|
||
|
||
var namespace = options.namespace || '', element;
|
||
|
||
if(namespace) {
|
||
element = document.createElementNS(namespace, options.tag);
|
||
}
|
||
else {
|
||
element = document.createElement(options.tag);
|
||
}
|
||
|
||
if (options.className || options.id) {
|
||
options.properties = options.properties || {};
|
||
options.properties.className = options.className;
|
||
options.properties.id = options.id;
|
||
}
|
||
|
||
// Set properties, attributes and contents
|
||
_.element.set(element, options);
|
||
|
||
// Place the element in the DOM (inside, before or after an existing element)
|
||
// This could be a selector
|
||
if(options.before) {
|
||
var before = $(options.before);
|
||
|
||
if (before && before.parentNode) {
|
||
before.parentNode.insertBefore(element, before);
|
||
}
|
||
}
|
||
|
||
if (options.after && element.parentNode === null) {
|
||
var after = $(options.after);
|
||
|
||
if (after && after.parentNode) {
|
||
after.parentNode.insertBefore(element, after.nextSibling)
|
||
}
|
||
}
|
||
|
||
if (options.inside && element.parentNode === null) {
|
||
$(options.inside).appendChild(element);
|
||
}
|
||
|
||
return element;
|
||
},
|
||
|
||
set: function(element, options) {
|
||
_.element.prop(element, options.properties || options.prop);
|
||
|
||
_.element.attr(element, options.attributes || options.attr);
|
||
|
||
_.element.contents(element, options.contents);
|
||
|
||
return element;
|
||
},
|
||
|
||
prop: function (element, properties) {
|
||
if (properties) {
|
||
for (var prop in properties) {
|
||
element[prop] = properties[prop];
|
||
}
|
||
}
|
||
|
||
return element;
|
||
},
|
||
|
||
attr: function (element, attributes) {
|
||
if (attributes) {
|
||
for (attr in attributes) {
|
||
element.setAttribute(attr, attributes[attr]);
|
||
}
|
||
}
|
||
|
||
return element;
|
||
},
|
||
|
||
/**
|
||
* Sets an element’s contents
|
||
* Contents could be: One or multiple (as an array) of the following:
|
||
* - An object literal that will be passed through Utopia.element.create
|
||
* - A string or number, which will become a text node
|
||
* - An existing DOM element
|
||
*/
|
||
contents: function (element, contents, where) {
|
||
if(contents || contents === 0) {
|
||
if (_.type(contents) !== 'array') {
|
||
contents = [contents];
|
||
}
|
||
|
||
var firstChild = element.firstChild;
|
||
|
||
for (var i=0; i<contents.length; i++) {
|
||
var content = contents[i], child;
|
||
|
||
switch(_.type(content)) {
|
||
case 'string':
|
||
if(content === '') {
|
||
continue;
|
||
}
|
||
// fall through
|
||
case 'number':
|
||
child = document.createTextNode(content);
|
||
break;
|
||
case 'object':
|
||
child = _.element.create(content);
|
||
break;
|
||
default:
|
||
child = content;
|
||
|
||
}
|
||
|
||
if(child) {
|
||
if (!where || where === 'end') {
|
||
element.appendChild(child);
|
||
}
|
||
else if (where === 'start') {
|
||
element.insertBefore(child, firstChild);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return element;
|
||
}
|
||
},
|
||
|
||
elements: {
|
||
// set, attr, prop, contents functions from Utopia.element, but for multiple elements
|
||
},
|
||
|
||
event: {
|
||
/**
|
||
* Binds one or more events to one or more elements
|
||
*/
|
||
bind: function(target, event, callback, traditional) {
|
||
if(_.type(target) === 'string' || _.type(target) === 'array') {
|
||
var elements = _.type(target) === 'string'? $$(target) : target;
|
||
|
||
elements.forEach(function(element) {
|
||
_.event.bind(element, event, callback, traditional);
|
||
});
|
||
}
|
||
else if(_.type(event) === 'string') {
|
||
if(traditional) {
|
||
target['on' + event] = callback;
|
||
}
|
||
else {
|
||
target.addEventListener(event, callback, false);
|
||
}
|
||
}
|
||
else if(_.type(event) === 'array') {
|
||
for (var i=0; i<event.length; i++) {
|
||
_.event.bind(target, event[i], callback, arguments[2]);
|
||
}
|
||
}
|
||
else {
|
||
for (var name in event) {
|
||
_.event.bind(target, name, event[name], arguments[2]);
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Fire a custom event
|
||
*/
|
||
fire: function(target, type, properties) {
|
||
if(_.type(target) === 'string' || _.type(target) === 'array') {
|
||
var elements = _.type(target) === 'string'? $$(target) : target;
|
||
|
||
elements.forEach(function(element) {
|
||
_.event.fire(element, type, properties);
|
||
});
|
||
}
|
||
else if (document.createEvent) {
|
||
var evt = document.createEvent("HTMLEvents");
|
||
|
||
evt.initEvent(type, true, true );
|
||
evt.custom = true;
|
||
|
||
if(properties) {
|
||
_.attach(evt, properties);
|
||
}
|
||
|
||
target.dispatchEvent(evt);
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Helper for XHR requests
|
||
*/
|
||
xhr: function(o) {
|
||
document.body.setAttribute('data-loading', '');
|
||
|
||
var xhr = new XMLHttpRequest(),
|
||
method = o.method || 'GET',
|
||
data = o.data || '';
|
||
|
||
xhr.open(method, o.url + (method === 'GET' && data? '?' + data : ''), true);
|
||
|
||
o.headers = o.headers || {};
|
||
|
||
if(method !== 'GET' && !o.headers['Content-type'] && !o.headers['Content-Type']) {
|
||
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||
}
|
||
|
||
for (var header in o.headers) {
|
||
xhr.setRequestHeader(header, o.headers[header]);
|
||
}
|
||
|
||
xhr.onreadystatechange = function(){
|
||
|
||
if(xhr.readyState === 4) {
|
||
document.body.removeAttribute('data-loading');
|
||
|
||
o.callback(xhr);
|
||
}
|
||
};
|
||
|
||
xhr.send(method === 'GET'? null : data);
|
||
|
||
return xhr;
|
||
},
|
||
|
||
/**
|
||
* Lazy loads an external script
|
||
*/
|
||
script: function(url, callback, doc) {
|
||
doc = doc || document;
|
||
|
||
return _.element.create({
|
||
tag: 'script',
|
||
properties: {
|
||
src: url,
|
||
async: true,
|
||
onload: callback
|
||
},
|
||
inside: doc.documentElement
|
||
});
|
||
},
|
||
|
||
/**
|
||
* Returns the absolute X, Y offsets for an element
|
||
*/
|
||
offset: function(element) {
|
||
var left = 0, top = 0, el = element;
|
||
|
||
if (el.parentNode) {
|
||
do {
|
||
left += el.offsetLeft;
|
||
top += el.offsetTop;
|
||
} while ((el = el.offsetParent) && el.nodeType < 9);
|
||
|
||
el = element;
|
||
|
||
do {
|
||
left -= el.scrollLeft;
|
||
top -= el.scrollTop;
|
||
} while ((el = el.parentNode) && !/body/i.test(el.nodeName));
|
||
}
|
||
|
||
return {
|
||
top: top,
|
||
right: innerWidth - left - element.offsetWidth,
|
||
bottom: innerHeight - top - element.offsetHeight,
|
||
left: left,
|
||
};
|
||
}
|
||
};
|
||
|
||
['set', 'prop', 'attr', 'contents'].forEach(function(method) {
|
||
_.elements[method] = function(elements) {
|
||
elements = _.type(elements) === 'string'? $$(elements) : Array.prototype.slice.call(elements);
|
||
|
||
var args = Array.prototype.slice.call(arguments);
|
||
args.shift(); // Remove the elements argument
|
||
|
||
elements = elements.map(function(element) {
|
||
return _.element[method](element, args);
|
||
});
|
||
|
||
return elements;
|
||
}
|
||
});
|
||
|
||
})();
|
||
|
||
window.$u = window.$u || Utopia; |