/*! LAB.js (LABjs :: Loading And Blocking JavaScript) v2.0.3 (c) Kyle Simpson MIT License */ (function(global){ var _$LAB = global.$LAB, // constants for the valid keys of the options object _UseLocalXHR = "UseLocalXHR", _AlwaysPreserveOrder = "AlwaysPreserveOrder", _AllowDuplicates = "AllowDuplicates", _CacheBust = "CacheBust", /*!START_DEBUG*/_Debug = "Debug",/*!END_DEBUG*/ _BasePath = "BasePath", // stateless variables used across all $LAB instances root_page = /^[^?#]*\//.exec(location.href)[0], root_domain = /^\w+\:\/\/\/?[^\/]+/.exec(root_page)[0], append_to = document.head || document.getElementsByTagName("head"), // inferences... ick, but still necessary opera_or_gecko = (global.opera && Object.prototype.toString.call(global.opera) == "[object Opera]") || ("MozAppearance" in document.documentElement.style), /*!START_DEBUG*/ // console.log() and console.error() wrappers log_msg = function(){}, log_error = log_msg, /*!END_DEBUG*/ // feature sniffs (yay!) test_script_elem = document.createElement("script"), explicit_preloading = typeof test_script_elem.preload == "boolean", // http://wiki.whatwg.org/wiki/Script_Execution_Control#Proposal_1_.28Nicholas_Zakas.29 real_preloading = explicit_preloading || (test_script_elem.readyState && test_script_elem.readyState == "uninitialized"), // will a script preload with `src` set before DOM append? script_ordered_async = !real_preloading && test_script_elem.async === true, // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order // XHR preloading (same-domain) and cache-preloading (remote-domain) are the fallbacks (for some browsers) xhr_or_cache_preloading = !real_preloading && !script_ordered_async && !opera_or_gecko ; /*!START_DEBUG*/ // define console wrapper functions if applicable if (global.console && global.console.log) { if (!global.console.error) global.console.error = global.console.log; log_msg = function(msg) { global.console.log(msg); }; log_error = function(msg,err) { global.console.error(msg,err); }; } /*!END_DEBUG*/ // test for function function is_func(func) { return Object.prototype.toString.call(func) == "[object Function]"; } // test for array function is_array(arr) { return Object.prototype.toString.call(arr) == "[object Array]"; } // make script URL absolute/canonical function canonical_uri(src,base_path) { var absolute_regex = /^\w+\:\/\//; // is `src` is protocol-relative (begins with // or ///), prepend protocol if (/^\/\/\/?/.test(src)) { src = location.protocol + src; } // is `src` page-relative? (not an absolute URL, and not a domain-relative path, beginning with /) else if (!absolute_regex.test(src) && src.charAt(0) != "/") { // prepend `base_path`, if any src = (base_path || "") + src; } // make sure to return `src` as absolute return absolute_regex.test(src) ? src : ((src.charAt(0) == "/" ? root_domain : root_page) + src); } // merge `source` into `target` function merge_objs(source,target) { for (var k in source) { if (source.hasOwnProperty(k)) { target[k] = source[k]; // TODO: does this need to be recursive for our purposes? }} return target; } // does the chain group have any ready-to-execute scripts? function check_chain_group_scripts_ready(chain_group) { var any_scripts_ready = false; for (var i=0; i<chain_group.scripts.length; i++) { if (chain_group.scripts[i].ready && chain_group.scripts[i].exec_trigger) { any_scripts_ready = true; chain_group.scripts[i].exec_trigger(); chain_group.scripts[i].exec_trigger = null; } } return any_scripts_ready; } // creates a script load listener function create_script_load_listener(elem,registry_item,flag,onload) { elem.onload = elem.onreadystatechange = function() { if ((elem.readyState && elem.readyState != "complete" && elem.readyState != "loaded") || registry_item[flag]) return; elem.onload = elem.onreadystatechange = null; onload(); }; } // script executed handler function script_executed(registry_item) { registry_item.ready = registry_item.finished = true; for (var i=0; i<registry_item.finished_listeners.length; i++) { registry_item.finished_listeners[i](); } registry_item.ready_listeners = []; registry_item.finished_listeners = []; } // make the request for a scriptha function request_script(chain_opts,script_obj,registry_item,onload,preload_this_script) { // setTimeout() "yielding" prevents some weird race/crash conditions in older browsers setTimeout(function(){ var script, src = script_obj.real_src, xhr; // don't proceed until `append_to` is ready to append to if ("item" in append_to) { // check if `append_to` ref is still a live node list if (!append_to[0]) { // `append_to` node not yet ready // try again in a little bit -- note: will re-call the anonymous function in the outer setTimeout, not the parent `request_script()` setTimeout(arguments.callee,25); return; } // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists append_to = append_to[0]; } script = document.createElement("script"); if (script_obj.type) script.type = script_obj.type; if (script_obj.charset) script.charset = script_obj.charset; // should preloading be used for this script? if (preload_this_script) { // real script preloading? if (real_preloading) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload: "+src);/*!END_DEBUG*/ registry_item.elem = script; if (explicit_preloading) { // explicit preloading (aka, Zakas' proposal) script.preload = true; script.onpreload = onload; } else { script.onreadystatechange = function(){ if (script.readyState == "loaded") onload(); }; } script.src = src; // NOTE: no append to DOM yet, appending will happen when ready to execute } // same-domain and XHR allowed? use XHR preloading else if (preload_this_script && src.indexOf(root_domain) == 0 && chain_opts[_UseLocalXHR]) { xhr = new XMLHttpRequest(); // note: IE never uses XHR (it supports true preloading), so no more need for ActiveXObject fallback for IE <= 7 /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload (xhr): "+src);/*!END_DEBUG*/ xhr.onreadystatechange = function() { if (xhr.readyState == 4) { xhr.onreadystatechange = function(){}; // fix a memory leak in IE registry_item.text = xhr.responseText + "\n//@ sourceURL=" + src; // http://blog.getfirebug.com/2009/08/11/give-your-eval-a-name-with-sourceurl/ onload(); } }; xhr.open("GET",src); xhr.send(); } // as a last resort, use cache-preloading else { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload (cache): "+src);/*!END_DEBUG*/ script.type = "text/cache-script"; create_script_load_listener(script,registry_item,"ready",function() { append_to.removeChild(script); onload(); }); script.src = src; append_to.insertBefore(script,append_to.firstChild); } } // use async=false for ordered async? parallel-load-serial-execute http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order else if (script_ordered_async) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script load (ordered async): "+src);/*!END_DEBUG*/ script.async = false; create_script_load_listener(script,registry_item,"finished",onload); script.src = src; append_to.insertBefore(script,append_to.firstChild); } // otherwise, just a normal script element else { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script load: "+src);/*!END_DEBUG*/ create_script_load_listener(script,registry_item,"finished",onload); script.src = src; append_to.insertBefore(script,append_to.firstChild); } },0); } // create a clean instance of $LAB function create_sandbox() { var global_defaults = {}, can_use_preloading = real_preloading || xhr_or_cache_preloading, queue = [], registry = {}, instanceAPI ; // global defaults global_defaults[_UseLocalXHR] = true; global_defaults[_AlwaysPreserveOrder] = false; global_defaults[_AllowDuplicates] = false; global_defaults[_CacheBust] = false; /*!START_DEBUG*/global_defaults[_Debug] = false;/*!END_DEBUG*/ global_defaults[_BasePath] = ""; // execute a script that has been preloaded already function execute_preloaded_script(chain_opts,script_obj,registry_item) { var script; function preload_execute_finished() { if (script != null) { // make sure this only ever fires once script = null; script_executed(registry_item); } } if (registry[script_obj.src].finished) return; if (!chain_opts[_AllowDuplicates]) registry[script_obj.src].finished = true; script = registry_item.elem || document.createElement("script"); if (script_obj.type) script.type = script_obj.type; if (script_obj.charset) script.charset = script_obj.charset; create_script_load_listener(script,registry_item,"finished",preload_execute_finished); // script elem was real-preloaded if (registry_item.elem) { registry_item.elem = null; } // script was XHR preloaded else if (registry_item.text) { script.onload = script.onreadystatechange = null; // script injection doesn't fire these events script.text = registry_item.text; } // script was cache-preloaded else { script.src = script_obj.real_src; } append_to.insertBefore(script,append_to.firstChild); // manually fire execution callback for injected scripts, since events don't fire if (registry_item.text) { preload_execute_finished(); } } // process the script request setup function do_script(chain_opts,script_obj,chain_group,preload_this_script) { var registry_item, registry_items, ready_cb = function(){ script_obj.ready_cb(script_obj,function(){ execute_preloaded_script(chain_opts,script_obj,registry_item); }); }, finished_cb = function(){ script_obj.finished_cb(script_obj,chain_group); } ; script_obj.src = canonical_uri(script_obj.src,chain_opts[_BasePath]); script_obj.real_src = script_obj.src + // append cache-bust param to URL? (chain_opts[_CacheBust] ? ((/\?.*$/.test(script_obj.src) ? "&_" : "?_") + ~~(Math.random()*1E9) + "=") : "") ; if (!registry[script_obj.src]) registry[script_obj.src] = {items:[],finished:false}; registry_items = registry[script_obj.src].items; // allowing duplicates, or is this the first recorded load of this script? if (chain_opts[_AllowDuplicates] || registry_items.length == 0) { registry_item = registry_items[registry_items.length] = { ready:false, finished:false, ready_listeners:[ready_cb], finished_listeners:[finished_cb] }; request_script(chain_opts,script_obj,registry_item, // which callback type to pass? ( (preload_this_script) ? // depends on script-preloading function(){ registry_item.ready = true; for (var i=0; i<registry_item.ready_listeners.length; i++) { registry_item.ready_listeners[i](); } registry_item.ready_listeners = []; } : function(){ script_executed(registry_item); } ), // signal if script-preloading should be used or not preload_this_script ); } else { registry_item = registry_items[0]; if (registry_item.finished) { finished_cb(); } else { registry_item.finished_listeners.push(finished_cb); } } } // creates a closure for each separate chain spawned from this $LAB instance, to keep state cleanly separated between chains function create_chain() { var chainedAPI, chain_opts = merge_objs(global_defaults,{}), chain = [], exec_cursor = 0, scripts_currently_loading = false, group ; // called when a script has finished preloading function chain_script_ready(script_obj,exec_trigger) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("script preload finished: "+script_obj.real_src);/*!END_DEBUG*/ script_obj.ready = true; script_obj.exec_trigger = exec_trigger; advance_exec_cursor(); // will only check for 'ready' scripts to be executed } // called when a script has finished executing function chain_script_executed(script_obj,chain_group) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("script execution finished: "+script_obj.real_src);/*!END_DEBUG*/ script_obj.ready = script_obj.finished = true; script_obj.exec_trigger = null; // check if chain group is all finished for (var i=0; i<chain_group.scripts.length; i++) { if (!chain_group.scripts[i].finished) return; } // chain_group is all finished if we get this far chain_group.finished = true; advance_exec_cursor(); } // main driver for executing each part of the chain function advance_exec_cursor() { while (exec_cursor < chain.length) { if (is_func(chain[exec_cursor])) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("$LAB.wait() executing: "+chain[exec_cursor]);/*!END_DEBUG*/ try { chain[exec_cursor++](); } catch (err) { /*!START_DEBUG*/if (chain_opts[_Debug]) log_error("$LAB.wait() error caught: ",err);/*!END_DEBUG*/ } continue; } else if (!chain[exec_cursor].finished) { if (check_chain_group_scripts_ready(chain[exec_cursor])) continue; break; } exec_cursor++; } // we've reached the end of the chain (so far) if (exec_cursor == chain.length) { scripts_currently_loading = false; group = false; } } // setup next chain script group function init_script_chain_group() { if (!group || !group.scripts) { chain.push(group = {scripts:[],finished:true}); } } // API for $LAB chains chainedAPI = { // start loading one or more scripts script:function(){ for (var i=0; i<arguments.length; i++) { (function(script_obj,script_list){ var splice_args; if (!is_array(script_obj)) { script_list = [script_obj]; } for (var j=0; j<script_list.length; j++) { init_script_chain_group(); script_obj = script_list[j]; if (is_func(script_obj)) script_obj = script_obj(); if (!script_obj) continue; if (is_array(script_obj)) { // set up an array of arguments to pass to splice() splice_args = [].slice.call(script_obj); // first include the actual array elements we want to splice in splice_args.unshift(j,1); // next, put the `index` and `howMany` parameters onto the beginning of the splice-arguments array [].splice.apply(script_list,splice_args); // use the splice-arguments array as arguments for splice() j--; // adjust `j` to account for the loop's subsequent `j++`, so that the next loop iteration uses the same `j` index value continue; } if (typeof script_obj == "string") script_obj = {src:script_obj}; script_obj = merge_objs(script_obj,{ ready:false, ready_cb:chain_script_ready, finished:false, finished_cb:chain_script_executed }); group.finished = false; group.scripts.push(script_obj); do_script(chain_opts,script_obj,group,(can_use_preloading && scripts_currently_loading)); scripts_currently_loading = true; if (chain_opts[_AlwaysPreserveOrder]) chainedAPI.wait(); } })(arguments[i],arguments[i]); } return chainedAPI; }, // force LABjs to pause in execution at this point in the chain, until the execution thus far finishes, before proceeding wait:function(){ if (arguments.length > 0) { for (var i=0; i<arguments.length; i++) { chain.push(arguments[i]); } group = chain[chain.length-1]; } else group = false; advance_exec_cursor(); return chainedAPI; } }; // the first chain link API (includes `setOptions` only this first time) return { script:chainedAPI.script, wait:chainedAPI.wait, setOptions:function(opts){ merge_objs(opts,chain_opts); return chainedAPI; } }; } // API for each initial $LAB instance (before chaining starts) instanceAPI = { // main API functions setGlobalDefaults:function(opts){ merge_objs(opts,global_defaults); return instanceAPI; }, setOptions:function(){ return create_chain().setOptions.apply(null,arguments); }, script:function(){ return create_chain().script.apply(null,arguments); }, wait:function(){ return create_chain().wait.apply(null,arguments); }, // built-in queuing for $LAB `script()` and `wait()` calls // useful for building up a chain programmatically across various script locations, and simulating // execution of the chain queueScript:function(){ queue[queue.length] = {type:"script", args:[].slice.call(arguments)}; return instanceAPI; }, queueWait:function(){ queue[queue.length] = {type:"wait", args:[].slice.call(arguments)}; return instanceAPI; }, runQueue:function(){ var $L = instanceAPI, len=queue.length, i=len, val; for (;--i>=0;) { val = queue.shift(); $L = $L[val.type].apply(null,val.args); } return $L; }, // rollback `[global].$LAB` to what it was before this file was loaded, the return this current instance of $LAB noConflict:function(){ global.$LAB = _$LAB; return instanceAPI; }, // create another clean instance of $LAB sandbox:function(){ return create_sandbox(); } }; return instanceAPI; } // create the main instance of $LAB global.$LAB = create_sandbox(); /* The following "hack" was suggested by Andrea Giammarchi and adapted from: http://webreflection.blogspot.com/2009/11/195-chars-to-help-lazy-loading.html NOTE: this hack only operates in FF and then only in versions where document.readyState is not present (FF < 3.6?). The hack essentially "patches" the **page** that LABjs is loaded onto so that it has a proper conforming document.readyState, so that if a script which does proper and safe dom-ready detection is loaded onto a page, after dom-ready has passed, it will still be able to detect this state, by inspecting the now hacked document.readyState property. The loaded script in question can then immediately trigger any queued code executions that were waiting for the DOM to be ready. For instance, jQuery 1.4+ has been patched to take advantage of document.readyState, which is enabled by this hack. But 1.3.2 and before are **not** safe or fixed by this hack, and should therefore **not** be lazy-loaded by script loader tools such as LABjs. */ (function(addEvent,domLoaded,handler){ if (document.readyState == null && document[addEvent]){ document.readyState = "loading"; document[addEvent](domLoaded,handler = function(){ document.removeEventListener(domLoaded,handler,false); document.readyState = "complete"; },false); } })("addEventListener","DOMContentLoaded"); })(this);