UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

724 lines (604 loc) 19 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2004-2011 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Tristan Koch (tristankoch) ************************************************************************ */ /** * Script loader with interface similar to * <a href="http://www.w3.org/TR/XMLHttpRequest/">XmlHttpRequest</a>. * * The script loader can be used to load scripts from arbitrary sources. * <span class="desktop"> * For JSONP requests, consider the {@link qx.bom.request.Jsonp} transport * that derives from the script loader. * </span> * * <div class="desktop"> * Example: * * <pre class="javascript"> * var req = new qx.bom.request.Script(); * req.onload = function() { * // Script is loaded and parsed and * // globals set are available * } * * req.open("GET", url); * req.send(); * </pre> * </div> * * @ignore(qx.core, qx.core.Environment.*) * @require(qx.bom.request.Script#_success) * @require(qx.bom.request.Script#abort) * @require(qx.bom.request.Script#dispose) * @require(qx.bom.request.Script#isDisposed) * @require(qx.bom.request.Script#getAllResponseHeaders) * @require(qx.bom.request.Script#getResponseHeader) * @require(qx.bom.request.Script#setDetermineSuccess) * @require(qx.bom.request.Script#setRequestHeader) * * @group (IO) */ qx.Bootstrap.define("qx.bom.request.Script", { implement: [ qx.core.IDisposable ], construct : function() { this.__initXhrProperties(); this.__onNativeLoadBound = qx.Bootstrap.bind(this._onNativeLoad, this); this.__onNativeErrorBound = qx.Bootstrap.bind(this._onNativeError, this); this.__onTimeoutBound = qx.Bootstrap.bind(this._onTimeout, this); this.__headElement = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement; this._emitter = new qx.event.Emitter(); // BUGFIX: Browsers not supporting error handler // Set default timeout to capture network errors // // Note: The script is parsed and executed, before a "load" is fired. this.timeout = this.__supportsErrorHandler() ? 0 : 15000; }, events : { /** Fired at ready state changes. */ "readystatechange" : "qx.bom.request.Script", /** Fired on error. */ "error" : "qx.bom.request.Script", /** Fired at loadend. */ "loadend" : "qx.bom.request.Script", /** Fired on timeouts. */ "timeout" : "qx.bom.request.Script", /** Fired when the request is aborted. */ "abort" : "qx.bom.request.Script", /** Fired on successful retrieval. */ "load" : "qx.bom.request.Script" }, members : { /** * @type {Number} Ready state. * * States can be: * UNSENT: 0, * OPENED: 1, * LOADING: 2, * LOADING: 3, * DONE: 4 * * Contrary to {@link qx.bom.request.Xhr#readyState}, the script transport * does not receive response headers. For compatibility, another LOADING * state is implemented that replaces the HEADERS_RECEIVED state. */ readyState: null, /** * @type {Number} The status code. * * Note: The script transport cannot determine the HTTP status code. */ status: null, /** * @type {String} The status text. * * The script transport does not receive response headers. For compatibility, * the statusText property is set to the status casted to string. */ statusText: null, /** * @type {Number} Timeout limit in milliseconds. * * 0 (default) means no timeout. */ timeout: null, /** * @type {Function} Function that is executed once the script was loaded. */ __determineSuccess: null, /** * Add an event listener for the given event name. * * @param name {String} The name of the event to listen to. * @param listener {Function} The function to execute when the event is fired * @param ctx {var?} The context of the listener. * @return {qx.bom.request.Script} Self for chaining. */ on: function(name, listener, ctx) { this._emitter.on(name, listener, ctx); return this; }, /** * Initializes (prepares) request. * * @param method {String} * The HTTP method to use. * This parameter exists for compatibility reasons. The script transport * does not support methods other than GET. * @param url {String} * The URL to which to send the request. */ open: function(method, url) { if (this.__disposed) { return; } // Reset XHR properties that may have been set by previous request this.__initXhrProperties(); this.__abort = null; this.__url = url; if (this.__environmentGet("qx.debug.io")) { qx.Bootstrap.debug(qx.bom.request.Script, "Open native request with " + "url: " + url); } this._readyStateChange(1); }, /** * Appends a query parameter to URL. * * This method exists for compatibility reasons. The script transport * does not support request headers. However, many services parse query * parameters like request headers. * * Note: The request must be initialized before using this method. * * @param key {String} * The name of the header whose value is to be set. * @param value {String} * The value to set as the body of the header. * @return {qx.bom.request.Script} Self for chaining. */ setRequestHeader: function(key, value) { if (this.__disposed) { return null; } var param = {}; if (this.readyState !== 1) { throw new Error("Invalid state"); } param[key] = value; this.__url = qx.util.Uri.appendParamsToUrl(this.__url, param); return this; }, /** * Sends request. * @return {qx.bom.request.Script} Self for chaining. */ send: function() { if (this.__disposed) { return null; } var script = this.__createScriptElement(), head = this.__headElement, that = this; if (this.timeout > 0) { this.__timeoutId = window.setTimeout(this.__onTimeoutBound, this.timeout); } if (this.__environmentGet("qx.debug.io")) { qx.Bootstrap.debug(qx.bom.request.Script, "Send native request"); } // Attach script to DOM head.insertBefore(script, head.firstChild); // The resource is loaded once the script is in DOM. // Assume HEADERS_RECEIVED and LOADING and dispatch async. window.setTimeout(function() { that._readyStateChange(2); that._readyStateChange(3); }); return this; }, /** * Aborts request. * @return {qx.bom.request.Script} Self for chaining. */ abort: function() { if (this.__disposed) { return null; } this.__abort = true; this.__disposeScriptElement(); this._emit("abort"); return this; }, /** * Helper to emit events and call the callback methods. * @param event {String} The name of the event. */ _emit: function(event) { this["on" + event](); this._emitter.emit(event, this); }, /** * Event handler for an event that fires at every state change. * * Replace with custom method to get informed about the communication progress. */ onreadystatechange: function() {}, /** * Event handler for XHR event "load" that is fired on successful retrieval. * * Note: This handler is called even when an invalid script is returned. * * Warning: Internet Explorer < 9 receives a false "load" for invalid URLs. * This "load" is fired about 2 seconds after sending the request. To * distinguish from a real "load", consider defining a custom check * function using {@link #setDetermineSuccess} and query the status * property. However, the script loaded needs to have a known impact on * the global namespace. If this does not work for you, you may be able * to set a timeout lower than 2 seconds, depending on script size, * complexity and execution time. * * Replace with custom method to listen to the "load" event. */ onload: function() {}, /** * Event handler for XHR event "loadend" that is fired on retrieval. * * Note: This handler is called even when a network error (or similar) * occurred. * * Replace with custom method to listen to the "loadend" event. */ onloadend: function() {}, /** * Event handler for XHR event "error" that is fired on a network error. * * Note: Some browsers do not support the "error" event. * * Replace with custom method to listen to the "error" event. */ onerror: function() {}, /** * Event handler for XHR event "abort" that is fired when request * is aborted. * * Replace with custom method to listen to the "abort" event. */ onabort: function() {}, /** * Event handler for XHR event "timeout" that is fired when timeout * interval has passed. * * Replace with custom method to listen to the "timeout" event. */ ontimeout: function() {}, /** * Get a single response header from response. * * Note: This method exists for compatibility reasons. The script * transport does not receive response headers. * * @param key {String} * Key of the header to get the value from. * @return {String|null} Warning message or <code>null</code> if the request * is disposed */ getResponseHeader: function(key) { if (this.__disposed) { return null; } if (this.__environmentGet("qx.debug")) { qx.Bootstrap.debug("Response header cannot be determined for " + "requests made with script transport."); } return "unknown"; }, /** * Get all response headers from response. * * Note: This method exists for compatibility reasons. The script * transport does not receive response headers. * @return {String|null} Warning message or <code>null</code> if the request * is disposed */ getAllResponseHeaders: function() { if (this.__disposed) { return null; } if (this.__environmentGet("qx.debug")) { qx.Bootstrap.debug("Response headers cannot be determined for" + "requests made with script transport."); } return "Unknown response headers"; }, /** * Determine if loaded script has expected impact on global namespace. * * The function is called once the script was loaded and must return a * boolean indicating if the response is to be considered successful. * * @param check {Function} Function executed once the script was loaded. * */ setDetermineSuccess: function(check) { this.__determineSuccess = check; }, /** * Dispose object. */ dispose: function() { var script = this.__scriptElement; if (!this.__disposed) { // Prevent memory leaks if (script) { script.onload = script.onreadystatechange = null; this.__disposeScriptElement(); } if (this.__timeoutId) { window.clearTimeout(this.__timeoutId); } this.__disposed = true; } }, /** * Check if the request has already beed disposed. * @return {Boolean} <code>true</code>, if the request has been disposed. */ isDisposed : function() { return !!this.__disposed; }, /* --------------------------------------------------------------------------- PROTECTED --------------------------------------------------------------------------- */ /** * Get URL of request. * * @return {String} URL of request. */ _getUrl: function() { return this.__url; }, /** * Get script element used for request. * * @return {Element} Script element. */ _getScriptElement: function() { return this.__scriptElement; }, /** * Handle timeout. */ _onTimeout: function() { this.__failure(); if (!this.__supportsErrorHandler()) { this._emit("error"); } this._emit("timeout"); if (!this.__supportsErrorHandler()) { this._emit("loadend"); } }, /** * Handle native load. */ _onNativeLoad: function() { var script = this.__scriptElement, determineSuccess = this.__determineSuccess, that = this; // Aborted request must not fire load if (this.__abort) { return; } // BUGFIX: IE < 9 // When handling "readystatechange" event, skip if readyState // does not signal loaded script if (this.__environmentGet("engine.name") === "mshtml" && this.__environmentGet("browser.documentmode") < 9) { if (!(/loaded|complete/).test(script.readyState)) { return; } else { if (this.__environmentGet("qx.debug.io")) { qx.Bootstrap.debug(qx.bom.request.Script, "Received native readyState: loaded"); } } } if (this.__environmentGet("qx.debug.io")) { qx.Bootstrap.debug(qx.bom.request.Script, "Received native load"); } // Determine status by calling user-provided check function if (determineSuccess) { // Status set before has higher precedence if (!this.status) { this.status = determineSuccess() ? 200 : 500; } } if (this.status === 500) { if (this.__environmentGet("qx.debug.io")) { qx.Bootstrap.debug(qx.bom.request.Script, "Detected error"); } } if (this.__timeoutId) { window.clearTimeout(this.__timeoutId); } window.setTimeout(function() { that._success(); that._readyStateChange(4); that._emit("load"); that._emit("loadend"); }); }, /** * Handle native error. */ _onNativeError: function() { this.__failure(); this._emit("error"); this._emit("loadend"); }, /* --------------------------------------------------------------------------- PRIVATE --------------------------------------------------------------------------- */ /** * @type {Element} Script element */ __scriptElement: null, /** * @type {Element} Head element */ __headElement: null, /** * @type {String} URL */ __url: "", /** * @type {Function} Bound _onNativeLoad handler. */ __onNativeLoadBound: null, /** * @type {Function} Bound _onNativeError handler. */ __onNativeErrorBound: null, /** * @type {Function} Bound _onTimeout handler. */ __onTimeoutBound: null, /** * @type {Number} Timeout timer iD. */ __timeoutId: null, /** * @type {Boolean} Whether request was aborted. */ __abort: null, /** * @type {Boolean} Whether request was disposed. */ __disposed: null, /* --------------------------------------------------------------------------- HELPER --------------------------------------------------------------------------- */ /** * Initialize properties. */ __initXhrProperties: function() { this.readyState = 0; this.status = 0; this.statusText = ""; }, /** * Change readyState. * * @param readyState {Number} The desired readyState */ _readyStateChange: function(readyState) { this.readyState = readyState; this._emit("readystatechange"); }, /** * Handle success. */ _success: function() { this.__disposeScriptElement(); this.readyState = 4; // By default, load is considered successful if (!this.status) { this.status = 200; } this.statusText = "" + this.status; }, /** * Handle failure. */ __failure: function() { this.__disposeScriptElement(); this.readyState = 4; this.status = 0; this.statusText = null; }, /** * Looks up whether browser supports error handler. * * @return {Boolean} Whether browser supports error handler. */ __supportsErrorHandler: function() { var isLegacyIe = this.__environmentGet("engine.name") === "mshtml" && this.__environmentGet("browser.documentmode") < 9; var isOpera = this.__environmentGet("engine.name") === "opera"; return !(isLegacyIe || isOpera); }, /** * Create and configure script element. * * @return {Element} Configured script element. */ __createScriptElement: function() { var script = this.__scriptElement = document.createElement("script"); script.src = this.__url; script.onerror = this.__onNativeErrorBound; script.onload = this.__onNativeLoadBound; // BUGFIX: IE < 9 // Legacy IEs do not fire the "load" event for script elements. // Instead, they support the "readystatechange" event if (this.__environmentGet("engine.name") === "mshtml" && this.__environmentGet("browser.documentmode") < 9) { script.onreadystatechange = this.__onNativeLoadBound; } return script; }, /** * Remove script element from DOM. */ __disposeScriptElement: function() { var script = this.__scriptElement; if (script && script.parentNode) { this.__headElement.removeChild(script); } }, /** * Proxy Environment.get to guard against env not being present yet. * * @param key {String} Environment key. * @return {var} Value of the queried environment key * @lint environmentNonLiteralKey(key) */ __environmentGet: function(key) { if (qx && qx.core && qx.core.Environment) { return qx.core.Environment.get(key); } else { if (key === "engine.name") { return qx.bom.client.Engine.getName(); } if (key === "browser.documentmode") { return qx.bom.client.Browser.getDocumentMode(); } if (key == "qx.debug.io") { return false; } throw new Error("Unknown environment key at this phase"); } } }, defer: function() { if (qx && qx.core && qx.core.Environment) { qx.core.Environment.add("qx.debug.io", false); } } });