Skip to content

Instantly share code, notes, and snippets.

@jankuca
Created January 17, 2011 12:33
Show Gist options
  • Save jankuca/782800 to your computer and use it in GitHub Desktop.
Save jankuca/782800 to your computer and use it in GitHub Desktop.
require.js for front-end
(function(doc) {
var head = doc.documentElement,
isHeadReady,
isDomReady,
domWaiters = [],
queue = [], // waiters for the "head ready" event
handlers = {}, // user functions waiting for events
scripts = {}, // loadable scripts in different states
isAsync = doc.createElement("script").async === true || "MozAppearance" in doc.documentElement.style || window.opera;
/*** public API ***/
var head_var = window.head_conf && head_conf.head || "require",
api = window[head_var] = (window[head_var] || function() { api.ready.apply(null, arguments); });
// states
var PRELOADED = 1,
PRELOADING = 2,
LOADING = 3,
LOADED = 4;
api.ROOT = '';
// Method 1: simply load and let browser take care of ordering
if (isAsync) {
api.js = function() {
var args = arguments,
fn = args[args.length -1],
els = {};
if (!isFunc(fn)) { fn = null; }
each(args, function(el, i) {
if (el != fn) {
el = getScript(el);
els[el.name] = el;
load(el, fn && i == args.length -2 ? function() {
if (allLoaded(els)) { one(fn); }
} : null);
}
});
return api;
};
// Method 2: preload with text/cache hack
} else {
api.js = function() {
var args = arguments,
rest = [].slice.call(args, 1),
next = rest[0];
// wait for a while. immediate execution causes some browsers to ignore caching
if (!isHeadReady) {
queue.push(function() {
api.js.apply(null, args);
});
return api;
}
// multiple arguments
if (next) {
// load
each(rest, function(el) {
if (!isFunc(el)) {
preload(getScript(el));
}
});
// execute
load(getScript(args[0]), isFunc(next) ? next : function() {
api.js.apply(null, rest);
});
// single script
} else {
load(getScript(args[0]));
}
return api;
};
}
api.ready = function(key, fn) {
// DOM ready check: head.ready(document, function() { });
if (key == doc) {
if (isDomReady) { one(fn); }
else { domWaiters.push(fn); }
return api;
}
// shift arguments
if (isFunc(key)) {
fn = key;
key = "ALL";
}
// make sure arguments are sane
if (typeof key != 'string' || !isFunc(fn)) { return api; }
var script = scripts[key];
// script already loaded --> execute and return
if (script && script.state == LOADED || key == 'ALL' && allLoaded() && isDomReady) {
one(fn);
return api;
}
var arr = handlers[key];
if (!arr) { arr = handlers[key] = [fn]; }
else { arr.push(fn); }
return api;
};
/*** private functions ***/
// call function once
function one(fn) {
if (fn._done) { return; }
fn();
fn._done = 1;
}
function toLabel(url) {
var els = url.split("/"),
name = els[els.length -1],
i = name.indexOf("?");
return i != -1 ? name.substring(0, i) : name;
}
function getScript(url) {
var script;
if (typeof url == 'object') {
for (var key in url) {
if (url[key]) {
script = { name: key, url: api.ROOT + url[key] };
}
}
} else {
script = { name: toLabel(url), url: api.ROOT + url };
}
var existing = scripts[script.name];
if (existing && existing.url === script.url) { return existing; }
scripts[script.name] = script;
return script;
}
function each(arr, fn) {
if (!arr) { return; }
// arguments special type
if (typeof arr == 'object') { arr = [].slice.call(arr); }
// do the job
for (var i = 0; i < arr.length; i++) {
fn.call(arr, arr[i], i);
}
}
function isFunc(el) {
return Object.prototype.toString.call(el) == '[object Function]';
}
function allLoaded(els) {
els = els || scripts;
var loaded;
for (var name in els) {
if (els.hasOwnProperty(name) && els[name].state != LOADED) { return false; }
loaded = true;
}
return loaded;
}
function onPreload(script) {
script.state = PRELOADED;
each(script.onpreload, function(el) {
el.call();
});
}
function preload(script, callback) {
if (script.state === undefined) {
script.state = PRELOADING;
script.onpreload = [];
scriptTag({ src: script.url, type: 'cache'}, function() {
onPreload(script);
});
}
}
function load(script, callback) {
if (script.state == LOADED) {
return callback && callback();
}
if (script.state == LOADING) {
return api.ready(script.name, callback);
}
if (script.state == PRELOADING) {
return script.onpreload.push(function() {
load(script, callback);
});
}
script.state = LOADING;
scriptTag(script.url, function() {
script.state = LOADED;
if (callback) { callback(); }
// handlers for this script
each(handlers[script.name], function(fn) {
one(fn);
});
// everything ready
if (allLoaded() && isDomReady) {
each(handlers.ALL, function(fn) {
one(fn);
});
}
});
}
function scriptTag(src, callback) {
var s = doc.createElement('script');
s.type = 'text/' + (src.type || 'javascript');
s.src = src.src || src;
s.async = false;
s.onreadystatechange = s.onload = function() {
var state = s.readyState;
if (!callback.done && (!state || /loaded|complete/.test(state))) {
callback.done = true;
callback();
}
};
// use body if available. more safe in IE
(doc.body || head).appendChild(s);
}
/*
The much desired DOM ready check
Thanks to jQuery and http://javascript.nwbox.com/IEContentLoaded/
*/
function fireReady() {
if (!isDomReady) {
isDomReady = true;
each(domWaiters, function(fn) {
one(fn);
});
}
}
// W3C
if (window.addEventListener) {
doc.addEventListener("DOMContentLoaded", fireReady, false);
// fallback. this is always called
window.addEventListener("load", fireReady, false);
// IE
} else if (window.attachEvent) {
// for iframes
doc.attachEvent("onreadystatechange", function() {
if (doc.readyState === "complete" ) {
fireReady();
}
});
// avoid frames with different domains issue
var frameElement = 1;
try {
frameElement = window.frameElement;
} catch(e) {}
if (!frameElement && head.doScroll) {
(function() {
try {
head.doScroll("left");
fireReady();
} catch(e) {
setTimeout(arguments.callee, 1);
return;
}
})();
}
// fallback
window.attachEvent("onload", fireReady);
}
// enable document.readyState for Firefox <= 3.5
if (!doc.readyState && doc.addEventListener) {
doc.readyState = "loading";
doc.addEventListener("DOMContentLoaded", handler = function () {
doc.removeEventListener("DOMContentLoaded", handler, false);
doc.readyState = "complete";
}, false);
}
/*
We wait for 300 ms before script loading starts. for some reason this is needed
to make sure scripts are cached. Not sure why this happens yet. A case study:
https://github.com/headjs/headjs/issues/closed#issue/83
*/
setTimeout(function() {
isHeadReady = true;
each(queue, function(fn) { fn(); });
}, 300);
})(document);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment