You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
308 lines
7.2 KiB
308 lines
7.2 KiB
loadjs = (function () { |
|
/** |
|
* Global dependencies. |
|
* @global {Object} document - DOM |
|
*/ |
|
|
|
var devnull = function() {}, |
|
bundleIdCache = {}, |
|
bundleResultCache = {}, |
|
bundleCallbackQueue = {}; |
|
|
|
|
|
/** |
|
* Subscribe to bundle load event. |
|
* @param {string[]} bundleIds - Bundle ids |
|
* @param {Function} callbackFn - The callback function |
|
*/ |
|
function subscribe(bundleIds, callbackFn) { |
|
// listify |
|
bundleIds = bundleIds.push ? bundleIds : [bundleIds]; |
|
|
|
var depsNotFound = [], |
|
i = bundleIds.length, |
|
numWaiting = i, |
|
fn, |
|
bundleId, |
|
r, |
|
q; |
|
|
|
// define callback function |
|
fn = function (bundleId, pathsNotFound) { |
|
if (pathsNotFound.length) depsNotFound.push(bundleId); |
|
|
|
numWaiting--; |
|
if (!numWaiting) callbackFn(depsNotFound); |
|
}; |
|
|
|
// register callback |
|
while (i--) { |
|
bundleId = bundleIds[i]; |
|
|
|
// execute callback if in result cache |
|
r = bundleResultCache[bundleId]; |
|
if (r) { |
|
fn(bundleId, r); |
|
continue; |
|
} |
|
|
|
// add to callback queue |
|
q = bundleCallbackQueue[bundleId] = bundleCallbackQueue[bundleId] || []; |
|
q.push(fn); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Publish bundle load event. |
|
* @param {string} bundleId - Bundle id |
|
* @param {string[]} pathsNotFound - List of files not found |
|
*/ |
|
function publish(bundleId, pathsNotFound) { |
|
// exit if id isn't defined |
|
if (!bundleId) return; |
|
|
|
var q = bundleCallbackQueue[bundleId]; |
|
|
|
// cache result |
|
bundleResultCache[bundleId] = pathsNotFound; |
|
|
|
// exit if queue is empty |
|
if (!q) return; |
|
|
|
// empty callback queue |
|
while (q.length) { |
|
q[0](bundleId, pathsNotFound); |
|
q.splice(0, 1); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Execute callbacks. |
|
* @param {Object or Function} args - The callback args |
|
* @param {string[]} depsNotFound - List of dependencies not found |
|
*/ |
|
function executeCallbacks(args, depsNotFound) { |
|
// accept function as argument |
|
if (args.call) args = {success: args}; |
|
|
|
// success and error callbacks |
|
if (depsNotFound.length) (args.error || devnull)(depsNotFound); |
|
else (args.success || devnull)(args); |
|
} |
|
|
|
|
|
/** |
|
* Load individual file. |
|
* @param {string} path - The file path |
|
* @param {Function} callbackFn - The callback function |
|
*/ |
|
function loadFile(path, callbackFn, args, numTries) { |
|
var doc = document, |
|
async = args.async, |
|
maxTries = (args.numRetries || 0) + 1, |
|
beforeCallbackFn = args.before || devnull, |
|
pathname = path.replace(/[\?|#].*$/, ''), |
|
pathStripped = path.replace(/^(css|img)!/, ''), |
|
isLegacyIECss, |
|
e; |
|
|
|
numTries = numTries || 0; |
|
|
|
if (/(^css!|\.css$)/.test(pathname)) { |
|
// css |
|
e = doc.createElement('link'); |
|
e.rel = 'stylesheet'; |
|
e.href = pathStripped; |
|
|
|
// tag IE9+ |
|
isLegacyIECss = 'hideFocus' in e; |
|
|
|
// use preload in IE Edge (to detect load errors) |
|
if (isLegacyIECss && e.relList) { |
|
isLegacyIECss = 0; |
|
e.rel = 'preload'; |
|
e.as = 'style'; |
|
} |
|
} else if (/(^img!|\.(png|gif|jpg|svg|webp)$)/.test(pathname)) { |
|
// image |
|
e = doc.createElement('img'); |
|
e.src = pathStripped; |
|
} else { |
|
// javascript |
|
e = doc.createElement('script'); |
|
e.src = path; |
|
e.async = async === undefined ? true : async; |
|
} |
|
|
|
e.onload = e.onerror = e.onbeforeload = function (ev) { |
|
var result = ev.type[0]; |
|
|
|
// treat empty stylesheets as failures to get around lack of onerror |
|
// support in IE9-11 |
|
if (isLegacyIECss) { |
|
try { |
|
if (!e.sheet.cssText.length) result = 'e'; |
|
} catch (x) { |
|
// sheets objects created from load errors don't allow access to |
|
// `cssText` (unless error is Code:18 SecurityError) |
|
if (x.code != 18) result = 'e'; |
|
} |
|
} |
|
|
|
// handle retries in case of load failure |
|
if (result == 'e') { |
|
// increment counter |
|
numTries += 1; |
|
|
|
// exit function and try again |
|
if (numTries < maxTries) { |
|
return loadFile(path, callbackFn, args, numTries); |
|
} |
|
} else if (e.rel == 'preload' && e.as == 'style') { |
|
// activate preloaded stylesheets |
|
return e.rel = 'stylesheet'; // jshint ignore:line |
|
} |
|
|
|
// execute callback |
|
callbackFn(path, result, ev.defaultPrevented); |
|
}; |
|
|
|
// add to document (unless callback returns `false`) |
|
if (beforeCallbackFn(path, e) !== false) doc.head.appendChild(e); |
|
} |
|
|
|
|
|
/** |
|
* Load multiple files. |
|
* @param {string[]} paths - The file paths |
|
* @param {Function} callbackFn - The callback function |
|
*/ |
|
function loadFiles(paths, callbackFn, args) { |
|
// listify paths |
|
paths = paths.push ? paths : [paths]; |
|
|
|
var numWaiting = paths.length, |
|
x = numWaiting, |
|
pathsNotFound = [], |
|
fn, |
|
i; |
|
|
|
// define callback function |
|
fn = function(path, result, defaultPrevented) { |
|
// handle error |
|
if (result == 'e') pathsNotFound.push(path); |
|
|
|
// handle beforeload event. If defaultPrevented then that means the load |
|
// will be blocked (ex. Ghostery/ABP on Safari) |
|
if (result == 'b') { |
|
if (defaultPrevented) pathsNotFound.push(path); |
|
else return; |
|
} |
|
|
|
numWaiting--; |
|
if (!numWaiting) callbackFn(pathsNotFound); |
|
}; |
|
|
|
// load scripts |
|
for (i=0; i < x; i++) loadFile(paths[i], fn, args); |
|
} |
|
|
|
|
|
/** |
|
* Initiate script load and register bundle. |
|
* @param {(string|string[])} paths - The file paths |
|
* @param {(string|Function|Object)} [arg1] - The (1) bundleId or (2) success |
|
* callback or (3) object literal with success/error arguments, numRetries, |
|
* etc. |
|
* @param {(Function|Object)} [arg2] - The (1) success callback or (2) object |
|
* literal with success/error arguments, numRetries, etc. |
|
*/ |
|
function loadjs(paths, arg1, arg2) { |
|
var bundleId, |
|
args; |
|
|
|
// bundleId (if string) |
|
if (arg1 && arg1.trim) bundleId = arg1; |
|
|
|
// args (default is {}) |
|
args = (bundleId ? arg2 : arg1) || {}; |
|
|
|
// throw error if bundle is already defined |
|
if (bundleId) { |
|
if (bundleId in bundleIdCache) { |
|
throw "LoadJS"; |
|
} else { |
|
bundleIdCache[bundleId] = true; |
|
} |
|
} |
|
|
|
function loadFn(resolve, reject) { |
|
loadFiles(paths, function (pathsNotFound) { |
|
// execute callbacks |
|
executeCallbacks(args, pathsNotFound); |
|
|
|
// resolve Promise |
|
if (resolve) { |
|
executeCallbacks({success: resolve, error: reject}, pathsNotFound); |
|
} |
|
|
|
// publish bundle load event |
|
publish(bundleId, pathsNotFound); |
|
}, args); |
|
} |
|
|
|
if (args.returnPromise) return new Promise(loadFn); |
|
else loadFn(); |
|
} |
|
|
|
|
|
/** |
|
* Execute callbacks when dependencies have been satisfied. |
|
* @param {(string|string[])} deps - List of bundle ids |
|
* @param {Object} args - success/error arguments |
|
*/ |
|
loadjs.ready = function ready(deps, args) { |
|
// subscribe to bundle load event |
|
subscribe(deps, function (depsNotFound) { |
|
// execute callbacks |
|
executeCallbacks(args, depsNotFound); |
|
}); |
|
|
|
return loadjs; |
|
}; |
|
|
|
|
|
/** |
|
* Manually satisfy bundle dependencies. |
|
* @param {string} bundleId - The bundle id |
|
*/ |
|
loadjs.done = function done(bundleId) { |
|
publish(bundleId, []); |
|
}; |
|
|
|
|
|
/** |
|
* Reset loadjs dependencies statuses |
|
*/ |
|
loadjs.reset = function reset() { |
|
bundleIdCache = {}; |
|
bundleResultCache = {}; |
|
bundleCallbackQueue = {}; |
|
}; |
|
|
|
|
|
/** |
|
* Determine if bundle has already been defined |
|
* @param String} bundleId - The bundle id |
|
*/ |
|
loadjs.isDefined = function isDefined(bundleId) { |
|
return bundleId in bundleIdCache; |
|
}; |
|
|
|
|
|
// export |
|
return loadjs; |
|
|
|
})();
|
|
|