API Docs for: 1.0.0
Show:

File: src/gallery-io-multiresponse/js/multi-response.js

/**
 * <p>Extends the IO base class to enable multiple responses using an
 * iframe as the transport medium.</p>
 * 
 * @module gallery-io-multiresponse
 */

/**
 * <p>Extends the IO base class to enable multiple responses using an
 * iframe as the transport medium.  Each response fires the response event.
 * The only events that are fired are the start and end events.</p>
 * 
 * <p>All the YUI 3 IO features are supported, so the request can be sent
 * as either GET (for simple query args) or POST (for anything, even file
 * uploads).  The module uses an iframe to send the request and includes a
 * callback parameter.  (So you cannot include a parameter named
 * <q>callback</q>.)  For each response, the server must send a script
 * block invoking the callback, similar to JSONP.  Unlike JSONP, however,
 * requests can only be made to your own server because the callback will
 * reference <code>window.parent</code>.  In order to trigger script
 * parsing in all browsers, the first chunk of data that the server writes
 * to the connection must be at least 1024 bytes, and it must be part of
 * the body, so you will need to explicitly send an empty head.</p>
 * 
 * <p>Due to the way that the request data is parsed, it is not safe to
 * send JSON-encoded data using the standard YUI 3 IO methods.  However, if
 * you define <code>json</code> in the configuration passed to
 * <code>Y.io()</code>, then the data will be passed to the server under
 * the <code>json</code> parameter.  (If you pass an object, it will be
 * serialized with <code>Y.JSON.stringify()</code>.)
 * 
 * <p>To keep the iframe after it has finished loading, set
 * <code>debug:true</code> in the configuration passed to
 * <code>Y.io()</code>.</p>
 * 
 * @class io~multiresponse
 */

var L = Y.Lang,
    w = Y.config.win,
    d = Y.config.doc,
    _std = (d.documentMode && d.documentMode >= 8),
    _d = decodeURIComponent;

/**
 * Creates the iframe used in file upload transactions, and binds the
 * response event handler.
 *
 * @method _iframe
 * @private
 * @static
 * @param {object} o Transaction object generated by _iframe().
 * @param {object} c Configuration object passed to YUI.io().
 * @return {void}
 */
function _iframe(o, c, io) {
    var i = Y.Node.create('<iframe id="io-multi-response-' + o.id + '" name="io-multi-response-' + o.id + '" />');
        i._node.style.position = 'absolute';
        i._node.style.top = '-1000px';
        i._node.style.left = '-1000px';

    Y.one('body').appendChild(i);
    // Bind the onload handler to the iframe to detect the file upload response.
    Y.on("load", function() { io._multi_complete(o, c); }, '#io-multi-response-' + o.id);
}

/**
 * Creates a temporary form, if the caller doesn't provide one.
 *
 * @method _form
 * @private
 * @static
 * @param {object} c Configuration object passed to YUI.io().
 * @return {string} form id
 */
function _form(c) {
    var id = Y.guid('io-multi-response-form-'),
        f = Y.Node.create('<form id="' + id + '" name="' + id + '" />');
        f._node.style.position = 'absolute';
        f._node.style.top = '-1000px';
        f._node.style.left = '-1000px';

    Y.one('body').appendChild(f);
    return id;
}

Y.mix(Y.IO.prototype, {
    /**
     * Adds JSON encoded data to the form.
     * 
     * @method _addJSON
     * @private
     * @static
     * @param {object} f HTML form object.
     * @param {string|object} s The JSON data or object to serialize.
     * @return {array} created fields.
     */
    _addJSON: function(f, s) {
        if (!Y.Lang.isString(s)) {
            s = Y.JSON.stringify(s);
        }

        var el  = d.createElement('input');
        el.type = 'hidden';
        el.name = 'json';
        el.value = s;
        f.appendChild(el);
        Y.log('key: json and value: ' + s + ' added as form data.', 'info', 'io');

        return el;
    },

    /**
     * Sets the appropriate attributes and values to the HTML form, in
     * preparation of a file upload transaction.
     * 
     * @method _multi_setAttrs
     * @private
     * @static
     * @param {object} f HTML form object.
     * @param {object} id The Transaction ID.
     * @param {object} uri Qualified path to transaction resource.
     * @param {string} method POST or GET.
     * @return {void}
     */
    _multi_setAttrs: function(f, id, uri, method) {
        f.setAttribute('action', uri);
        f.setAttribute('method', method || 'POST');
        f.setAttribute('target', 'io-multi-response-' + id );
        f.setAttribute(Y.UA.ie && !_std ? 'encoding' : 'enctype', 'multipart/form-data');
    },

    /**
     * Starts timeout count if the configuration object has a defined
     * timeout property.
     *
     * @method _multi_startTimeout
     * @private
     * @static
     * @param {object} o Transaction object generated by _iframe().
     * @param {object} c Configuration object passed to YUI.io().
     * @return {void}
     */
    _multi_startTimeout: function(o, c) {
        var io = this;

        io._timeout[o.id] = w.setTimeout(
            function() {
                o.status = 0;
                o.statusText = 'timeout';
                io.end(o, c);
                Y.log('Transaction ' + o.id + ' timeout.', 'info', 'io');
            }, c.timeout);
    },

    // reuse _clearTimeout()

    /**
     * Destroy the iframe and temp form, if any.
     * 
     * @method _multi_destroy
     * @private
     * @static
     * @param {number} id Transaction id.
     * @param {object} c Configuration object for the transaction.
     * @return {void}
     */
    _multi_destroy: function(id, c) {
        if (!c.debug) {
            Y.Event.purgeElement('#io-multi-response-' + id, false);
            Y.one('body').removeChild(Y.one('#io-multi-response-' + id));
            Y.log('The iframe for transaction ' + id + ' has been destroyed.', 'info', 'io');
        }

        if (c.form.id.indexOf('io-multi-response-form-') === 0) {
            Y.one('body').removeChild(Y.one('#' + c.form.id));
            Y.log('The temporary form for transaction ' + id + ' has been destroyed.', 'info', 'io');
        }
    },

    /**
     * Bound to the iframe's Load event and processes the response data.
     * 
     * @method _multi_complete
     * @private
     * @static
     * @param {o} o The transaction object
     * @param {object} c Configuration object for the transaction.
     * @return {void}
     */
    _multi_complete: function(o, c) {
        var io = this;

        if (c.timeout) {
            io._clearTimeout(o.id);
        }

        io.end(o, c);
        // The transaction is complete, so call _multi_destroy to remove
        // the event listener bound to the iframe, and then
        // destroy the iframe.
        w.setTimeout( function() { io._multi_destroy(o.id, c); }, 0);
    },

    /**
     * Uploads HTML form data, inclusive of files/attachments, using the
     * iframe created in _iframe to facilitate the transaction.
     * 
     * @method _multi_send
     * @private
     * @static
     * @param {o} o The transaction object
     * @param {object} uri Qualified path to transaction resource.
     * @param {object} c Configuration object for the transaction.
     * @return {void}
     */
    _multi_send: function(o, uri, c) {
        var io = this,
            f = (typeof c.form.id === 'string') ? d.getElementById(c.form.id) : c.form.id,
            // Track original HTML form attribute values.
            attr = {
                method: f.getAttribute('method'),
                action: f.getAttribute('action'),
                target: f.getAttribute('target')
            },
            fields = [];

        // Initialize the HTML form properties in case they are
        // not defined in the HTML form.
        io._multi_setAttrs(f, o.id, uri, c.method);
        if (c.data) {
            fields = io._addData(f, c.data);
        }
        if (c.json) {
            fields.push(io._addJSON(f, c.json));
        }

        // Start polling if a callback is present and the timeout
        // property has been defined.
        if (c.timeout) {
            io._multi_startTimeout(o, c);
            Y.log('Transaction timeout started for transaction ' + o.id + '.', 'info', 'io');
        }

        // Start file upload.
        f.submit();
        io.start(o, c);
        if (c.data) {
            io._removeData(f, fields);
        }
        // Restore HTML form attributes to their original values.
        io._resetAttrs(f, attr);

        return {
            id: o.id,
            abort: function() {
                o.status = 0;
                o.statusText = 'abort';
                if (Y.one('#io-multi-response-' + o.id)) {
                    io._multi_destroy(o.id, c);
                    io.end(o, c);
                    Y.log('Transaction ' + o.id + ' aborted.', 'info', 'io');
                }
                else {
                    Y.log('Attempted to abort transaction ' + o.id + ' but transaction has completed.', 'warn', 'io');
                    return false;
                }
            },
            isInProgress: function() {
                return Y.one('#io-multi-response-' + o.id) ? true : false;
            },
            io: io
        };
    }
});

var orig_init = Y.IO.prototype._init;

Y.IO.prototype._init = function(c) {
    orig_init.apply(this, arguments);

    this.publish('io:response', Y.merge({ broadcast: 1 }, c));
    this.publish('io-trn:response', c);
};

var orig_upload = Y.IO.prototype.upload;

/* @param {object} o - transaction object.
 * @param {string} uri - qualified path to transaction resource.
 * @param {object} c - configuration object for the transaction.
 * @return object
 */
Y.IO.prototype.upload = function(o, uri, c) {
    if (!c.multiresponse) {
        return orig_upload.apply(this, arguments);
    }

    var io = this;
    YUI.Env.io_multi_response_callback[ o.id ] = function(data) {
        if (!data) {
            Y.error('Callback ' + o.id + ' invoked without data.', null, 'io');
            return;
        }

        // reset timeout
        if (c.timeout) {
            io._clearTimeout(o.id);
            io._multi_startTimeout(o, c);
        }

        if (c.on && c.on.response) {
            o.c = data;
            io._evt('response', o, c);
        }
    };

    var callback_value = encodeURIComponent('window.parent.YUI.Env.io_multi_response_callback[' + o.id + ']');
    var callback_arg   = 'callback=' + callback_value;
    if (!c.data) {
        c.data = callback_arg;
    }
    else if (L.isObject(c.data)) {
        c.data.callback = callback_value;
    }
    else {
        c.data = c.data + '&' + callback_arg;
    }

    if (c.form && !c.form.id) {
        delete c.form;
    }

    _iframe(o, c, io);
    return io._multi_send(o, uri, c);
};

if (!YUI.Env.io_multi_response_callback)
{
    YUI.Env.io_multi_response_callback = [];
}

var orig_io = Y.io;

/* @param {string} uri - qualified path to transaction resource.
 * @param {object} c - configuration object for the transaction.
 * @return object
 */
Y.io = function(uri, c) {
    if (c.multiresponse && !c.form) {
        c.form = {
            id:     _form(c),
            upload: true
        };
    }
    else if (c.multiresponse && !c.form.upload) {
        c.form.upload = true;
    }

    return orig_io.call(this, uri, c);
};

Y.mix(Y.io, orig_io);