/*
* XHRRequest
* Visit http://createjs.com/ for documentation, updates and examples.
*
*
* Copyright (c) 2012 gskinner.com, inc.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
/**
* @module PreloadJS
*/
// namespace:
this.createjs = this.createjs || {};
(function () {
"use strict";
// constructor
/**
* A preloader that loads items using XHR requests, usually XMLHttpRequest. However XDomainRequests will be used
* for cross-domain requests if possible, and older versions of IE fall back on to ActiveX objects when necessary.
* XHR requests load the content as text or binary data, provide progress and consistent completion events, and
* can be canceled during load. Note that XHR is not supported in IE 6 or earlier, and is not recommended for
* cross-domain loading.
* @class XHRRequest
* @constructor
* @param {Object} item The object that defines the file to load. Please see the {{#crossLink "LoadQueue/loadFile"}}{{/crossLink}}
* for an overview of supported file properties.
* @extends AbstractLoader
*/
function XHRRequest (item) {
this.AbstractRequest_constructor(item);
// protected properties
/**
* A reference to the XHR request used to load the content.
* @property _request
* @type {XMLHttpRequest | XDomainRequest | ActiveX.XMLHTTP}
* @private
*/
this._request = null;
/**
* A manual load timeout that is used for browsers that do not support the onTimeout event on XHR (XHR level 1,
* typically IE9).
* @property _loadTimeout
* @type {Number}
* @private
*/
this._loadTimeout = null;
/**
* The browser's XHR (XMLHTTPRequest) version. Supported versions are 1 and 2. There is no official way to detect
* the version, so we use capabilities to make a best guess.
* @property _xhrLevel
* @type {Number}
* @default 1
* @private
*/
this._xhrLevel = 1;
/**
* The response of a loaded file. This is set because it is expensive to look up constantly. This property will be
* null until the file is loaded.
* @property _response
* @type {mixed}
* @private
*/
this._response = null;
/**
* The response of the loaded file before it is modified. In most cases, content is converted from raw text to
* an HTML tag or a formatted object which is set to the <code>result</code> property, but the developer may still
* want to access the raw content as it was loaded.
* @property _rawResponse
* @type {String|Object}
* @private
*/
this._rawResponse = null;
this._canceled = false;
// Setup our event handlers now.
this._handleLoadStartProxy = createjs.proxy(this._handleLoadStart, this);
this._handleProgressProxy = createjs.proxy(this._handleProgress, this);
this._handleAbortProxy = createjs.proxy(this._handleAbort, this);
this._handleErrorProxy = createjs.proxy(this._handleError, this);
this._handleTimeoutProxy = createjs.proxy(this._handleTimeout, this);
this._handleLoadProxy = createjs.proxy(this._handleLoad, this);
this._handleReadyStateChangeProxy = createjs.proxy(this._handleReadyStateChange, this);
if (!this._createXHR(item)) {
//TODO: Throw error?
}
};
var p = createjs.extend(XHRRequest, createjs.AbstractRequest);
// static properties
/**
* A list of XMLHTTP object IDs to try when building an ActiveX object for XHR requests in earlier versions of IE.
* @property ACTIVEX_VERSIONS
* @type {Array}
* @since 0.4.2
* @private
*/
XHRRequest.ACTIVEX_VERSIONS = [
"Msxml2.XMLHTTP.6.0",
"Msxml2.XMLHTTP.5.0",
"Msxml2.XMLHTTP.4.0",
"MSXML2.XMLHTTP.3.0",
"MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"
];
// Public methods
/**
* Look up the loaded result.
* @method getResult
* @param {Boolean} [raw=false] Return a raw result instead of a formatted result. This applies to content
* loaded via XHR such as scripts, XML, CSS, and Images. If there is no raw result, the formatted result will be
* returned instead.
* @return {Object} A result object containing the content that was loaded, such as:
* <ul>
* <li>An image tag (<image />) for images</li>
* <li>A script tag for JavaScript (<script />). Note that scripts loaded with tags may be added to the
* HTML head.</li>
* <li>A style tag for CSS (<style />)</li>
* <li>Raw text for TEXT</li>
* <li>A formatted JavaScript object defined by JSON</li>
* <li>An XML document</li>
* <li>An binary arraybuffer loaded by XHR</li>
* </ul>
* Note that if a raw result is requested, but not found, the result will be returned instead.
*/
p.getResult = function (raw) {
if (raw && this._rawResponse) {
return this._rawResponse;
}
return this._response;
};
// Overrides abstract method in AbstractRequest
p.cancel = function () {
this.canceled = true;
this._clean();
this._request.abort();
};
// Overrides abstract method in AbstractLoader
p.load = function () {
if (this._request == null) {
this._handleError();
return;
}
//Events
if (this._request.addEventListener != null) {
this._request.addEventListener("loadstart", this._handleLoadStartProxy, false);
this._request.addEventListener("progress", this._handleProgressProxy, false);
this._request.addEventListener("abort", this._handleAbortProxy, false);
this._request.addEventListener("error", this._handleErrorProxy, false);
this._request.addEventListener("timeout", this._handleTimeoutProxy, false);
// Note: We don't get onload in all browsers (earlier FF and IE). onReadyStateChange handles these.
this._request.addEventListener("load", this._handleLoadProxy, false);
this._request.addEventListener("readystatechange", this._handleReadyStateChangeProxy, false);
} else {
// IE9 support
this._request.onloadstart = this._handleLoadStartProxy;
this._request.onprogress = this._handleProgressProxy;
this._request.onabort = this._handleAbortProxy;
this._request.onerror = this._handleErrorProxy;
this._request.ontimeout = this._handleTimeoutProxy;
// Note: We don't get onload in all browsers (earlier FF and IE). onReadyStateChange handles these.
this._request.onload = this._handleLoadProxy;
this._request.onreadystatechange = this._handleReadyStateChangeProxy;
}
// Set up a timeout if we don't have XHR2
if (this._xhrLevel == 1) {
this._loadTimeout = setTimeout(createjs.proxy(this._handleTimeout, this), this._item.loadTimeout);
}
// Sometimes we get back 404s immediately, particularly when there is a cross origin request. // note this does not catch in Chrome
try {
if (!this._item.values || this._item.method == createjs.AbstractLoader.GET) {
this._request.send();
} else if (this._item.method == createjs.AbstractLoader.POST) {
this._request.send(createjs.RequestUtils.formatQueryString(this._item.values));
}
} catch (error) {
this.dispatchEvent(new createjs.ErrorEvent("XHR_SEND", null, error));
}
};
p.setResponseType = function (type) {
// Some old browsers doesn't support blob, so we convert arraybuffer to blob after response is downloaded
if (type === 'blob') {
type = window.URL ? 'blob' : 'arraybuffer';
this._responseType = type;
}
this._request.responseType = type;
};
/**
* Get all the response headers from the XmlHttpRequest.
*
* <strong>From the docs:</strong> Return all the HTTP headers, excluding headers that are a case-insensitive match
* for Set-Cookie or Set-Cookie2, as a single string, with each header line separated by a U+000D CR U+000A LF pair,
* excluding the status line, and with each header name and header value separated by a U+003A COLON U+0020 SPACE
* pair.
* @method getAllResponseHeaders
* @return {String}
* @since 0.4.1
*/
p.getAllResponseHeaders = function () {
if (this._request.getAllResponseHeaders instanceof Function) {
return this._request.getAllResponseHeaders();
} else {
return null;
}
};
/**
* Get a specific response header from the XmlHttpRequest.
*
* <strong>From the docs:</strong> Returns the header field value from the response of which the field name matches
* header, unless the field name is Set-Cookie or Set-Cookie2.
* @method getResponseHeader
* @param {String} header The header name to retrieve.
* @return {String}
* @since 0.4.1
*/
p.getResponseHeader = function (header) {
if (this._request.getResponseHeader instanceof Function) {
return this._request.getResponseHeader(header);
} else {
return null;
}
};
// protected methods
/**
* The XHR request has reported progress.
* @method _handleProgress
* @param {Object} event The XHR progress event.
* @private
*/
p._handleProgress = function (event) {
if (!event || event.loaded > 0 && event.total == 0) {
return; // Sometimes we get no "total", so just ignore the progress event.
}
var newEvent = new createjs.ProgressEvent(event.loaded, event.total);
this.dispatchEvent(newEvent);
};
/**
* The XHR request has reported a load start.
* @method _handleLoadStart
* @param {Object} event The XHR loadStart event.
* @private
*/
p._handleLoadStart = function (event) {
clearTimeout(this._loadTimeout);
this.dispatchEvent("loadstart");
};
/**
* The XHR request has reported an abort event.
* @method handleAbort
* @param {Object} event The XHR abort event.
* @private
*/
p._handleAbort = function (event) {
this._clean();
this.dispatchEvent(new createjs.ErrorEvent("XHR_ABORTED", null, event));
};
/**
* The XHR request has reported an error event.
* @method _handleError
* @param {Object} event The XHR error event.
* @private
*/
p._handleError = function (event) {
this._clean();
this.dispatchEvent(new createjs.ErrorEvent(event.message));
};
/**
* The XHR request has reported a readyState change. Note that older browsers (IE 7 & 8) do not provide an onload
* event, so we must monitor the readyStateChange to determine if the file is loaded.
* @method _handleReadyStateChange
* @param {Object} event The XHR readyStateChange event.
* @private
*/
p._handleReadyStateChange = function (event) {
if (this._request.readyState == 4) {
this._handleLoad();
}
};
/**
* The XHR request has completed. This is called by the XHR request directly, or by a readyStateChange that has
* <code>request.readyState == 4</code>. Only the first call to this method will be processed.
* @method _handleLoad
* @param {Object} event The XHR load event.
* @private
*/
p._handleLoad = function (event) {
if (this.loaded) {
return;
}
this.loaded = true;
var error = this._checkError();
if (error) {
this._handleError(error);
return;
}
this._response = this._getResponse();
// Convert arraybuffer back to blob
if (this._responseType === 'arraybuffer') {
try {
this._response = new Blob([this._response]);
} catch (e) {
// Fallback to use BlobBuilder if Blob constructor is not supported
// Tested on Android 2.3 ~ 4.2 and iOS5 safari
window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;
if (e.name === 'TypeError' && window.BlobBuilder) {
var builder = new BlobBuilder();
builder.append(this._response);
this._response = builder.getBlob();
}
}
}
this._clean();
this.dispatchEvent(new createjs.Event("complete"));
};
/**
* The XHR request has timed out. This is called by the XHR request directly, or via a <code>setTimeout</code>
* callback.
* @method _handleTimeout
* @param {Object} [event] The XHR timeout event. This is occasionally null when called by the backup setTimeout.
* @private
*/
p._handleTimeout = function (event) {
this._clean();
this.dispatchEvent(new createjs.ErrorEvent("PRELOAD_TIMEOUT", null, event));
};
// Protected
/**
* Determine if there is an error in the current load. This checks the status of the request for problem codes. Note
* that this does not check for an actual response. Currently, it only checks for 404 or 0 error code.
* @method _checkError
* @return {int} If the request status returns an error code.
* @private
*/
p._checkError = function () {
//LM: Probably need additional handlers here, maybe 501
var status = parseInt(this._request.status);
switch (status) {
case 404: // Not Found
case 0: // Not Loaded
return new Error(status);
}
return null;
};
/**
* Validate the response. Different browsers have different approaches, some of which throw errors when accessed
* in other browsers. If there is no response, the <code>_response</code> property will remain null.
* @method _getResponse
* @private
*/
p._getResponse = function () {
if (this._response != null) {
return this._response;
}
if (this._request.response != null) {
return this._request.response;
}
// Android 2.2 uses .responseText
try {
if (this._request.responseText != null) {
return this._request.responseText;
}
} catch (e) {
}
// When loading XML, IE9 does not return .response, instead it returns responseXML.xml
try {
if (this._request.responseXML != null) {
return this._request.responseXML;
}
} catch (e) {
}
return null;
};
/**
* Create an XHR request. Depending on a number of factors, we get totally different results.
* <ol><li>Some browsers get an <code>XDomainRequest</code> when loading cross-domain.</li>
* <li>XMLHttpRequest are created when available.</li>
* <li>ActiveX.XMLHTTP objects are used in older IE browsers.</li>
* <li>Text requests override the mime type if possible</li>
* <li>Origin headers are sent for crossdomain requests in some browsers.</li>
* <li>Binary loads set the response type to "arraybuffer"</li></ol>
* @method _createXHR
* @param {Object} item The requested item that is being loaded.
* @return {Boolean} If an XHR request or equivalent was successfully created.
* @private
*/
p._createXHR = function (item) {
// Check for cross-domain loads. We can't fully support them, but we can try.
var crossdomain = createjs.RequestUtils.isCrossDomain(item);
var headers = {};
// Create the request. Fallback to whatever support we have.
var req = null;
if (window.XMLHttpRequest) {
req = new XMLHttpRequest();
// This is 8 or 9, so use XDomainRequest instead.
if (crossdomain && req.withCredentials === undefined && window.XDomainRequest) {
req = new XDomainRequest();
}
} else { // Old IE versions use a different approach
for (var i = 0, l = s.ACTIVEX_VERSIONS.length; i < l; i++) {
var axVersion = s.ACTIVEX_VERSIONS[i];
try {
req = new ActiveXObject(axVersion);
break;
} catch (e) {
}
}
if (req == null) {
return false;
}
}
// Default to utf-8 for Text requests.
if (item.mimeType == null && createjs.RequestUtils.isText(item.type)) {
item.mimeType = "text/plain; charset=utf-8";
}
// IE9 doesn't support overrideMimeType(), so we need to check for it.
if (item.mimeType && req.overrideMimeType) {
req.overrideMimeType(item.mimeType);
}
// Determine the XHR level
this._xhrLevel = (typeof req.responseType === "string") ? 2 : 1;
var src = null;
if (item.method == createjs.AbstractLoader.GET) {
src = createjs.RequestUtils.buildPath(item.src, item.values);
} else {
src = item.src;
}
// Open the request. Set cross-domain flags if it is supported (XHR level 1 only)
req.open(item.method || createjs.AbstractLoader.GET, src, true);
if (crossdomain && req instanceof XMLHttpRequest && this._xhrLevel == 1) {
headers["Origin"] = location.origin;
}
// To send data we need to set the Content-type header)
if (item.values && item.method == createjs.AbstractLoader.POST) {
headers["Content-Type"] = "application/x-www-form-urlencoded";
}
if (!crossdomain && !headers["X-Requested-With"]) {
headers["X-Requested-With"] = "XMLHttpRequest";
}
if (item.headers) {
for (var n in item.headers) {
headers[n] = item.headers[n];
}
}
for (n in headers) {
req.setRequestHeader(n, headers[n])
}
if (req instanceof XMLHttpRequest && item.withCredentials !== undefined) {
req.withCredentials = item.withCredentials;
}
this._request = req;
return true;
};
/**
* A request has completed (or failed or canceled), and needs to be disposed.
* @method _clean
* @private
*/
p._clean = function () {
clearTimeout(this._loadTimeout);
if (this._request.removeEventListener != null) {
this._request.removeEventListener("loadstart", this._handleLoadStartProxy);
this._request.removeEventListener("progress", this._handleProgressProxy);
this._request.removeEventListener("abort", this._handleAbortProxy);
this._request.removeEventListener("error", this._handleErrorProxy);
this._request.removeEventListener("timeout", this._handleTimeoutProxy);
this._request.removeEventListener("load", this._handleLoadProxy);
this._request.removeEventListener("readystatechange", this._handleReadyStateChangeProxy);
} else {
this._request.onloadstart = null;
this._request.onprogress = null;
this._request.onabort = null;
this._request.onerror = null;
this._request.ontimeout = null;
this._request.onload = null;
this._request.onreadystatechange = null;
}
};
p.toString = function () {
return "[PreloadJS XHRRequest]";
};
createjs.XHRRequest = createjs.promote(XHRRequest, "AbstractRequest");
}());