Friday, October 2, 2009

Pieces of my core javascript library: The Script Loader

I'm about to release a few more open source SharePoint solutions, including a core javascript library feature. The CoreJS is a required feature for the hierarchical navigation component, which will also be properly released as OSS on CodePlex before long.

A central piece of the CoreJS library and activation feature, is the Script Loader. This is in fact the only piece of the library which is automatically injected (as a delegate control) into any page in the site collection it's activated for.

The Script Loader's purpose is pretty much exactly what it sounds like: it's a general purpose way of loading scripts from javascript code. A sample scenario would e.g. be if you need a few external utility library, but you don't know if it has been loaded yet. Using the script loader, this would amount to:


function loadComplete()
{
alert("The scripts are loaded, and we're good to go!");
}

window.scriptloader.loadScripts(
"http://www.json.org/json2.js",
"myJsonAjaxLibrary.js",
loadComplete
);

The Script Loader would ensure that all the scripts are loaded in order, so in this above scenario, where the fictional myJsonAjaxLibrary relies on json2.js to do its thing, myJsonAjaxLibrary would not be loaded at all until json2 is present and ready. Additionally, we can supply a function as a parameter to loadScripts, which is called once all previous tasks have completed.

Making more calls to loadScripts would queue up even more scripts to be loaded. Imagine the following turn of events:

// On top of your script
window.scriptloader.loadScripts("http://www.json.org/json2.js");

// ... Meanwhile (actually later) in some distant part of the same script
window.scriptloader.loadScripts(
"myLibrary.js",
function() { alert("myLibrary loaded"); },
"http://www.json.org/json2.js",
function() { alert("json2 loaded"); });

It's pretty common for different parts of your script setup to require different libraries, and yet other times they even require the same scripts (whew). The script loader library does two things here: It will load all scripts in order, even between calls to loadScripts. In the above case, myLibrary would *not* be loaded until json2.js has been loaded from json.org; since that was specified in the first call to loadScripts. Second, it it will check each source url, to make sure that it hasn't already been loaded. If it's already loaded; it will be skipped.

In the above case, the actual sequence of events would be:
  1. load json2
  2. load myLibrary
  3. alert(myLibrary loaded)
  4. alert(json2 loaded)
In case of an error, the global error handler (window.scriptloader.errorCallback) will be called. Override this to take on all errors. In addition to this, if the failed script queue item was defined as an object on the form { src: string, onload: function, onerror: function }, the onerror callback will be called prior to the global error handler. Both the global error handler and the queue item local error handler receives arguments: event, failedScriptAddress, restOfQueue. Upon an error, the rest of the load queue will be cleared, and continued loading will be stopped. An error handler can continue the load, by calling window.scriptloader.loadScripts(restOfQueue), where restOfQueue is the error handler argument.

Here's the full script, ready for inclusion.

window.scriptloader = window.scriptloader || {
nocache: false,
errorCallback: function(event, failedScript, restOfQueue) {},
loadScripts: function() {
for (var i = 0; i < arguments.length; ++i) {
var arg = arguments[i];
var type = Object.prototype.toString.apply(arg);
if (type === "[object Function]" || type === "[object String]") {
window.scriptloader._queue.push(arg);
}
else if (type === "[object Array]" && arg.length > 0) {
for (var x = 0; x < arg.length; ++x) {
window.scriptloader._queue.push(arg[x]);
}
}
else if (type === "[object Object]" && typeof arg.src !== "undefined") {
// Expects an object such as:
// {
// src: "string",
// onload: function() {}, // optional
// onerror: function(event, failedScript, restOfQueue) {} // optional
// }
window.scriptloader._queue.push(arg);
}
}
if (!window.scriptloader._active) {
window.scriptloader._active = true;
window.scriptloader._processNextQueueItem();
}
},

/* Private interface */
_active: false,
_queue: [],
_loaded: {},
_processNextQueueItem: function() {
if (window.scriptloader._queue.length > 0) {
var item = window.scriptloader._queue.shift();
var type = Object.prototype.toString.apply(item);
if (type === "[object Function]") {
item();
window.scriptloader._processNextQueueItem();
}
else if (type === "[object String]") {
if (!window.scriptloader._loaded[item]) {
window.scriptloader._loaded[item] = true;
window.scriptloader._loadScript(item, window.scriptloader._processNextQueueItem);
}
else {
window.scriptloader._processNextQueueItem();
}
}
else if (type === "[object Object]") {
if (!window.scriptloader._loaded[item.src]) {
window.scriptloader._loaded[item.src] = true;
var onload = item.onload || function() {};
var onerror = item.onerror || function() {};
window.scriptloader._loadScript(item.src, function() {
item.onload();
window.scriptloader._processNextQueueItem();
}, item.onerror);
}
else {
window.scriptloader._processNextQueueItem();
}
}
else _processNextQueueItem();
}
else {
window.scriptloader._active = false;
}
},
_loadScript: function(src, onload, onerror) {
var errorHandler = function(e) {
window.scriptloader._active = false;
delete window.scriptloader._loaded[src];
var restOfQueue = window.scriptloader._queue.splice(0, window.scriptloader._queue.length);
if (onerror) onerror(e, src, restOfQueue);
window.scriptloader.errorCallback(e, src, restOfQueue);
};
var js = document.createElement('script');
js.src = src + (window.scriptloader.nocache ? "?" + Math.random() : "");
js.type = 'text/javascript';
if (js.readyState) {
// IE handling isn't very pretty
var detacher = null;
if (window.attachEvent) {
var escapedSrc = window.scriptloader._regexpEscape(src);
var windowErrorHandler = function(msg, url, line) {
// See if the window error was for our target script
if (url.search(new RegExp("/" + escapedSrc + "$")) != -1) {
if (detacher) detacher();
errorHandler(null);
}
};
window.attachEvent("onerror", windowErrorHandler);
detacher = function() { window.detachEvent("onerror", windowErrorHandler); };
}
else {
js.onerror = errorHandler;
}
js.onreadystatechange = function() {
if (detacher) detacher();
if (js.readyState === "loaded" || js.readyState === "complete") {
js.onreadystatechange = null;
if (onload) {
onload(src);
}
}
};
}
else {
// Other browsers, on the other hand
js.onerror = errorHandler;
js.onload = function() {
if (onload) {
onload(src);
}
};
}
var head = document.getElementsByTagName('head')[0];
if (head) {
head.appendChild(js);
}
else {
document.body.appendChild(js);
}
},
_regexpEscape: function(string) {
return string.replace(/[\*\\\.\^\$\[\]\(\)]/g, function(m) { return "\\" + m; });
}
};

0 kommentarer: