conversant/ad-libs.js

View on GitHub
lib/loadScript.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict';

var d = document,
    head = d.getElementsByTagName('head')[0];

var isFunction = function (fn) {
    return typeof fn === 'function';
};

var setScriptLoadedHandler = function(script, fn) {
    script.onload = fn;

    // Prevent 'Not implemented' errors in IE 8
    try {
        script.onreadystatechange = fn;
        script.onerror = fn;
    } catch (e) {}
};

var createScript = function (url, loaded, requestTimeout, target) {
    var script = d.createElement('script'),
        timeoutId;

    if (!target){
        target = head;
    }

    var done = function (evt) {
        var readyState = script.readyState,
            error = null;

        if (readyState && readyState !== 'loaded' && readyState !== 'complete') {
            return false;
        }

        if (evt && evt.type && evt.type === 'error') {
            error = 'script.onerror triggered';
        }

        if (timeoutId) {
            clearTimeout(timeoutId);
        }

        setScriptLoadedHandler(script, undefined);
        loaded(error, url);

        return true;
    };

    script.onload = done;
    script.onreadystatechange = done;
    setScriptLoadedHandler(script, done);

    // Asynchronous loading yields thread control until the next tick every time. This is only good if loading multiple
    // scripts in parallel. Because we have the thread now, it's better to keep control while we have it and run it now
    // in a locking fashion instead. This can be changed later when we move to json from jsonp.
//    script.async = true;
    script.src = url;

    target.appendChild(script);

    if (requestTimeout > 0) {
        timeoutId = setTimeout(function () {
            // Do not cancel the download, just log the timeout expiry.
            setScriptLoadedHandler(script, undefined);
            loaded(requestTimeout + 'ms timeout triggered', url);
        }, requestTimeout);
    }
};

var loadScript = function (urls, onLoaded, onError, requestTimeout, target) {
    var queue = 0,
        errorUrls = [],
        url,
        i;

    onLoaded = isFunction(onLoaded) ? onLoaded : function() {};

    if (urls && !urls.push) {
        urls = [urls];
    }

    var onScriptLoaded = function(err, requestedUrl) {
        if (err) {
            errorUrls.push(requestedUrl);
        }

        if (--queue === 0) {
            if (isFunction(onError) && errorUrls.length > 0) {
                onError(errorUrls);
            } else {
                onLoaded();
            }
        }
    };

    for (i = 0; i < urls.length; i++) {
        url = urls[i];
        ++queue;

        createScript(urls[i], onScriptLoaded, requestTimeout, target);
    }
};

/**
 * @module loadScript
 * @desc Dynamically loads scripts in parallel, and executes a single callback after all scripts have loaded.
 * @param {String|Array} urls - A single url, or a list of urls of scripts to load.
 * @param {Function} onLoaded - Callback is executed when all scripts have finished loading.
 * @param {Function} onError - Callback is executed if one or more scripts fail to load, and passed
 *                               a single argument: the list of script urls that failed to load.
 * @param {Number} [requestTimeout] - When supplied, this will explicitly timeout the script request
 *                               and report back to onError, or, if onError is not supplied, to onLoaded.
 *                               IMPORTANT: This does not cancel the script load, just reports that it
 *                               has exceeded the timeout duration.
 */
module.exports = loadScript;