Skip to content

Instantly share code, notes, and snippets.

@Error601
Last active August 29, 2015 14:16
Show Gist options
  • Save Error601/0d1e3d172bcd35750286 to your computer and use it in GitHub Desktop.
Save Error601/0d1e3d172bcd35750286 to your computer and use it in GitHub Desktop.
loadScripts - run callback functions after a number of scripts have loaded (optionally into a specified element)
/*!
* The gist of this gist:
*
* This script is meant to be a *simple* way
* to ensure JavaScript dependencies are loaded
* before trying to run functions that need them.
* This is by no means an attempt to replace AMD
* modules or require.js - it's just a more lightweight
* option for loading dependencies.
*
* After successful loading of JavaScript files
* perform specified callback function(s).
* You can do a callback for each function
* and another callback after all scripts
* have loaded. Optionally, you can specify
* a DOM element where you want the scripts
* loaded (this script uses Element.appendChild
* rather than AJAX, so if you want to load your
* scripts inside the <body>, you can do so),
* otherwise they will be appended to the <head>.
*
* (will not re-load scripts that are already loaded)
*
* Yes, these functions are declared in the global
* scope because, well, it didn't really matter
* in the webapp where these are used. Call it
* blashpemy. Call it laziness. Whatever. Feel free
* to wrap them in closures or add them to your
* app's namespace.
*
*/
//////////////////////////////////////////////////
// DON'T RUN THIS FUNCTION (duh)
// FOR EXAMPLES ONLY
function usageExamples(){
// argument signature
loadScripts( /* array of scripts, (parent element or callback), callback */ );
// load multiple scripts and run callback after they load
loadScripts(['script1.js', 'script2.js'], function(){
doSomething();
doSomethingElse();
});
// call this below on success
function successCallback(script, status){
// 'script' references the actual
// <script> element just loaded
// good idea to make sure script actually loaded
if (status === 'ok'){
// do stuff
}
}
// call this below if there's an error
function errorCallback(script, status){
// we know the 'status' is 'error'
// do stuff if there's an error
}
// scripts can be configured as array of objects also
// this allows a callback after loading of each script
loadScripts(
[
{
url: 'script1.js',
// 'success', 'complete', or 'callback'
// they're all the same
success: successCallback,
error: errorCallback
},
{
url: 'script2.js',
// can also use 'complete' method name
complete: function(script, status){
// call anonymous function on success
// check status - if no 'error'
// function is specified, the 'callback'
// function will also run on error
if (status === 'ok'){
successCallback(script, status)
}
if (status === 'error'){
errorCallback(script, status)
}
}
}
],
// callback after all scripts are loaded
function(){
doSomethingAfterLastScriptLoads();
}
);
}
// end usageExamples();
//////////////////////////////////////////////////
// add HTML5 'data-' attributes to an element
function setElementData(element, name, val){
if (document.head && document.head.dataset){
element.dataset[name] = val;
}
else {
element.setAttribute('data-'+name, val);
}
}
function getScriptElements(){
var scripts = document.querySelectorAll('script[src]'),
scriptsArray = window.loadedScripts.slice() || [],
len = scripts.length,
i = -1,
src;
while (++i < len){
src = scripts[i].getAttribute('src');
if (scriptsArray.indexOf(src) === -1){
scriptsArray.push(src);
}
}
if (window.jsdebug){
console.log(scriptsArray);
}
return window.loadedScripts = scriptsArray;
}
// what scripts have loaded so far?
getScriptElements();
function hasScript( url ){
// fastest check
// (if window.loadedScripts has all currently loaded scripts)
if (window.loadedScripts.indexOf(url) > -1){
return true;
}
return getScriptElements().indexOf(url) > -1;
}
// splits params contained in a string into
// config object properties
function scriptParams( script ){
var obj = { url: '', min: '', name: '' };
if (isString(script)){
script = script.split('|');
obj.url = script[0] ? script[0].trim() : '';
obj.min = script[1] ? script[1].replace(/\*/,'').trim() : '';
obj.name = script[2] ? script[2].replace(/\*/,'').trim() : '';
obj.parent = script[3] ? script[3].replace(/\*/,'').trim() : '';
}
else if (isPlainObject(script)) {
script.url = script.src =
script.url || script.src; // tolerate use of 'src' prop name
extend(obj, script);
}
obj.src = obj.url = obj.url.replace(/\.js$/i,'') + obj.min + '.js';
return obj;
}
// returns DOM ELEMENT for <script>
function scriptElement( src, name ){
var _script = document.createElement('script');
_script.type = "text/javascript";
// fast-track empty script (why would we need this?)
if (!src){
return _script;
}
if (src && !hasScript(src)){
_script.src = src;
}
if (name){
_script.title = name;
setElementData(_script, 'name', name);
}
if (_script.src || src === '' || src === null){
return _script;
}
return document.createDocumentFragment();
}
// load a script,
// optionally into a specific parent element,
// and/or with a callback (optional)
function loadScript( /* script, parent/callback, callback */ ) {
var obj, parent, _parent,
script, status, callback,
noop = function(){},
args = arguments,
arg2 = args[1],
arg3 = args[2],
len = args.length,
done = false;
if (len === 0){
// no args no run
return;
}
if (len > 3){
console.log('max 3 arguments allowed');
return;
}
if (len >= 2){
// parent could be second argument
// if there are 2 or 3 args
parent = arg2;
callback = arg3;
}
// but arg2 *could* be a callback instead of the parent
if (len === 2 && isFunction(arg2)){
parent = 'head';
callback = arg2;
}
// process params input in string or object format
// returns params object
obj = scriptParams(args[0]);
obj.callback = obj.success || obj.complete || obj.callback || noop;
if (!isFunction(callback)){
callback = noop;
}
script = scriptElement(obj.src, obj.name);
script.url = obj.src;
script.onload = function(complete){
status = complete || 'ok';
if (!done) {
done = true;
window.loadedScripts.push(obj.src);
obj.callback(this, status); // callback function in config object
callback(this, status); // callback function argument
}
};
script.onreadystatechange = function(){
if (!done) {
if (this.readyState === 'complete') {
this.onload('complete');
}
}
};
script.onerror = function(){
status = 'error';
// prefer to call 'error' callback
obj.callback = obj.error || obj.callback;
if (!done) {
done = true;
obj.callback(this, status); // callback function in config object
callback(this, status);
}
};
// 'parent' param could be a separate argument for this function
// or a property property on the 'script' arg
_parent = document.querySelector(parent||obj.parent||'head');
_parent.appendChild(script);
}
// load multiple scripts
// (into the same parent, if specified)
// and run optional callbacks for each script
// and a final callback after all scripts are loaded
function loadScripts( scripts, parent_or_callback, callback ){
var i = -1, _script, _parent, _callback, len;
scripts = scripts.slice();
len = scripts.length;
if (len === 0){
// need args
return;
}
if (!callback){
if (isFunction(parent_or_callback)){
_parent = 'head';
_callback = parent_or_callback;
}
else {
_parent = parent_or_callback;
_callback = function(){};
}
}
else {
_callback = callback;
}
while (++i < len){
_script = scriptParams(scripts[i]);
_script.min = ''; // 'min's been added already
_script.parent = _script.parent || _parent;
if (i === len-1){
// do callback after *last* script has loaded
loadScript(_script, _callback);
}
else {
loadScript(_script);
}
}
}
//////////////////////////////////////////////////
// helper utility functions
//////////////////////////////////////////////////
function isString( str ){
return typeof str === 'string';
}
function isPlainObject( obj ){
return Object.prototype.toString.call(obj) === '[object Object]';
}
function isFunction( func ){
return typeof func == 'function';
}
function isArray( arr ){
if ( Array.isArray ) {
return Array.isArray(arr);
}
else {
return Object.prototype.toString.call(arr) === '[object Array]';
}
}
// convert array-like object or arguments to a real array
// (twice as fast as Array.prototype.slice.call(arguments))
function toArray(args) {
var i = -1,
len = args.length,
_args = new Array(len);
while (++i < len) {
_args[i] = args[i];
}
return _args;
}
// copy of jQuery's $.extend() method
function extend(){
var src, copyIsArray, copy, name, options, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
undefined;
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !isFunction(target) ) {
target = {};
}
// extend parent object if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null ) {
// Extend the base object
for ( name in options ) {
// don't check for this - extend everything
//if ( !options.hasOwnProperty(name) ) {
// continue;
//}
src = target[ name ];
copy = options[ name ];
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( isPlainObject(copy) || (copyIsArray = isArray(copy)) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && isArray(src) ? src : [];
}
else {
clone = src && isPlainObject(src) ? src : {};
}
// Never move original objects, clone them
target[ name ] = extend(deep, clone, copy);
// Don't bring in undefined values
}
else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// Return the modified object
return target;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment