JSL - AJAX plugin

An AJAX plugin for JSL

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/2817/7911/JSL%20-%20AJAX%20plugin.js

// ==UserScript==
// @name          JSL - AJAX plugin
// @namespace     http://userscripts.org/users/23652
// @description   An AJAX plugin for JSL
// @include       *
// @version       1.1.0
// @grant         GM_xmlhttpRequest
// ==/UserScript==

/* CHANGELOG

1.1.0 (3/28/2014)
    - added JSL.ajaxClear() to clear all current and pending requests

1.0.21 (10/6/2013)
    - fixed bug with .clear()

1.0.2 (10/6/2013)
    - added a new option: async
        false (default) ==> synchronous, but non-freezing requests (sequential)
            waits for previous request to finish before starting a new one
        true ==> sends all requests immediately when they are added to the queue
    - fixed delay issue.
        the next request would get run on the 'onprogress' & 'onreadystatechange' events
        instead of when they actually load fully
    - added a .clear() method
        it may be called on any ajax instance like JSL.ajax(...).clear()
        it can even simply be called as JSL.ajax().clear()
        it will clear ALL requests at the moment

1.0.1 (10/3/2013)
    - fixed small bug with passing a url array
    - fixed bug not allowing HEAD requests to be recognized

1.0.0 (10/1/2013)
    - created

*/

(function (undefined) {

    'use strict'; // use strict mode in ECMAScript-5

    var queue = [],               // the request queue
        blank = function () {},   // blank function to use as default callback
        xhrInProgress = false,    // boolean to know if we should load the next request
        xhrCleared = false;       // boolean to know if the xhr has been cleared and if
                                  //     we should execute any of the callbacks

    var core = {
        // object
        'hasOwnProperty' : Object.prototype.hasOwnProperty
    };

    function copyObject(o) {
        var key, value, newO = {};

        for (key in o) {
            value = o[key];

            if (core.hasOwnProperty.call(o, key) && value != null) {
                newO[key] = value;
            }
        }

        return newO;
    }

    function toDataString(o) {
        var key, value, dataString = '';

        for (key in o) {
            value = o[key];

            if (core.hasOwnProperty.call(o, key) && value != null) {
                dataString += key + '=' + encodeURIComponent(value) + '&';
            }
        }

        return dataString.slice(0, -1);
    }

    function xhr() {
        var req = queue[0], // get the object which is first in the queue
            xhrObj = {}, key;

        function handleEvents(type, resp, event) {
            var event = event || {}, newResp, context;
                req.delay = req.delay > 15 ? req.delay : 15; // don't want to mess up callbacks

            if (xhrCleared === true) {
                return;
            }

            if (req[type] !== blank) {
                // define a new response object to give to the user
                newResp = {
                    lengthComputable : resp.lengthComputable || event.lengthComputable || null,
                    loaded : resp.loaded || event.loaded || null,
                    readyState : resp.readyState,
                    responseHeaders : resp.responseHeaders ||
                        ( typeof resp.getAllResponseHeaders === 'function' ? resp.getAllResponseHeaders() : null) || '',
                    responseText : resp.responseText,
                    status : resp.status,
                    statusText : resp.statusText,
                    total : resp.total || event.total || null,
                    url : resp.finalUrl || req.url,
                };

                // allow new requests to be run if our request is done
                if (type === 'onerror' || type === 'onload') {
                    xhrInProgress = false;

                    // run the next in queue, if any
                    window.setTimeout(xhr, req.delay);
                }

                // run the callback
                context = req.context || newResp;
                req[type].call(context, newResp);
            }
        }

        if ( req && (xhrInProgress === false || req.async === true) && queue.length > 0) {
            // make it so no other requests get run while we
            // run this one, if async isn't enabled
            xhrInProgress = true;

            // remove the first item in the queue if it is going to be run
            queue.shift();

            if (typeof GM_xmlhttpRequest === 'function') {
                if (req.method.toUpperCase() === 'GET' && req.data !== '') {
                    req.url += '?' + req.data;
                    req.data = '';
                }

                GM_xmlhttpRequest({
                    'data' : req.data,
                    'headers' : req.headers,
                    'method' : req.method,
                    'onerror' : function (resp) {
                        handleEvents('onerror', resp);
                    },
                    'onload' : function (resp) {
                        handleEvents('onload', resp);
                    },
                    'onreadystatechange' : function (resp) {
                        handleEvents('onreadystatechange', resp);
                    },
                    'onprogress' : function (resp) {
                        handleEvents('onprogress', resp);
                    },
                    'url' : req.url,
                });
            } else if (typeof XMLHttpRequest === 'function' || typeof ActiveXObject === 'function') {
                xhrObj = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');

                // set events
                xhrObj.onload = function (resp) {
                    handleEvents('onload', xhrObj);
                };
                xhrObj.onerror = function (resp) {
                    handleEvents('onerror', xhrObj);
                };
                xhrObj.onprogress = function (resp) {
                    handleEvents('onprogress', xhrObj, resp);
                };

                if (req.mimeType !== '') {
                    xhrObj.overrideMimeType(req.mimeType);
                }

                // add headers
                for (key in req.headers) {
                    xhrObj.setRequestHeader( key, req.headers[key] );
                }

                xhrObj.open(req.method, req.url, true);
                xhrObj.send( (req.data || null) );
            }
        }
    }

    function init(url, settings) {
        var urls = [],
            realSettings = { // defaults
                async : false,
                data : '',
                headers : {},
                method : 'GET',
                mimeType : '',
                onload : blank,
                onerror : blank,
                onprogress : blank,
                onreadystatechange : blank,
                delay : 0
            },
            key, value;

        if (typeof url === 'string') {
            urls.push(url);
        } else if (JSL.typeOf(url) === 'array') {
            urls = urls.concat(url);
        }

        if (JSL.typeOf(settings) === 'object') {
            for (key in settings) {
                value = settings[key];

                switch (key) {
                    case 'async': {
                        if (typeof value === 'boolean') {
                            realSettings[key] = value;
                        }
                        break;
                    }
                    case 'context': {
                        if (value != null) {
                            realSettings[key] = value;
                        }
                        break;
                    }
                    case 'data': {
                        if (typeof value === 'string') {
                            realSettings[key] = value;
                        } else if (JSL.typeOf(value) === 'object') {
                            realSettings[key] = toDataString(value);
                        }
                        break;
                    }
                    case 'delay': {
                        if (typeof value === 'number' && value > 0) {
                            realSettings[key] = value;
                        }
                        break;
                    }
                    case 'headers': {
                        if (JSL.typeOf(value) === 'object') {
                            realSettings[key] = toDataString(value);
                        }
                        break;
                    }
                    case 'method': {
                        if ( typeof value === 'string' && /get|post|head/i.test(value) ) {
                            realSettings[key] = value.toUpperCase();
                        }
                        break;
                    }
                    case 'mimeType': {
                        if (typeof value === 'string') {
                            realSettings[key] = value;
                        }
                        break;
                    }
                    case 'onload': case 'onerror': case 'onreadystatechange': case 'onprogress': {
                        if (typeof value === 'function') {
                            realSettings[key] = value;
                        }
                        break;
                    }
                }
            }
        }

        // add an object to the queue for each url
        if (urls.length > 0) {
            JSL.each(urls, function (url) {
                var newO = copyObject(realSettings);
                newO.url = url;
                queue.push(newO);
            });

            // enable ajax if it was cleared earlier
            xhrCleared = false;

            // run the xhr function
            // it will determine whether or not a request needs to be sent
            xhr();
        }
    }

    init.prototype = {
        constructor: init,

        clear : function() {
            queue.length = 0;
            xhrInProgress = false;
            xhrCleared = true;
        },

        get length() {
            return queue.length;
        }
    };

    JSL.extend({
        ajax : function (url, settings) {
            return new init(url, settings);
        },
        ajaxClear : init.prototype.clear
    });

}());