/** Base IO functionality. Provides basic XHR transport support. @module io @submodule io-base @for IO **/ var // List of events that comprise the IO event lifecycle. EVENTS = ['start', 'complete', 'end', 'success', 'failure', 'progress'], // Whitelist of used XHR response object properties. XHR_PROPS = ['status', 'statusText', 'responseText', 'responseXML'], win = Y.config.win, uid = 0; /** The IO class is a utility that brokers HTTP requests through a simplified interface. Specifically, it allows JavaScript to make HTTP requests to a resource without a page reload. The underlying transport for making same-domain requests is the XMLHttpRequest object. IO can also use Flash, if specified as a transport, for cross-domain requests. @class IO @constructor @param {Object} config Object of EventTarget's publish method configurations used to configure IO's events. IO can be called statically using {{#crossLink "YUI/io:method"}}YUI.io{{/crossLink}}. **/ function IO (config) { var io = this; io._uid = 'io:' + uid++; io._init(config); Y.io._map[io._uid] = io; } IO.prototype = { //-------------------------------------- // Properties //-------------------------------------- /** * A counter that increments for each transaction. * * @property _id * @private * @type {Number} */ _id: 0, /** * Object of IO HTTP headers sent with each transaction. * * @property _headers * @private * @type {Object} */ _headers: { 'X-Requested-With' : 'XMLHttpRequest' }, /** * Object that stores timeout values for any transaction with a defined * "timeout" configuration property. * * @property _timeout * @private * @type {Object} */ _timeout: {}, //-------------------------------------- // Methods //-------------------------------------- _init: function(config) { var io = this, i, len; io.cfg = config || {}; Y.augment(io, Y.EventTarget); for (i = 0, len = EVENTS.length; i < len; ++i) { // Publish IO global events with configurations, if any. // IO global events are set to broadcast by default. // These events use the "io:" namespace. io.publish('io:' + EVENTS[i], Y.merge({ broadcast: 1 }, config)); // Publish IO transaction events with configurations, if // any. These events use the "io-trn:" namespace. io.publish('io-trn:' + EVENTS[i], config); } }, /** * Method that creates a unique transaction object for each request. * * @method _create * @private * @param {Object} cfg Configuration object subset to determine if * the transaction is an XDR or file upload, * requiring an alternate transport. * @param {Number} id Transaction id * @return {Object} The transaction object */ _create: function(config, id) { var io = this, transaction = { id : Y.Lang.isNumber(id) ? id : io._id++, uid: io._uid }, alt = config.xdr ? config.xdr.use : null, form = config.form && config.form.upload ? 'iframe' : null, use; if (alt === 'native') { // Non-IE and IE >= 10 can use XHR level 2 and not rely on an // external transport. alt = Y.UA.ie && !SUPPORTS_CORS ? 'xdr' : null; // Prevent "pre-flight" OPTIONS request by removing the // `X-Requested-With` HTTP header from CORS requests. This header // can be added back on a per-request basis, if desired. io.setHeader('X-Requested-With'); } use = alt || form; transaction = use ? Y.merge(Y.IO.customTransport(use), transaction) : Y.merge(Y.IO.defaultTransport(), transaction); if (transaction.notify) { config.notify = function (e, t, c) { io.notify(e, t, c); }; } if (!use) { if (win && win.FormData && config.data instanceof win.FormData) { transaction.c.upload.onprogress = function (e) { io.progress(transaction, e, config); }; transaction.c.onload = function (e) { io.load(transaction, e, config); }; transaction.c.onerror = function (e) { io.error(transaction, e, config); }; transaction.upload = true; } } return transaction; }, _destroy: function(transaction) { if (win && !transaction.notify && !transaction.xdr) { if (XHR && !transaction.upload) { transaction.c.onreadystatechange = null; } else if (transaction.upload) { transaction.c.upload.onprogress = null; transaction.c.onload = null; transaction.c.onerror = null; } else if (Y.UA.ie && !transaction.e) { // IE, when using XMLHttpRequest as an ActiveX Object, will throw // a "Type Mismatch" error if the event handler is set to "null". transaction.c.abort(); } } transaction = transaction.c = null; }, /** * Method for creating and firing events. * * @method _evt * @private * @param {String} eventName Event to be published. * @param {Object} transaction Transaction object. * @param {Object} config Configuration data subset for event subscription. */ _evt: function(eventName, transaction, config) { var io = this, params, args = config['arguments'], emitFacade = io.cfg.emitFacade, globalEvent = "io:" + eventName, trnEvent = "io-trn:" + eventName; // Workaround for #2532107 this.detach(trnEvent); if (transaction.e) { transaction.c = { status: 0, statusText: transaction.e }; } // Fire event with parameters or an Event Facade. params = [ emitFacade ? { id: transaction.id, data: transaction.c, cfg: config, 'arguments': args } : transaction.id ]; if (!emitFacade) { if (eventName === EVENTS[0] || eventName === EVENTS[2]) { if (args) { params.push(args); } } else { if (transaction.evt) { params.push(transaction.evt); } else { params.push(transaction.c); } if (args) { params.push(args); } } } params.unshift(globalEvent); // Fire global events. io.fire.apply(io, params); // Fire transaction events, if receivers are defined. if (config.on) { params[0] = trnEvent; io.once(trnEvent, config.on[eventName], config.context || Y); io.fire.apply(io, params); } }, /** * Fires event "io:start" and creates, fires a transaction-specific * start event, if `config.on.start` is defined. * * @method start * @param {Object} transaction Transaction object. * @param {Object} config Configuration object for the transaction. */ start: function(transaction, config) { /** * Signals the start of an IO request. * @event io:start */ this._evt(EVENTS[0], transaction, config); }, /** * Fires event "io:complete" and creates, fires a * transaction-specific "complete" event, if config.on.complete is * defined. * * @method complete * @param {Object} transaction Transaction object. * @param {Object} config Configuration object for the transaction. */ complete: function(transaction, config) { /** * Signals the completion of the request-response phase of a * transaction. Response status and data are accessible, if * available, in this event. * @event io:complete */ this._evt(EVENTS[1], transaction, config); }, /** * Fires event "io:end" and creates, fires a transaction-specific "end" * event, if config.on.end is defined. * * @method end * @param {Object} transaction Transaction object. * @param {Object} config Configuration object for the transaction. */ end: function(transaction, config) { /** * Signals the end of the transaction lifecycle. * @event io:end */ this._evt(EVENTS[2], transaction, config); this._destroy(transaction); }, /** * Fires event "io:success" and creates, fires a transaction-specific * "success" event, if config.on.success is defined. * * @method success * @param {Object} transaction Transaction object. * @param {Object} config Configuration object for the transaction. */ success: function(transaction, config) { /** * Signals an HTTP response with status in the 2xx range. * Fires after io:complete. * @event io:success */ this._evt(EVENTS[3], transaction, config); this.end(transaction, config); }, /** * Fires event "io:failure" and creates, fires a transaction-specific * "failure" event, if config.on.failure is defined. * * @method failure * @param {Object} transaction Transaction object. * @param {Object} config Configuration object for the transaction. */ failure: function(transaction, config) { /** * Signals an HTTP response with status outside of the 2xx range. * Fires after io:complete. * @event io:failure */ this._evt(EVENTS[4], transaction, config); this.end(transaction, config); }, /** * Fires event "io:progress" and creates, fires a transaction-specific * "progress" event -- for XMLHttpRequest file upload -- if * config.on.progress is defined. * * @method progress * @param {Object} transaction Transaction object. * @param {Object} progress event. * @param {Object} config Configuration object for the transaction. */ progress: function(transaction, e, config) { /** * Signals the interactive state during a file upload transaction. * This event fires after io:start and before io:complete. * @event io:progress */ transaction.evt = e; this._evt(EVENTS[5], transaction, config); }, /** * Fires event "io:complete" and creates, fires a transaction-specific * "complete" event -- for XMLHttpRequest file upload -- if * config.on.complete is defined. * * @method load * @param {Object} transaction Transaction object. * @param {Object} load event. * @param {Object} config Configuration object for the transaction. */ load: function (transaction, e, config) { transaction.evt = e.target; this._evt(EVENTS[1], transaction, config); }, /** * Fires event "io:failure" and creates, fires a transaction-specific * "failure" event -- for XMLHttpRequest file upload -- if * config.on.failure is defined. * * @method error * @param {Object} transaction Transaction object. * @param {Object} error event. * @param {Object} config Configuration object for the transaction. */ error: function (transaction, e, config) { transaction.evt = e; this._evt(EVENTS[4], transaction, config); }, /** * Retry an XDR transaction, using the Flash tranport, if the native * transport fails. * * @method _retry * @private * @param {Object} transaction Transaction object. * @param {String} uri Qualified path to transaction resource. * @param {Object} config Configuration object for the transaction. */ _retry: function(transaction, uri, config) { this._destroy(transaction); config.xdr.use = 'flash'; return this.send(uri, config, transaction.id); }, /** * Method that concatenates string data for HTTP GET transactions. * * @method _concat * @private * @param {String} uri URI or root data. * @param {String} data Data to be concatenated onto URI. * @return {String} */ _concat: function(uri, data) { uri += (uri.indexOf('?') === -1 ? '?' : '&') + data; return uri; }, /** * Stores default client headers for all transactions. If a label is * passed with no value argument, the header will be deleted. * * @method setHeader * @param {String} name HTTP header * @param {String} value HTTP header value */ setHeader: function(name, value) { if (value) { this._headers[name] = value; } else { delete this._headers[name]; } }, /** * Method that sets all HTTP headers to be sent in a transaction. * * @method _setHeaders * @private * @param {Object} transaction - XHR instance for the specific transaction. * @param {Object} headers - HTTP headers for the specific transaction, as * defined in the configuration object passed to YUI.io(). */ _setHeaders: function(transaction, headers) { headers = Y.merge(this._headers, headers); Y.Object.each(headers, function(value, name) { if (value !== 'disable') { transaction.setRequestHeader(name, headers[name]); } }); }, /** * Starts timeout count if the configuration object has a defined * timeout property. * * @method _startTimeout * @private * @param {Object} transaction Transaction object generated by _create(). * @param {Object} timeout Timeout in milliseconds. */ _startTimeout: function(transaction, timeout) { var io = this; io._timeout[transaction.id] = setTimeout(function() { io._abort(transaction, 'timeout'); }, timeout); }, /** * Clears the timeout interval started by _startTimeout(). * * @method _clearTimeout * @private * @param {Number} id - Transaction id. */ _clearTimeout: function(id) { clearTimeout(this._timeout[id]); delete this._timeout[id]; }, /** * Method that determines if a transaction response qualifies as success * or failure, based on the response HTTP status code, and fires the * appropriate success or failure events. * * @method _result * @private * @static * @param {Object} transaction Transaction object generated by _create(). * @param {Object} config Configuration object passed to io(). */ _result: function(transaction, config) { var status; // Firefox will throw an exception if attempting to access // an XHR object's status property, after a request is aborted. try { status = transaction.c.status; } catch(e) { status = 0; } // IE reports HTTP 204 as HTTP 1223. if (status >= 200 && status < 300 || status === 304 || status === 1223) { this.success(transaction, config); } else { this.failure(transaction, config); } }, /** * Event handler bound to onreadystatechange. * * @method _rS * @private * @param {Object} transaction Transaction object generated by _create(). * @param {Object} config Configuration object passed to YUI.io(). */ _rS: function(transaction, config) { var io = this; if (transaction.c.readyState === 4) { if (config.timeout) { io._clearTimeout(transaction.id); } // Yield in the event of request timeout or abort. setTimeout(function() { io.complete(transaction, config); io._result(transaction, config); }, 0); } }, /** * Terminates a transaction due to an explicit abort or timeout. * * @method _abort * @private * @param {Object} transaction Transaction object generated by _create(). * @param {String} type Identifies timed out or aborted transaction. */ _abort: function(transaction, type) { if (transaction && transaction.c) { transaction.e = type; transaction.c.abort(); } }, /** * Requests a transaction. `send()` is implemented as `Y.io()`. Each * transaction may include a configuration object. Its properties are: * * <dl> * <dt>method</dt> * <dd>HTTP method verb (e.g., GET or POST). If this property is not * not defined, the default value will be GET.</dd> * * <dt>data</dt> * <dd>This is the name-value string that will be sent as the * transaction data. If the request is HTTP GET, the data become * part of querystring. If HTTP POST, the data are sent in the * message body.</dd> * * <dt>xdr</dt> * <dd>Defines the transport to be used for cross-domain requests. * By setting this property, the transaction will use the specified * transport instead of XMLHttpRequest. The properties of the * transport object are: * <dl> * <dt>use</dt> * <dd>The transport to be used: 'flash' or 'native'</dd> * <dt>dataType</dt> * <dd>Set the value to 'XML' if that is the expected response * content type.</dd> * <dt>credentials</dt> * <dd>Set the value to 'true' to set XHR.withCredentials property to true.</dd> * </dl></dd> * * <dt>form</dt> * <dd>Form serialization configuration object. Its properties are: * <dl> * <dt>id</dt> * <dd>Node object or id of HTML form</dd> * <dt>useDisabled</dt> * <dd>`true` to also serialize disabled form field values * (defaults to `false`)</dd> * </dl></dd> * * <dt>on</dt> * <dd>Assigns transaction event subscriptions. Available events are: * <dl> * <dt>start</dt> * <dd>Fires when a request is sent to a resource.</dd> * <dt>complete</dt> * <dd>Fires when the transaction is complete.</dd> * <dt>success</dt> * <dd>Fires when the HTTP response status is within the 2xx * range.</dd> * <dt>failure</dt> * <dd>Fires when the HTTP response status is outside the 2xx * range, if an exception occurs, if the transation is aborted, * or if the transaction exceeds a configured `timeout`.</dd> * <dt>end</dt> * <dd>Fires at the conclusion of the transaction * lifecycle, after `success` or `failure`.</dd> * </dl> * * <p>Callback functions for `start` and `end` receive the id of the * transaction as a first argument. For `complete`, `success`, and * `failure`, callbacks receive the id and the response object * (usually the XMLHttpRequest instance). If the `arguments` * property was included in the configuration object passed to * `Y.io()`, the configured data will be passed to all callbacks as * the last argument.</p> * </dd> * * <dt>sync</dt> * <dd>Pass `true` to make a same-domain transaction synchronous. * <strong>CAVEAT</strong>: This will negatively impact the user * experience. Have a <em>very</em> good reason if you intend to use * this.</dd> * * <dt>context</dt> * <dd>The "`this'" object for all configured event handlers. If a * specific context is needed for individual callbacks, bind the * callback to a context using `Y.bind()`.</dd> * * <dt>headers</dt> * <dd>Object map of transaction headers to send to the server. The * object keys are the header names and the values are the header * values.</dd> * * <dt>username</dt> * <dd>Username to use in a HTTP authentication.</dd> * * <dt>password</dt> * <dd>Password to use in a HTTP authentication.</dd> * * <dt>timeout</dt> * <dd>Millisecond threshold for the transaction before being * automatically aborted.</dd> * * <dt>arguments</dt> * <dd>User-defined data passed to all registered event handlers. * This value is available as the second argument in the "start" and * "end" event handlers. It is the third argument in the "complete", * "success", and "failure" event handlers. <strong>Be sure to quote * this property name in the transaction configuration as * "arguments" is a reserved word in JavaScript</strong> (e.g. * `Y.io({ ..., "arguments": stuff })`).</dd> * </dl> * * @method send * @public * @param {String} uri Qualified path to transaction resource. * @param {Object} config Configuration object for the transaction. * @param {Number} id Transaction id, if already set. * @return {Object} An object containing: * <dl> * <dt>`id`</dt> * <dd> * The transaction ID for this request. * </dd> * <dt>`abort`</dt> * <dd> * A function to abort the current transaction. * </dd> * <dt>`isInProgress`</dt> * <dd> * A helper to determine whether the current transaction is in progress. * </dd> * <dt>`io`</dt> * <dd> * A reference to the IO object for this transaction. * </dd> * </dl> */ send: function(uri, config, id) { var transaction, method, i, len, sync, data, io = this, u = uri, response = {}; config = config ? Y.Object(config) : {}; transaction = io._create(config, id); method = config.method ? config.method.toUpperCase() : 'GET'; sync = config.sync; data = config.data; // Serialize a map object into a key-value string using // querystring-stringify-simple. if ((Y.Lang.isObject(data) && !data.nodeType) && !transaction.upload) { if (Y.QueryString && Y.QueryString.stringify) { Y.log('Stringifying config.data for request', 'info', 'io'); config.data = data = Y.QueryString.stringify(data); } else { Y.log('Failed to stringify config.data object, likely because `querystring-stringify-simple` is missing.', 'warn', 'io'); } } if (config.form) { if (config.form.upload) { // This is a file upload transaction, calling // upload() in io-upload-iframe. return io.upload(transaction, uri, config); } else { // Serialize HTML form data into a key-value string. data = io._serialize(config.form, data); } } // Convert falsy values to an empty string. This way IE can't be // rediculous and translate `undefined` to "undefined". data || (data = ''); if (data) { switch (method) { case 'GET': case 'HEAD': case 'DELETE': u = io._concat(u, data); data = ''; Y.log('HTTP' + method + ' with data. The querystring is: ' + u, 'info', 'io'); break; case 'POST': case 'PUT': // If Content-Type is defined in the configuration object, or // or as a default header, it will be used instead of // 'application/x-www-form-urlencoded; charset=UTF-8' config.headers = Y.merge({ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, config.headers); break; } } if (transaction.xdr) { // Route data to io-xdr module for flash and XDomainRequest. return io.xdr(u, transaction, config); } else if (transaction.notify) { // Route data to custom transport return transaction.c.send(transaction, uri, config); } if (!sync && !transaction.upload) { transaction.c.onreadystatechange = function() { io._rS(transaction, config); }; } try { // Determine if request is to be set as // synchronous or asynchronous. transaction.c.open(method, u, !sync, config.username || null, config.password || null); io._setHeaders(transaction.c, config.headers || {}); io.start(transaction, config); // Will work only in browsers that implement the // Cross-Origin Resource Sharing draft. if (config.xdr && config.xdr.credentials && SUPPORTS_CORS) { transaction.c.withCredentials = true; } // Using "null" with HTTP POST will result in a request // with no Content-Length header defined. transaction.c.send(data); if (sync) { // Create a response object for synchronous transactions, // mixing id and arguments properties with the xhr // properties whitelist. for (i = 0, len = XHR_PROPS.length; i < len; ++i) { response[XHR_PROPS[i]] = transaction.c[XHR_PROPS[i]]; } response.getAllResponseHeaders = function() { return transaction.c.getAllResponseHeaders(); }; response.getResponseHeader = function(name) { return transaction.c.getResponseHeader(name); }; io.complete(transaction, config); io._result(transaction, config); return response; } } catch(e) { if (transaction.xdr) { // This exception is usually thrown by browsers // that do not support XMLHttpRequest Level 2. // Retry the request with the XDR transport set // to 'flash'. If the Flash transport is not // initialized or available, the transaction // will resolve to a transport error. return io._retry(transaction, uri, config); } else { io.complete(transaction, config); io._result(transaction, config); } } // If config.timeout is defined, and the request is standard XHR, // initialize timeout polling. if (config.timeout) { io._startTimeout(transaction, config.timeout); Y.log('Configuration timeout set to: ' + config.timeout, 'info', 'io'); } return { id: transaction.id, abort: function() { return transaction.c ? io._abort(transaction, 'abort') : false; }, isInProgress: function() { return transaction.c ? (transaction.c.readyState % 4) : false; }, io: io }; } }; /** Method for initiating an ajax call. The first argument is the url end point for the call. The second argument is an object to configure the transaction and attach event subscriptions. The configuration object supports the following properties: <dl> <dt>method</dt> <dd>HTTP method verb (e.g., GET or POST). If this property is not not defined, the default value will be GET.</dd> <dt>data</dt> <dd>This is the name-value string that will be sent as the transaction data. If the request is HTTP GET, the data become part of querystring. If HTTP POST, the data are sent in the message body.</dd> <dt>xdr</dt> <dd>Defines the transport to be used for cross-domain requests. By setting this property, the transaction will use the specified transport instead of XMLHttpRequest. The properties of the transport object are: <dl> <dt>use</dt> <dd>The transport to be used: 'flash' or 'native'</dd> <dt>dataType</dt> <dd>Set the value to 'XML' if that is the expected response content type.</dd> </dl></dd> <dt>form</dt> <dd>Form serialization configuration object. Its properties are: <dl> <dt>id</dt> <dd>Node object or id of HTML form</dd> <dt>useDisabled</dt> <dd>`true` to also serialize disabled form field values (defaults to `false`)</dd> </dl></dd> <dt>on</dt> <dd>Assigns transaction event subscriptions. Available events are: <dl> <dt>start</dt> <dd>Fires when a request is sent to a resource.</dd> <dt>complete</dt> <dd>Fires when the transaction is complete.</dd> <dt>success</dt> <dd>Fires when the HTTP response status is within the 2xx range.</dd> <dt>failure</dt> <dd>Fires when the HTTP response status is outside the 2xx range, if an exception occurs, if the transation is aborted, or if the transaction exceeds a configured `timeout`.</dd> <dt>end</dt> <dd>Fires at the conclusion of the transaction lifecycle, after `success` or `failure`.</dd> </dl> <p>Callback functions for `start` and `end` receive the id of the transaction as a first argument. For `complete`, `success`, and `failure`, callbacks receive the id and the response object (usually the XMLHttpRequest instance). If the `arguments` property was included in the configuration object passed to `Y.io()`, the configured data will be passed to all callbacks as the last argument.</p> </dd> <dt>sync</dt> <dd>Pass `true` to make a same-domain transaction synchronous. <strong>CAVEAT</strong>: This will negatively impact the user experience. Have a <em>very</em> good reason if you intend to use this.</dd> <dt>context</dt> <dd>The "`this'" object for all configured event handlers. If a specific context is needed for individual callbacks, bind the callback to a context using `Y.bind()`.</dd> <dt>headers</dt> <dd>Object map of transaction headers to send to the server. The object keys are the header names and the values are the header values.</dd> <dt>timeout</dt> <dd>Millisecond threshold for the transaction before being automatically aborted.</dd> <dt>arguments</dt> <dd>User-defined data passed to all registered event handlers. This value is available as the second argument in the "start" and "end" event handlers. It is the third argument in the "complete", "success", and "failure" event handlers. <strong>Be sure to quote this property name in the transaction configuration as "arguments" is a reserved word in JavaScript</strong> (e.g. `Y.io({ ..., "arguments": stuff })`).</dd> </dl> @method io @static @param {String} url qualified path to transaction resource. @param {Object} config configuration object for the transaction. @return {Object} An object containing: <dl> <dt>`id`</dt> <dd> The transaction ID for this request. </dd> <dt>`abort`</dt> <dd> A function to abort the current transaction. </dd> <dt>`isInProgress`</dt> <dd> A helper to determine whether the current transaction is in progress. </dd> <dt>`io`</dt> <dd> A reference to the IO object for this transaction. </dd> </dl> @for YUI **/ Y.io = function(url, config) { // Calling IO through the static interface will use and reuse // an instance of IO. var transaction = Y.io._map['io:0'] || new IO(); return transaction.send.apply(transaction, [url, config]); }; /** Method for setting and deleting IO HTTP headers to be sent with every request. Hosted as a property on the `io` function (e.g. `Y.io.header`). @method header @param {String} name HTTP header @param {String} value HTTP header value @static **/ Y.io.header = function(name, value) { // Calling IO through the static interface will use and reuse // an instance of IO. var transaction = Y.io._map['io:0'] || new IO(); transaction.setHeader(name, value); }; Y.IO = IO; // Map of all IO instances created. Y.io._map = {};