Skip to content

Instantly share code, notes, and snippets.

@SlexAxton
Created February 7, 2012 21:34
Show Gist options
  • Save SlexAxton/1762161 to your computer and use it in GitHub Desktop.
Save SlexAxton/1762161 to your computer and use it in GitHub Desktop.
yepnope
/*yepnope1.5.2|WTFPL*/
// yepnope.js
// Version - 1.5.2
//
// by
// Alex Sexton - @SlexAxton - AlexSexton[at]gmail.com
// Ralph Holzmann - @ralphholzmann - ralphholzmann[at]gmail.com
//
// http://yepnopejs.com/
// https://github.com/SlexAxton/yepnope.js/
//
// Tri-license - WTFPL | MIT | BSD
//
// Please minify before use.
// Also available as Modernizr.load via the Modernizr Project
//
( function ( window, doc, undef ) {
var docElement = doc.documentElement,
sTimeout = window.setTimeout,
firstScript = doc.getElementsByTagName( "script" )[ 0 ],
toString = {}.toString,
execStack = [],
started = 0,
noop = function () {},
// Before you get mad about browser sniffs, please read:
// https://github.com/Modernizr/Modernizr/wiki/Undetectables
// If you have a better solution, we are actively looking to solve the problem
isGecko = ( "MozAppearance" in docElement.style ),
isGeckoLTE18 = isGecko && !! doc.createRange().compareNode,
insBeforeObj = isGeckoLTE18 ? docElement : firstScript.parentNode,
isIE = !! doc.attachEvent,
strJsElem = isGecko ? "object" : isIE ? "script" : "img",
strCssElem = isIE ? "script" : strJsElem,
isArray = Array.isArray || function ( obj ) {
return toString.call( obj ) == "[object Array]";
},
isObject = function ( obj ) {
return Object(obj) === obj;
},
isString = function ( s ) {
return typeof s == "string";
},
isFunction = function ( fn ) {
return toString.call( fn ) == "[object Function]";
},
globalFilters = [],
scriptCache = {},
prefixes = {
// key value pair timeout options
timeout : function( resourceObj, prefix_parts ) {
if ( prefix_parts.length ) {
resourceObj['timeout'] = prefix_parts[ 0 ];
}
return resourceObj;
}
},
handler,
yepnope;
/* Loader helper functions */
function isFileReady ( readyState ) {
// Check to see if any of the ways a file can be ready are available as properties on the file's element
return ( ! readyState || readyState == "loaded" || readyState == "complete" || readyState == "uninitialized" );
}
// Takes a preloaded js obj (changes in different browsers) and injects it into the head
// in the appropriate order
function injectJs ( src, cb, attrs, timeout, /* internal use */ err, internal ) {
var script = doc.createElement( "script" ),
done, i;
timeout = timeout || yepnope['errorTimeout'];
script.src = src;
// Add our extra attributes to the script element
for ( i in attrs ) {
script.setAttribute( i, attrs[ i ] );
}
cb = internal ? executeStack : ( cb || noop );
// Bind to load events
script.onreadystatechange = script.onload = function () {
if ( ! done && isFileReady( script.readyState ) ) {
// Set done to prevent this function from being called twice.
done = 1;
cb();
// Handle memory leak in IE
script.onload = script.onreadystatechange = null;
}
};
// 404 Fallback
sTimeout(function () {
if ( ! done ) {
done = 1;
// Might as well pass in an error-state if we fire the 404 fallback
cb(1);
}
}, timeout );
// Inject script into to document
// or immediately callback if we know there
// was previously a timeout error
err ? script.onload() : firstScript.parentNode.insertBefore( script, firstScript );
}
// Takes a preloaded css obj (changes in different browsers) and injects it into the head
function injectCss ( href, cb, attrs, timeout, /* Internal use */ err, internal ) {
// Create stylesheet link
var link = doc.createElement( "link" ),
done, i;
timeout = timeout || yepnope['errorTimeout'];
cb = internal ? executeStack : ( cb || noop );
// Add attributes
link.href = href;
link.rel = "stylesheet";
link.type = "text/css";
// Add our extra attributes to the link element
for ( i in attrs ) {
link.setAttribute( i, attrs[ i ] );
}
if ( ! err ) {
firstScript.parentNode.insertBefore( link, firstScript );
sTimeout(cb, 0);
}
}
function executeStack ( ) {
// shift an element off of the stack
var i = execStack.shift();
started = 1;
// if a is truthy and the first item in the stack has an src
if ( i ) {
// if it's a script, inject it into the head with no type attribute
if ( i['t'] ) {
// Inject after a timeout so FF has time to be a jerk about it and
// not double load (ignore the cache)
sTimeout( function () {
(i['t'] == "c" ? yepnope['injectCss'] : yepnope['injectJs'])( i['s'], 0, i['a'], i['x'], i['e'], 1 );
}, 0 );
}
// Otherwise, just call the function and potentially run the stack
else {
i();
executeStack();
}
}
else {
// just reset out of recursive mode
started = 0;
}
}
function preloadFile ( elem, url, type, splicePoint, dontExec, attrObj, timeout ) {
timeout = timeout || yepnope['errorTimeout'];
// Create appropriate element for browser and type
var preloadElem = {},
done = 0,
firstFlag = 0,
stackObject = {
"t": type, // type
"s": url, // src
//r: 0, // ready
"e": dontExec,// set to true if we don't want to reinject
"a": attrObj,
"x": timeout
};
// The first time (common-case)
if ( scriptCache[ url ] === 1 ) {
firstFlag = 1;
scriptCache[ url ] = [];
preloadElem = doc.createElement( elem );
}
function onload ( first ) {
// If the script/css file is loaded
if ( ! done && isFileReady( preloadElem.readyState ) ) {
// Set done to prevent this function from being called twice.
stackObject['r'] = done = 1;
! started && executeStack();
// Handle memory leak in IE
preloadElem.onload = preloadElem.onreadystatechange = null;
if ( first ) {
if ( elem != "img" ) {
sTimeout(function(){ insBeforeObj.removeChild( preloadElem ) }, 50);
}
for ( var i in scriptCache[ url ] ) {
if ( scriptCache[ url ].hasOwnProperty( i ) ) {
scriptCache[ url ][ i ].onload();
}
}
}
}
}
// Setting url to data for objects or src for img/scripts
if ( elem == "object" ) {
preloadElem.data = url;
} else {
preloadElem.src = url;
// Setting bogus script type to allow the script to be cached
preloadElem.type = elem;
}
// Don't let it show up visually
preloadElem.width = preloadElem.height = "0";
// Attach handlers for all browsers
preloadElem.onerror = preloadElem.onload = preloadElem.onreadystatechange = function(){
onload.call(this, firstFlag);
};
// inject the element into the stack depending on if it's
// in the middle of other scripts or not
execStack.splice( splicePoint, 0, stackObject );
// The only place these can't go is in the <head> element, since objects won't load in there
// so we have two options - insert before the head element (which is hard to assume) - or
// insertBefore technically takes null/undefined as a second param and it will insert the element into
// the parent last. We try the head, and it automatically falls back to undefined.
if ( elem != "img" ) {
// If it's the first time, or we've already loaded it all the way through
if ( firstFlag || scriptCache[ url ] === 2 ) {
insBeforeObj.insertBefore( preloadElem, isGeckoLTE18 ? null : firstScript );
// If something fails, and onerror doesn't fire,
// continue after a timeout.
sTimeout( onload, timeout );
}
else {
// instead of injecting, just hold on to it
scriptCache[ url ].push( preloadElem );
}
}
}
function load ( resource, type, dontExec, attrObj, timeout ) {
// If this method gets hit multiple times, we should flag
// that the execution of other threads should halt.
started = 0;
// We'll do 'j' for js and 'c' for css, yay for unreadable minification tactics
type = type || "j";
if ( isString( resource ) ) {
// if the resource passed in here is a string, preload the file
preloadFile( type == "c" ? strCssElem : strJsElem, resource, type, this['i']++, dontExec, attrObj, timeout );
} else {
// Otherwise it's a callback function and we can splice it into the stack to run
execStack.splice( this['i']++, 0, resource );
execStack.length == 1 && executeStack();
}
// OMG is this jQueries? For chaining...
return this;
}
// return the yepnope object with a fresh loader attached
function getYepnope () {
var y = yepnope;
y['loader'] = {
"load": load,
"i" : 0
};
return y;
}
/* End loader helper functions */
// Yepnope Function
yepnope = function ( needs ) {
var i,
need,
// start the chain as a plain instance
chain = this['yepnope']['loader'];
function satisfyPrefixes ( url ) {
// split all prefixes out
var parts = url.split( "!" ),
gLen = globalFilters.length,
origUrl = parts.pop(),
pLen = parts.length,
res = {
"url" : origUrl,
// keep this one static for callback variable consistency
"origUrl" : origUrl,
"prefixes" : parts
},
mFunc,
j,
prefix_parts;
// loop through prefixes
// if there are none, this automatically gets skipped
for ( j = 0; j < pLen; j++ ) {
prefix_parts = parts[ j ].split( '=' );
mFunc = prefixes[ prefix_parts.shift() ];
if ( mFunc ) {
res = mFunc( res, prefix_parts );
}
}
// Go through our global filters
for ( j = 0; j < gLen; j++ ) {
res = globalFilters[ j ]( res );
}
// return the final url
return res;
}
function getExtension ( url ) {
return url.split(".").pop().split("?").shift();
}
function loadScriptOrStyle ( input, callback, chain, index, testResult ) {
// run through our set of prefixes
var resource = satisfyPrefixes( input ),
autoCallback = resource['autoCallback'],
extension = getExtension( resource['url'] );
// if no object is returned or the url is empty/0 just exit the load
if ( resource['bypass'] ) {
return;
}
// Determine callback, if any
if ( callback ) {
callback = isFunction( callback ) ?
callback :
callback[ input ] ||
callback[ index ] ||
callback[ ( input.split( "/" ).pop().split( "?" )[ 0 ] ) ] ||
executeStack;
}
// if someone is overriding all normal functionality
if ( resource['instead'] ) {
return resource['instead']( input, callback, chain, index, testResult );
}
else {
// Handle if we've already had this url and it's completed loaded already
if ( scriptCache[ resource['url'] ] ) {
// don't let this execute again
resource['noexec'] = true;
}
else {
scriptCache[ resource['url'] ] = 1;
}
// Throw this into the queue
chain.load( resource['url'], ( ( resource['forceCSS'] || ( ! resource['forceJS'] && "css" == getExtension( resource['url'] ) ) ) ) ? "c" : undef, resource['noexec'], resource['attrs'], resource['timeout'] );
// If we have a callback, we'll start the chain over
if ( isFunction( callback ) || isFunction( autoCallback ) ) {
// Call getJS with our current stack of things
chain['load']( function () {
// Hijack yepnope and restart index counter
getYepnope();
// Call our callbacks with this set of data
callback && callback( resource['origUrl'], testResult, index );
autoCallback && autoCallback( resource['origUrl'], testResult, index );
// Override this to just a boolean positive
scriptCache[ resource['url'] ] = 2;
} );
}
}
}
function loadFromTestObject ( testObject, chain ) {
var testResult = !! testObject['test'],
group = testResult ? testObject['yep'] : testObject['nope'],
always = testObject['load'] || testObject['both'],
callback = testObject['callback'] || noop,
cbRef = callback,
complete = testObject['complete'] || noop,
needGroupSize,
callbackKey;
// Reusable function for dealing with the different input types
// NOTE:: relies on closures to keep 'chain' up to date, a bit confusing, but
// much smaller than the functional equivalent in this case.
function handleGroup ( needGroup, moreToCome ) {
if ( ! needGroup ) {
// Call the complete callback when there's nothing to load.
! moreToCome && complete();
}
// If it's a string
else if ( isString( needGroup ) ) {
// if it's a string, it's the last
if ( !moreToCome ) {
// Add in the complete callback to go at the end
callback = function () {
var args = [].slice.call( arguments );
cbRef.apply( this, args );
complete();
};
}
// Just load the script of style
loadScriptOrStyle( needGroup, callback, chain, 0, testResult );
}
// See if we have an object. Doesn't matter if it's an array or a key/val hash
// Note:: order cannot be guaranteed on an key value object with multiple elements
// since the for-in does not preserve order. Arrays _should_ go in order though.
else if ( isObject( needGroup ) ) {
// I hate this, but idk another way for objects.
needGroupSize = (function(){
var count = 0, i
for (i in needGroup ) {
if ( needGroup.hasOwnProperty( i ) ) {
count++;
}
}
return count;
})();
for ( callbackKey in needGroup ) {
// Safari 2 does not have hasOwnProperty, but not worth the bytes for a shim
// patch if needed. Kangax has a nice shim for it. Or just remove the check
// and promise not to extend the object prototype.
if ( needGroup.hasOwnProperty( callbackKey ) ) {
// Find the last added resource, and append to it's callback.
if ( ! moreToCome && ! ( --needGroupSize ) ) {
// If this is an object full of callbacks
if ( ! isFunction( callback ) ) {
// Add in the complete callback to go at the end
callback[ callbackKey ] = (function( innerCb ) {
return function () {
var args = [].slice.call( arguments );
innerCb && innerCb.apply( this, args );
complete();
};
})( cbRef[ callbackKey ] );
}
// If this is just a single callback
else {
callback = function () {
var args = [].slice.call( arguments );
cbRef.apply( this, args );
complete();
};
}
}
loadScriptOrStyle( needGroup[ callbackKey ], callback, chain, callbackKey, testResult );
}
}
}
}
// figure out what this group should do
handleGroup( group, !!always );
// Run our loader on the load/both group too
// the always stuff always loads second.
always && handleGroup( always );
}
// Someone just decides to load a single script or css file as a string
if ( isString( needs ) ) {
loadScriptOrStyle( needs, 0, chain, 0 );
}
// Normal case is likely an array of different types of loading options
else if ( isArray( needs ) ) {
// go through the list of needs
for( i = 0; i < needs.length; i++ ) {
need = needs[ i ];
// if it's a string, just load it
if ( isString( need ) ) {
loadScriptOrStyle( need, 0, chain, 0 );
}
// if it's an array, call our function recursively
else if ( isArray( need ) ) {
yepnope( need );
}
// if it's an object, use our modernizr logic to win
else if ( isObject( need ) ) {
loadFromTestObject( need, chain );
}
}
}
// Allow a single object to be passed in
else if ( isObject( needs ) ) {
loadFromTestObject( needs, chain );
}
};
// This publicly exposed function is for allowing
// you to add functionality based on prefixes on the
// string files you add. 'css!' is a builtin prefix
//
// The arguments are the prefix (not including the !) as a string
// and
// A callback function. This function is passed a resource object
// that can be manipulated and then returned. (like middleware. har.)
//
// Examples of this can be seen in the officially supported ie prefix
yepnope['addPrefix'] = function ( prefix, callback ) {
prefixes[ prefix ] = callback;
};
// A filter is a global function that every resource
// object that passes through yepnope will see. You can
// of course conditionally choose to modify the resource objects
// or just pass them along. The filter function takes the resource
// object and is expected to return one.
//
// The best example of a filter is the 'autoprotocol' officially
// supported filter
yepnope['addFilter'] = function ( filter ) {
globalFilters.push( filter );
};
// Default error timeout to 10sec - modify to alter
yepnope['errorTimeout'] = 1e4;
// Webreflection readystate hack
// safe for jQuery 1.4+ ( i.e. don't use yepnope with jQuery 1.3.2 )
// if the readyState is null and we have a listener
if ( doc.readyState == null && doc.addEventListener ) {
// set the ready state to loading
doc.readyState = "loading";
// call the listener
doc.addEventListener( "DOMContentLoaded", handler = function () {
// Remove the listener
doc.removeEventListener( "DOMContentLoaded", handler, 0 );
// Set it to ready
doc.readyState = "complete";
}, 0 );
}
// Attach loader &
// Leak it
window['yepnope'] = getYepnope();
// Exposing executeStack to better facilitate plugins
window['yepnope']['executeStack'] = executeStack;
window['yepnope']['injectJs'] = injectJs;
window['yepnope']['injectCss'] = injectCss;
})( this, document );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment