From ab3ba4a81252c4357a7aab5f24d765d41d47986e Mon Sep 17 00:00:00 2001 From: jaubourg Date: Thu, 9 Dec 2010 13:34:28 -0500 Subject: [PATCH] Rewrite of the Ajax module by Julian Aubourg. Some (dated) details can be found here: http://oksoclap.com/6Y26bm1ZsB more details are forthcoming. Fixes #7195. --- Makefile | 6 +- src/ajax.js | 571 +++++---------------- src/transports/jsonp.js | 89 ++++ src/transports/script.js | 83 +++ src/transports/xhr.js | 191 +++++++ src/xhr.js | 941 +++++++++++++++++++++++++++++++++++ test/data/atom+xml.php | 4 + test/data/css.php | 15 + test/data/headers.php | 5 + test/data/with_fries_over_jsonp.php | 7 + test/index.html | 4 + test/unit/ajax.js | 538 +++++++++++++++++++- 12 files changed, 1991 insertions(+), 463 deletions(-) create mode 100644 src/transports/jsonp.js create mode 100644 src/transports/script.js create mode 100644 src/transports/xhr.js create mode 100644 src/xhr.js create mode 100644 test/data/atom+xml.php create mode 100644 test/data/css.php create mode 100644 test/data/headers.php create mode 100644 test/data/with_fries_over_jsonp.php diff --git a/Makefile b/Makefile index 0dae732..fdc655d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,11 @@ BASE_FILES = ${SRC_DIR}/core.js\ ${SRC_DIR}/traversing.js\ ${SRC_DIR}/manipulation.js\ ${SRC_DIR}/css.js\ - ${SRC_DIR}/ajax.js\ + ${SRC_DIR}/ajax.js\ + ${SRC_DIR}/xhr.js\ + ${SRC_DIR}/transports/jsonp.js\ + ${SRC_DIR}/transports/script.js\ + ${SRC_DIR}/transports/xhr.js\ ${SRC_DIR}/effects.js\ ${SRC_DIR}/offset.js\ ${SRC_DIR}/dimensions.js diff --git a/src/ajax.js b/src/ajax.js index 90dc350..ceeef5e 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,17 +1,11 @@ (function( jQuery ) { - -var jsc = jQuery.now(), - rscript = /)<[^<]*)*<\/script>/gi, + +var rscript = /)<[^<]*)*<\/script>/gi, rselectTextarea = /^(?:select|textarea)/i, rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i, - rnoContent = /^(?:GET|HEAD)$/, rbracket = /\[\]$/, - jsre = /\=\?(&|$)/, rquery = /\?/, - rts = /([?&])_=[^&]*/, - rurl = /^(\w+:)?\/\/([^\/?#]+)/, r20 = /%20/g, - rhash = /#.*$/, // Keep a copy of the old load method _load = jQuery.fn.load; @@ -49,9 +43,9 @@ jQuery.fn.extend({ type = "POST"; } } - + var self = this; - + // Request the remote document jQuery.ajax({ url: url, @@ -90,36 +84,37 @@ jQuery.fn.extend({ }, serializeArray: function() { - return this.map(function() { + return this.map(function(){ return this.elements ? jQuery.makeArray(this.elements) : this; }) - .filter(function() { + .filter(function(){ return this.name && !this.disabled && (this.checked || rselectTextarea.test(this.nodeName) || rinput.test(this.type)); }) - .map(function( i, elem ) { + .map(function(i, elem){ var val = jQuery(this).val(); return val == null ? null : jQuery.isArray(val) ? - jQuery.map( val, function( val, i ) { - return { name: elem.name, value: val }; + jQuery.map( val, function(val, i){ + return {name: elem.name, value: val}; }) : - { name: elem.name, value: val }; + {name: elem.name, value: val}; }).get(); } }); // Attach a bunch of functions for handling common AJAX events -jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function( i, o ) { - jQuery.fn[o] = function( f ) { +jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), function(i,o){ + jQuery.fn[o] = function(f){ return this.bind(o, f); }; }); jQuery.extend({ + get: function( url, data, callback, type ) { // shift arguments if data argument was omited if ( jQuery.isFunction( data ) ) { @@ -176,342 +171,120 @@ jQuery.extend({ /* timeout: 0, data: null, + dataType: null, + dataTypes: null, username: null, password: null, + cache: null, traditional: false, */ - // This function can be overriden by calling jQuery.ajaxSetup xhr: function() { return new window.XMLHttpRequest(); }, + xhrResponseFields: { + xml: "XML", + text: "Text", + json: "JSON" + }, + accepts: { xml: "application/xml, text/xml", html: "text/html", - script: "text/javascript, application/javascript", - json: "application/json, text/javascript", text: "text/plain", - _default: "*/*" - } - }, - - ajax: function( origSettings ) { - // IE8 leaks a lot when we've set abort, and IE6-8 a little - // when we have set onreadystatechange. Bug #6242 - // XXX IE7 still leaks when abort is called, no matter what - // we do - function cleanup() { - // IE6 will throw an error setting xhr.abort - try { - xhr.abort = xhr.onreadystatechange = jQuery.noop; - } catch(e) {} - } - - var s = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings), - jsonp, status, data, type = s.type.toUpperCase(), noContent = rnoContent.test(type); - - // toString fixes people passing a window.location or - // document.location to $.ajax, which worked in 1.4.2 and - // earlier (bug #7531). It should be removed in 1.5. - s.url = ("" + s.url).replace( rhash, "" ); - - // Use original (not extended) context object if it was provided - s.context = origSettings && origSettings.context != null ? origSettings.context : s; - - // convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Handle JSONP Parameter Callbacks - if ( s.dataType === "jsonp" ) { - if ( type === "GET" ) { - if ( !jsre.test( s.url ) ) { - s.url += (rquery.test( s.url ) ? "&" : "?") + (s.jsonp || "callback") + "=?"; + json: "application/json, text/javascript", + "*": "*/*" + }, + + autoDataType: { + xml: /xml/, + html: /html/, + json: /json/ + }, + + // Prefilters + // 1) They are useful to introduce custom dataTypes (see transport/jsonp for an example) + // 2) These are called: + // * BEFORE asking for a transport + // * AFTER param serialization (s.data is a string if s.processData is true) + // 3) They MUST be order agnostic + prefilters: [], + + // Transports bindings + // 1) key is the dataType + // 2) the catchall symbol "*" can be used + // 3) selection will start with transport dataType and THEN go to "*" if needed + transports: { + }, + + // Checkers + // 1) key is dataType + // 2) they are called to control successful response + // 3) error throws is used as error data + dataCheckers: { + + // Check if data is a string + "text": function(data) { + if ( typeof data != "string" ) { + jQuery.error("typeerror"); } - } else if ( !s.data || !jsre.test(s.data) ) { - s.data = (s.data ? s.data + "&" : "") + (s.jsonp || "callback") + "=?"; - } - s.dataType = "json"; - } - - // Build temporary JSONP function - if ( s.dataType === "json" && (s.data && jsre.test(s.data) || jsre.test(s.url)) ) { - jsonp = s.jsonpCallback || ("jsonp" + jsc++); - - // Replace the =? sequence both in the query string and the data - if ( s.data ) { - s.data = (s.data + "").replace(jsre, "=" + jsonp + "$1"); - } - - s.url = s.url.replace(jsre, "=" + jsonp + "$1"); - - // We need to make sure - // that a JSONP style response is executed properly - s.dataType = "script"; - - // Handle JSONP-style loading - var customJsonp = window[ jsonp ]; - - window[ jsonp ] = function( tmp ) { - if ( jQuery.isFunction( customJsonp ) ) { - customJsonp( tmp ); - - } else { - // Garbage collect - window[ jsonp ] = undefined; - - try { - delete window[ jsonp ]; - } catch( jsonpError ) {} + }, + + // Check if xml has been properly parsed + "xml": function(data) { + var documentElement = data ? data.documentElement : data; + if ( ! documentElement || ! documentElement.nodeName ) { + jQuery.error("typeerror"); } - - data = tmp; - jQuery.handleSuccess( s, xhr, status, data ); - jQuery.handleComplete( s, xhr, status, data ); - - if ( head ) { - head.removeChild( script ); - } - }; - } - - if ( s.dataType === "script" && s.cache === undefined ) { - s.cache = false; - } - - if ( s.cache === false && noContent ) { - var ts = jQuery.now(); - - // try replacing _= if it is there - var ret = s.url.replace(rts, "$1_=" + ts); - - // if nothing was replaced, add timestamp to the end - s.url = ret + ((ret === s.url) ? (rquery.test(s.url) ? "&" : "?") + "_=" + ts : ""); - } - - // If data is available, append data to url for GET/HEAD requests - if ( s.data && noContent ) { - s.url += (rquery.test(s.url) ? "&" : "?") + s.data; - } - - // Watch for a new set of requests - if ( s.global && jQuery.active++ === 0 ) { - jQuery.event.trigger( "ajaxStart" ); - } - - // Matches an absolute URL, and saves the domain - var parts = rurl.exec( s.url ), - remote = parts && (parts[1] && parts[1].toLowerCase() !== location.protocol || parts[2].toLowerCase() !== location.host); - - // If we're requesting a remote document - // and trying to load JSON or Script with a GET - if ( s.dataType === "script" && type === "GET" && remote ) { - var head = document.getElementsByTagName("head")[0] || document.documentElement; - var script = document.createElement("script"); - if ( s.scriptCharset ) { - script.charset = s.scriptCharset; - } - script.src = s.url; - - // Handle Script loading - if ( !jsonp ) { - var done = false; - - // Attach handlers for all browsers - script.onload = script.onreadystatechange = function() { - if ( !done && (!this.readyState || - this.readyState === "loaded" || this.readyState === "complete") ) { - done = true; - jQuery.handleSuccess( s, xhr, status, data ); - jQuery.handleComplete( s, xhr, status, data ); - - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - if ( head && script.parentNode ) { - head.removeChild( script ); - } - } - }; - } - - // Use insertBefore instead of appendChild to circumvent an IE6 bug. - // This arises when a base node is used (#2709 and #4378). - head.insertBefore( script, head.firstChild ); - - // We handle everything using the script element injection - return undefined; - } - - var requestDone = false; - - // Create the request object - var xhr = s.xhr(); - - if ( !xhr ) { - return; - } - - // Open the socket - // Passing null username, generates a login popup on Opera (#2865) - if ( s.username ) { - xhr.open(type, s.url, s.async, s.username, s.password); - } else { - xhr.open(type, s.url, s.async); - } - - // Need an extra try/catch for cross domain requests in Firefox 3 - try { - // Set content-type if data specified and content-body is valid for this type - if ( (s.data != null && !noContent) || (origSettings && origSettings.contentType) ) { - xhr.setRequestHeader("Content-Type", s.contentType); - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[s.url] ) { - xhr.setRequestHeader("If-Modified-Since", jQuery.lastModified[s.url]); - } - - if ( jQuery.etag[s.url] ) { - xhr.setRequestHeader("If-None-Match", jQuery.etag[s.url]); + if ( documentElement.nodeName == "parsererror" ) { + jQuery.error("parsererror"); } } - - // Set header so the called script knows that it's an XMLHttpRequest - // Only send the header if it's not a remote XHR - if ( !remote ) { - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - } - - // Set the Accepts header for the server, depending on the dataType - xhr.setRequestHeader("Accept", s.dataType && s.accepts[ s.dataType ] ? - s.accepts[ s.dataType ] + ", */*; q=0.01" : - s.accepts._default ); - } catch( headerError ) {} - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false ) { - // Handle the global AJAX counter - if ( s.global && jQuery.active-- === 1 ) { - jQuery.event.trigger( "ajaxStop" ); - } - - // close opended socket - xhr.abort(); - return false; - } - - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxSend", [xhr, s] ); - } - - // Wait for a response to come back - var onreadystatechange = xhr.onreadystatechange = function( isTimeout ) { - // The request was aborted - if ( !xhr || xhr.readyState === 0 || isTimeout === "abort" ) { - // Opera doesn't call onreadystatechange before this point - // so we simulate the call - if ( !requestDone ) { - jQuery.handleComplete( s, xhr, status, data ); - } - - requestDone = true; - if ( xhr ) { - cleanup(); - } - - // The transfer is complete and the data is available, or the request timed out - } else if ( !requestDone && xhr && (xhr.readyState === 4 || isTimeout === "timeout") ) { - requestDone = true; - - status = isTimeout === "timeout" ? - "timeout" : - !jQuery.httpSuccess( xhr ) ? - "error" : - s.ifModified && jQuery.httpNotModified( xhr, s.url ) ? - "notmodified" : - "success"; - - var errMsg; - - if ( status === "success" ) { - // Watch for, and catch, XML document parse errors - try { - // process the data (runs the xml through httpData regardless of callback) - data = jQuery.httpData( xhr, s.dataType, s ); - } catch( parserError ) { - status = "parsererror"; - errMsg = parserError; - } - } - - // Make sure that the request was successful or notmodified - if ( status === "success" || status === "notmodified" ) { - // JSONP handles its own success callback - if ( !jsonp ) { - jQuery.handleSuccess( s, xhr, status, data ); - } - } else { - jQuery.handleError( s, xhr, status, errMsg ); - } - - // Fire the complete handlers - if ( !jsonp ) { - jQuery.handleComplete( s, xhr, status, data ); - } - - if ( isTimeout === "timeout" ) { - xhr.abort(); + }, + + // List of data converters + // 1) key format is "source_type => destination_type" (spaces required) + // 2) the catchall symbol "*" can be used for source_type + dataConverters: { + + // Convert anything to text + "* => text": function(data) { + return "" + data; + }, + + // Text to html (no transformation) + "text => html": function(data) { + return data; + }, + + // Evaluate text as a json expression + "text => json": jQuery.parseJSON, + + // Parse text as xml + "text => xml": function(data) { + var xml, parser; + if ( window.DOMParser ) { // Standard + parser = new DOMParser(); + xml = parser.parseFromString(data,"text/xml"); + } else { // IE + xml = new ActiveXObject("Microsoft.XMLDOM"); + xml.async="false"; + xml.loadXML(data); } - - cleanup(); + return xml; } - }; - - // Override the abort handler, if we can (IE 6 doesn't allow it, but that's OK) - // Opera doesn't fire onreadystatechange at all on abort - try { - var oldAbort = xhr.abort; - xhr.abort = function() { - if ( xhr ) { - // oldAbort has no call property in IE7 so - // just do it this way, which works in all - // browsers - Function.prototype.call.call( oldAbort, xhr ); - } - - onreadystatechange( "abort" ); - }; - } catch( abortError ) {} - - // Timeout checker - if ( s.async && s.timeout > 0 ) { - setTimeout(function() { - // Check to see if the request is still happening - if ( xhr && !requestDone ) { - onreadystatechange( "timeout" ); - } - }, s.timeout); - } - - // Send the data - try { - xhr.send( noContent || s.data == null ? null : s.data ); - - } catch( sendError ) { - jQuery.handleError( s, xhr, null, sendError ); - - // Fire the complete handlers - jQuery.handleComplete( s, xhr, status, data ); } + }, - // firefox 1.5 doesn't fire statechange for sync requests - if ( !s.async ) { - onreadystatechange(); + // Main method + ajax: function( url , s ) { + + if ( arguments.length === 1 ) { + s = url; + url = s ? s.url : undefined; } - - // return XMLHttpRequest to allow aborting the request etc. - return xhr; + + return jQuery.xhr().open( s ? s.type : undefined , url ).send( undefined , s ); + }, // Serialize an array of form elements or a set of @@ -595,110 +368,7 @@ jQuery.extend({ // Last-Modified header cache for next request lastModified: {}, - etag: {}, - - handleError: function( s, xhr, status, e ) { - // If a local callback was specified, fire it - if ( s.error ) { - s.error.call( s.context, xhr, status, e ); - } - - // Fire the global callback - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxError", [xhr, s, e] ); - } - }, - - handleSuccess: function( s, xhr, status, data ) { - // If a local callback was specified, fire it and pass it the data - if ( s.success ) { - s.success.call( s.context, data, status, xhr ); - } - - // Fire the global callback - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxSuccess", [xhr, s] ); - } - }, - - handleComplete: function( s, xhr, status ) { - // Process result - if ( s.complete ) { - s.complete.call( s.context, xhr, status ); - } - - // The request was completed - if ( s.global ) { - jQuery.triggerGlobal( s, "ajaxComplete", [xhr, s] ); - } - - // Handle the global AJAX counter - if ( s.global && jQuery.active-- === 1 ) { - jQuery.event.trigger( "ajaxStop" ); - } - }, - - triggerGlobal: function( s, type, args ) { - (s.context && s.context.url == null ? jQuery(s.context) : jQuery.event).trigger(type, args); - }, - - // Determines if an XMLHttpRequest was successful or not - httpSuccess: function( xhr ) { - try { - // IE error sometimes returns 1223 when it should be 204 so treat it as success, see #1450 - return !xhr.status && location.protocol === "file:" || - xhr.status >= 200 && xhr.status < 300 || - xhr.status === 304 || xhr.status === 1223; - } catch(e) {} - - return false; - }, - - // Determines if an XMLHttpRequest returns NotModified - httpNotModified: function( xhr, url ) { - var lastModified = xhr.getResponseHeader("Last-Modified"), - etag = xhr.getResponseHeader("Etag"); - - if ( lastModified ) { - jQuery.lastModified[url] = lastModified; - } - - if ( etag ) { - jQuery.etag[url] = etag; - } - - return xhr.status === 304; - }, - - httpData: function( xhr, type, s ) { - var ct = xhr.getResponseHeader("content-type") || "", - xml = type === "xml" || !type && ct.indexOf("xml") >= 0, - data = xml ? xhr.responseXML : xhr.responseText; - - if ( xml && data.documentElement.nodeName === "parsererror" ) { - jQuery.error( "parsererror" ); - } - - // Allow a pre-filtering function to sanitize the response - // s is checked to keep backwards compatibility - if ( s && s.dataFilter ) { - data = s.dataFilter( data, type ); - } - - // The filter can actually parse the response - if ( typeof data === "string" ) { - // Get the JavaScript object, if JSON is used. - if ( type === "json" || !type && ct.indexOf("json") >= 0 ) { - data = jQuery.parseJSON( data ); - - // If the type is "script", eval it in global context - } else if ( type === "script" || !type && ct.indexOf("javascript") >= 0 ) { - jQuery.globalEval( data ); - } - } - - return data; - } + etag: {} }); @@ -711,19 +381,24 @@ jQuery.extend({ */ if ( window.ActiveXObject ) { jQuery.ajaxSettings.xhr = function() { - if ( window.location.protocol !== "file:" ) { - try { - return new window.XMLHttpRequest(); - } catch(xhrError) {} - } - + if ( window.location.protocol !== "file:" ) { try { - return new window.ActiveXObject("Microsoft.XMLHTTP"); - } catch(activeError) {} + return new window.XMLHttpRequest(); + } catch( xhrError ) {} + } + + try { + return new window.ActiveXObject("Microsoft.XMLHTTP"); + } catch( activeError ) {} }; } +var testXHR = jQuery.ajaxSettings.xhr(); + // Does this browser support XHR requests? -jQuery.support.ajax = !!jQuery.ajaxSettings.xhr(); +jQuery.support.ajax = !!testXHR; + +// Does this browser support crossDomain XHR requests +jQuery.support.cors = testXHR && "withCredentials" in testXHR; -})( jQuery ); +})(jQuery); \ No newline at end of file diff --git a/src/transports/jsonp.js b/src/transports/jsonp.js new file mode 100644 index 0000000..2bdce23 --- /dev/null +++ b/src/transports/jsonp.js @@ -0,0 +1,89 @@ +(function( jQuery ) { + +var jsc = jQuery.now(), + jsre = /\=\?(&|$)/, + rquery = /\?/; + +// Default jsonp callback name +jQuery.ajaxSettings.jsonpCallback = function() { + return "jsonp" + jsc++; +}; + +// Normalize jsonp queries +// 1) put callback parameter in url or data +// 2) ensure transportDataType is json +// 3) ensure options jsonp is always provided so that jsonp requests are always +// json request with the jsonp option set +jQuery.xhr.prefilter( function(s) { + + var transportDataType = s.dataTypes[0]; + + if ( s.jsonp || + transportDataType === "jsonp" || + transportDataType === "json" && ( jsre.test(s.url) || typeof(s.data) === "string" && jsre.test(s.data) ) ) { + + var jsonp = s.jsonp = s.jsonp || "callback", + jsonpCallback = s.jsonpCallback = + jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback, + url = s.url.replace(jsre, "=" + jsonpCallback + "$1"), + data = s.url == url && typeof(s.data) === "string" ? s.data.replace(jsre, "=" + jsonpCallback + "$1") : s.data; + + if ( url == s.url && data == s.data ) { + url = url += (rquery.test( url ) ? "&" : "?") + jsonp + "=" + jsonpCallback; + } + + s.url = url; + s.data = data; + + s.dataTypes[0] = "json"; + } + +}); + +// Bind transport to json dataType +jQuery.xhr.bindTransport("json", function(s) { + + if ( s.jsonp ) { + + // Put callback in place + var responseContainer, + jsonpCallback = s.jsonpCallback, + previous = window[ jsonpCallback ]; + + window [ jsonpCallback ] = function( response ) { + responseContainer = [response]; + }; + + s.complete = [function() { + + // Set callback back to previous value + window[ jsonpCallback ] = previous; + + // Call if it was a function and we have a response + if ( previous) { + if ( responseContainer && jQuery.isFunction ( previous ) ) { + window[ jsonpCallback ] ( responseContainer[0] ); + } + } else { + // else, more memory leak avoidance + try{ delete window[ jsonpCallback ]; } catch(e){} + } + + }, s.complete ]; + + // Use data converter to retrieve json after script execution + s.dataConverters["script => json"] = function() { + if ( ! responseContainer ) { + jQuery.error("Callback '" + jsonpCallback + "' was not called"); + } + return responseContainer[ 0 ]; + }; + + // Delegate to script transport + return "script"; + + } + +}); + +})(jQuery); diff --git a/src/transports/script.js b/src/transports/script.js new file mode 100644 index 0000000..5470dec --- /dev/null +++ b/src/transports/script.js @@ -0,0 +1,83 @@ +(function( jQuery ) { + +// Install text to script executor +jQuery.extend( true, jQuery.ajaxSettings , { + + accepts: { + script: "text/javascript, application/javascript" + }, + + autoDataType: { + script: /javascript/ + }, + + dataConverters: { + "text => script": jQuery.globalEval + } +} ); + +// Bind script tag hack transport +jQuery.xhr.bindTransport("script", function(s) { + + // Handle cache special case + if ( s.cache === undefined ) { + s.cache = false; + } + + // This transport only deals with cross domain get requests + if ( s.crossDomain && s.async && ( s.type === "GET" || ! s.data ) ) { + + s.global = false; + + var script, + head = document.getElementsByTagName("head")[0] || document.documentElement; + + return { + + send: function(_, callback) { + + script = document.createElement("script"); + + script.async = "async"; + + if ( s.scriptCharset ) { + script.charset = s.scriptCharset; + } + + script.src = s.url; + + // Attach handlers for all browsers + script.onload = script.onreadystatechange = function(statusText) { + + if ( (!script.readyState || + script.readyState === "loaded" || script.readyState === "complete") ) { + + // Handle memory leak in IE + script.onload = script.onreadystatechange = null; + + // Remove the script + if ( head && script.parentNode ) { + head.removeChild( script ); + } + + script = undefined; + + // Callback & dereference + callback(statusText ? 0 : 200, statusText || "success"); + } + }; + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709 and #4378). + head.insertBefore( script, head.firstChild ); + }, + + abort: function(statusText) { + if ( script ) { + script.onload(statusText); + } + } + }; + } +}); + +})(jQuery); diff --git a/src/transports/xhr.js b/src/transports/xhr.js new file mode 100644 index 0000000..129058e --- /dev/null +++ b/src/transports/xhr.js @@ -0,0 +1,191 @@ +(function( jQuery ) { + +var // Next fake timer id + xhrPollingId = jQuery.now(), + + // Callbacks hashtable + xhrs = {}, + + // #5280: see end of file + xhrUnloadAbortMarker = []; + + +jQuery.xhr.bindTransport( function( s , determineDataType ) { + + // Cross domain only allowed if supported through XMLHttpRequest + if ( ! s.crossDomain || jQuery.support.cors ) { + + var callback; + + return { + + send: function(headers, complete) { + + var xhr = s.xhr(), + handle; + + // Open the socket + // Passing null username, generates a login popup on Opera (#2865) + if ( s.username ) { + xhr.open(s.type, s.url, s.async, s.username, s.password); + } else { + xhr.open(s.type, s.url, s.async); + } + + // Requested-With header + // Not set for crossDomain requests with no content + // (see why at http://trac.dojotoolkit.org/ticket/9486) + // Won't change header if already provided in beforeSend + if ( ! ( s.crossDomain && ! s.hasContent ) && ! headers["x-requested-with"] ) { + headers["x-requested-with"] = "XMLHttpRequest"; + } + + // Need an extra try/catch for cross domain requests in Firefox 3 + try { + + jQuery.each(headers, function(key,value) { + xhr.setRequestHeader(key,value); + }); + + } catch(_) {} + + // Do send the request + try { + xhr.send( ( s.hasContent && s.data ) || null ); + } catch(e) { + complete(0, "error", "" + e); + return; + } + + // Listener + callback = function ( abortStatusText ) { + + // Was never called and is aborted or complete + if ( callback && ( abortStatusText || xhr.readyState === 4 ) ) { + + // Do not listen anymore + if (handle) { + xhr.onreadystatechange = jQuery.noop; + delete xhrs[ handle ]; + handle = undefined; + } + + callback = 0; + + // Get info + var status, statusText, response, responseHeaders; + + if ( abortStatusText ) { + + if ( xhr.readyState !== 4 ) { + xhr.abort(); + } + + // Stop here if unloadAbort + if ( abortStatusText === xhrUnloadAbortMarker ) { + return; + } + + status = 0; + statusText = abortStatusText; + + } else { + + status = xhr.status; + + try { // Firefox throws an exception when accessing statusText for faulty cross-domain requests + + statusText = xhr.statusText; + + } catch( e ) { + + statusText = ""; // We normalize with Webkit giving an empty statusText + + } + + responseHeaders = xhr.getAllResponseHeaders(); + + // Filter status for non standard behaviours + // (so many they seem to be the actual "standard") + status = + // Opera returns 0 when it should be 304 + // Webkit returns 0 for failing cross-domain no matter the real status + status === 0 ? + ( + ! s.crossDomain || statusText ? // Webkit, Firefox: filter out faulty cross-domain requests + ( + responseHeaders ? // Opera: filter out real aborts #6060 + 304 + : + 0 + ) + : + 302 // We assume 302 but could be anything cross-domain related + ) + : + ( + status == 1223 ? // IE sometimes returns 1223 when it should be 204 (see #1450) + 204 + : + status + ); + + // Guess response if needed & update datatype accordingly + if ( status >= 200 && status < 300 ) { + response = + determineDataType( + s, + xhr.getResponseHeader("content-type"), + xhr.responseText, + xhr.responseXML ); + } + } + + // Call complete + complete(status,statusText,response,responseHeaders); + } + }; + + // if we're in sync mode + // or it's in cache and has been retrieved directly (IE6 & IE7) + // we need to manually fire the callback + if ( ! s.async || xhr.readyState === 4 ) { + + callback(); + + } else { + + // Listener is externalized to handle abort on unload + handle = xhrPollingId++; + xhrs[ handle ] = xhr; + xhr.onreadystatechange = function() { + callback(); + }; + } + }, + + abort: function(statusText) { + if ( callback ) { + callback(statusText); + } + } + }; + } +}); + +// #5280: we need to abort on unload or IE will keep connections alive +jQuery(window).bind( "unload" , function() { + + // Abort all pending requests + jQuery.each(xhrs, function(_, xhr) { + if ( xhr.onreadystatechange ) { + xhr.onreadystatechange( xhrUnloadAbortMarker ); + } + }); + + // Resest polling structure to be safe + xhrs = {}; + +}); + +})(jQuery); diff --git a/src/xhr.js b/src/xhr.js new file mode 100644 index 0000000..fae1110 --- /dev/null +++ b/src/xhr.js @@ -0,0 +1,941 @@ +(function( jQuery , undefined ) { + +var rquery = /\?/, + rhash = /#.*$/, + rheaders = /^(.*?):\s*(.*?)\r?$/mg, // IE leaves an \r character at EOL + rnoContent = /^(?:GET|HEAD)$/, + rts = /([?&])_=[^&]*/, + rurl = /^(\w+:)?\/\/([^\/?#]+)/, + + slice = Array.prototype.slice, + + isFunction = jQuery.isFunction; + +// Creates a jQuery xhr object +jQuery.xhr = function( _native ) { + + if ( _native ) { + return jQuery.ajaxSettings.xhr(); + } + + function reset(force) { + + // We only need to reset if we went through the init phase + // (with the exception of object creation) + if ( force || internal ) { + + // Reset callbacks lists + callbacksLists = { + success: createCBList(), + error: createCBList(), + complete: createCBList() + }; + + // Reset private variables + requestHeaders = {}; + responseHeadersString = responseHeaders = internal = done = timeoutTimer = s = undefined; + + // Reset state + xhr.readyState = 0; + sendFlag = 0; + + // Remove responseX fields + for ( var name in xhr ) { + if ( /^response/.test(name) ) { + delete xhr[name]; + } + } + } + } + + function init() { + + var // Options extraction + + // Remove hash character (#7531: first for string promotion) + url = s.url = ( "" + s.url ).replace( rhash , "" ), + + // Uppercase the type + type = s.type = s.type.toUpperCase(), + + // Determine if request has content + hasContent = s.hasContent = ! rnoContent.test( type ), + + // Extract dataTypes list + dataType = s.dataType, + dataTypes = s.dataTypes = dataType ? jQuery.trim(dataType).toLowerCase().split(/\s+/) : ["*"], + + // Determine if a cross-domain request is in order + parts = rurl.exec( url.toLowerCase() ), + loc = location, + crossDomain = s.crossDomain = !!( parts && ( parts[1] && parts[1] != loc.protocol || parts[2] != loc.host ) ), + + // Get other options locally + data = s.data, + originalContentType = s.contentType, + prefilters = s.prefilters, + accepts = s.accepts, + headers = s.headers, + + // Other Variables + transportDataType, + i; + + // Convert data if not already a string + if ( data && s.processData && typeof data != "string" ) { + data = s.data = jQuery.param( data , s.traditional ); + } + + // Apply option prefilters + for (i in prefilters) { + prefilters[i](s); + } + + // Get internal + internal = selectTransport( s ); + + // Re-actualize url & data + url = s.url; + data = s.data; + + // If internal was found + if ( internal ) { + + // Get transportDataType + transportDataType = dataTypes[0]; + + // More options handling for requests with no content + if ( ! hasContent ) { + + // If data is available, append data to url + if ( data ) { + url += (rquery.test(url) ? "&" : "?") + data; + } + + // Add anti-cache in url if needed + if ( s.cache === false ) { + + var ts = jQuery.now(), + // try replacing _= if it is there + ret = url.replace(rts, "$1_=" + ts ); + + // if nothing was replaced, add timestamp to the end + url = ret + ((ret == url) ? (rquery.test(url) ? "&" : "?") + "_=" + ts : ""); + } + + s.url = url; + } + + // Set the correct header, if data is being sent + if ( ( data && hasContent ) || originalContentType ) { + requestHeaders["content-type"] = s.contentType; + } + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( s.ifModified ) { + if ( jQuery_lastModified[url] ) { + requestHeaders["if-modified-since"] = jQuery_lastModified[url]; + } + if ( jQuery_etag[url] ) { + requestHeaders["if-none-match"] = jQuery_etag[url]; + } + } + + // Set the Accepts header for the server, depending on the dataType + requestHeaders.accept = transportDataType && accepts[ transportDataType ] ? + accepts[ transportDataType ] + ( transportDataType !== "*" ? ", */*; q=0.01" : "" ) : + accepts[ "*" ]; + + // Check for headers option + if ( headers ) { + xhr.setRequestHeaders( headers ); + } + } + + callbackContext = s.context || s; + globalEventContext = s.context ? jQuery(s.context) : jQuery.event; + + for ( i in callbacksLists ) { + callbacksLists[i].bind(s[i]); + } + + // Watch for a new set of requests + if ( s.global && jQuery.active++ === 0 ) { + jQuery.event.trigger( "ajaxStart" ); + } + + done = whenDone; + } + + function whenDone(status, statusText, response, headers) { + + // Called once + done = undefined; + + // Reset sendFlag + sendFlag = 0; + + // Cache response headers + responseHeadersString = headers || ""; + + // Clear timeout if it exists + if ( timeoutTimer ) { + clearTimeout(timeoutTimer); + } + + var // Reference url + url = s.url, + // and ifModified status + ifModified = s.ifModified, + + // Is it a success? + isSuccess = 0, + // Stored success + success, + // Stored error + error = statusText; + + // If not timeout, force a jQuery-compliant status text + if ( statusText != "timeout" ) { + statusText = ( status >= 200 && status < 300 ) ? + "success" : + ( status === 304 ? "notmodified" : "error" ); + } + + // If successful, handle type chaining + if ( statusText === "success" || statusText === "notmodified" ) { + + // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. + if ( ifModified ) { + var lastModified = xhr.getResponseHeader("Last-Modified"), + etag = xhr.getResponseHeader("Etag"); + + if (lastModified) { + jQuery_lastModified[url] = lastModified; + } + if (etag) { + jQuery_etag[url] = etag; + } + } + + if ( ifModified && statusText === "notmodified" ) { + + success = null; + isSuccess = 1; + + } else { + // Chain data conversions and determine the final value + // (if an exception is thrown in the process, it'll be notified as an error) + try { + + function checkData(data) { + if ( data !== undefined ) { + var testFunction = s.dataCheckers[srcDataType]; + if ( isFunction( testFunction ) ) { + testFunction(data); + } + } + } + + function convertData (data) { + var conversionFunction = dataConverters[srcDataType+" => "+destDataType] || + dataConverters["* => "+destDataType], + noFunction = ! isFunction( conversionFunction ); + if ( noFunction ) { + if ( srcDataType != "text" && destDataType != "text" ) { + // We try to put text inbetween + var first = dataConverters[srcDataType+" => text"] || + dataConverters["* => text"], + second = dataConverters["text => "+destDataType] || + dataConverters["* => "+destDataType], + areFunctions = isFunction( first ) && isFunction( second ); + if ( areFunctions ) { + conversionFunction = function (data) { + return second( first ( data ) ); + }; + } + noFunction = ! areFunctions; + } + if ( noFunction ) { + jQuery.error( "no data converter between " + srcDataType + " and " + destDataType ); + } + + } + return conversionFunction(data); + } + + var dataTypes = s.dataTypes, + i, + length, + data = response, + dataConverters = s.dataConverters, + srcDataType, + destDataType, + responseTypes = s.xhrResponseFields; + + for ( i = 0, length = dataTypes.length ; i < length ; i++ ) { + + destDataType = dataTypes[i]; + + if ( !srcDataType ) { // First time + + // Copy type + srcDataType = destDataType; + // Check + checkData(data); + // Apply dataFilter + if ( isFunction( s.dataFilter ) ) { + data = s.dataFilter(data, s.dataType); + // Recheck data + checkData(data); + } + + } else { // Subsequent times + + // handle auto + // JULIAN: for reasons unknown to me === doesn't work here + if (destDataType == "*") { + + destDataType = srcDataType; + + } else if ( srcDataType != destDataType ) { + + // Convert + data = convertData(data); + // Copy type & check + srcDataType = destDataType; + checkData(data); + + } + + } + + // Copy response into the xhr if it hasn't been already + var responseDataType, + responseType = responseTypes[srcDataType]; + + if ( responseType ) { + + responseDataType = srcDataType; + + } else { + + responseType = responseTypes[ responseDataType = "text" ]; + + } + + if ( responseType !== 1 ) { + xhr[ "response" + responseType ] = data; + responseTypes[ responseType ] = 1; + } + + } + + // We have a real success + success = data; + isSuccess = 1; + + } catch(e) { + + statusText = "parsererror"; + error = "" + e; + + } + } + + } else { // if not success, mark it as an error + + error = error || statusText; + + } + + // Set data for the fake xhr object + xhr.status = status; + xhr.statusText = statusText; + + // Keep local copies of vars in case callbacks re-use the xhr + var _s = s, + _callbacksLists = callbacksLists, + _callbackContext = callbackContext, + _globalEventContext = globalEventContext; + + // Set state if the xhr hasn't been re-used + function _setState( value ) { + if ( xhr.readyState && s === _s ) { + setState( value ); + } + } + + // Really completed? + if ( status && s.async ) { + setState( 2 ); + _setState( 3 ); + } + + // We're done + _setState( 4 ); + + // Success + _callbacksLists.success.fire( isSuccess , _callbackContext , success, statusText, xhr); + if ( isSuccess && _s.global ) { + _globalEventContext.trigger( "ajaxSuccess", [xhr, _s, success] ); + } + // Error + _callbacksLists.error.fire( ! isSuccess , _callbackContext , xhr, statusText, error); + if ( !isSuccess && _s.global ) { + _globalEventContext.trigger( "ajaxError", [xhr, _s, error] ); + } + // Complete + _callbacksLists.complete.fire( 1 , _callbackContext, xhr, statusText); + if ( _s.global ) { + _globalEventContext.trigger( "ajaxComplete", [xhr, _s] ); + // Handle the global AJAX counter + if ( ! --jQuery.active ) { + jQuery.event.trigger( "ajaxStop" ); + } + } + } + + // Ready state control + function checkState( expected , test ) { + if ( expected !== true && ( expected === false || test === false || xhr.readyState !== expected ) ) { + jQuery.error("INVALID_STATE_ERR"); + } + } + + // Ready state change + function setState( value ) { + xhr.readyState = value; + if ( isFunction( xhr.onreadystatechange ) ) { + xhr.onreadystatechange(); + } + } + + var // jQuery lists + jQuery_lastModified = jQuery.lastModified, + jQuery_etag = jQuery.etag, + // Options object + s, + // Callback stuff + callbackContext, + globalEventContext, + callbacksLists, + // Headers (they are sent all at once) + requestHeaders, + // Response headers + responseHeadersString, + responseHeaders, + // Done callback + done, + // transport + internal, + // timeout handle + timeoutTimer, + // The send flag + sendFlag, + // Fake xhr + xhr = { + // state + readyState: 0, + + // Callback + onreadystatechange: null, + + // Open + open: function(type, url, async, username, password) { + + xhr.abort(); + reset(); + + s = { + type: type, + url: url, + async: async, + username: username, + password: password + }; + + setState(1); + + return xhr; + }, + + // Send + send: function(data, moreOptions) { + + checkState(1 , !sendFlag); + + s.data = data; + + s = jQuery.extend( true, + {}, + jQuery.ajaxSettings, + s, + moreOptions || ( moreOptions === false ? { global: false } : {} ) ); + + if ( moreOptions ) { + // We force the original context + // (plain objects used as context get extended) + s.context = moreOptions.context; + } + + init(); + + // If not internal, abort + if ( ! internal ) { + done( 0 , "transport not found" ); + return false; + } + + // Allow custom headers/mimetypes and early abort + if ( s.beforeSend ) { + + var _s = s; + + if ( s.beforeSend.call(callbackContext, xhr, s) === false || ! xhr.readyState || _s !== s ) { + + // Abort if not done + if ( xhr.readyState && _s === s ) { + xhr.abort(); + } + + // Handle the global AJAX counter + if ( _s.global && ! --jQuery.active ) { + jQuery.event.trigger( "ajaxStop" ); + } + + return false; + } + } + + sendFlag = 1; + + // Send global event + if ( s.global ) { + globalEventContext.trigger("ajaxSend", [xhr, s]); + } + + // Timeout + if ( s.async && s.timeout > 0 ) { + timeoutTimer = setTimeout(function(){ + xhr.abort("timeout"); + }, s.timeout); + } + + if ( s.async ) { + setState(1); + } + + try { + + internal.send(requestHeaders, done); + return xhr; + + } catch (e) { + + if ( done ) { + + done(0, "error", "" + e); + + } else { + + jQuery.error(e); + + } + } + + return false; + }, + + // Caches the header + setRequestHeader: function(name,value) { + checkState(1, !sendFlag); + requestHeaders[ name.toLowerCase() ] = value; + return xhr; + }, + + // Ditto with an s + setRequestHeaders: function(map) { + checkState(1, !sendFlag); + for ( var name in map ) { + requestHeaders[ name.toLowerCase() ] = map[name]; + } + return xhr; + }, + + // Utility method to get headers set + getRequestHeader: function(name) { + checkState(1, !sendFlag); + return requestHeaders[ name.toLowerCase() ]; + }, + + // Raw string + getAllResponseHeaders: function() { + return xhr.readyState <= 1 ? "" : responseHeadersString; + }, + + // Builds headers hashtable if needed + getResponseHeader: function( key ) { + + if ( xhr.readyState <= 1 ) { + + return null; + + } + + if ( responseHeaders === undefined ) { + + responseHeaders = {}; + + if ( typeof responseHeadersString === "string" ) { + + var match; + + while( ( match = rheaders.exec( responseHeadersString ) ) ) { + responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ]; + } + } + } + return responseHeaders[ key.toLowerCase() ]; + }, + + // Cancel the request + abort: function(statusText) { + if (internal) { + internal.abort( statusText || "abort" ); + } + xhr.readyState = 0; + } + }; + + // Init data (so that we can bind callbacks early + reset(1); + + // Install callbacks related methods + jQuery.each(["bind","unbind"], function(_, name) { + xhr[name] = function(type) { + + var functors = slice.call(arguments,1), + list; + + jQuery.each(type.split(/\s+/g), function() { + list = callbacksLists[this]; + if ( list ) { + list[name].apply(list, functors ); + } + }); + + return this; + }; + }); + + jQuery.each(callbacksLists, function(name) { + var list; + xhr[name] = function() { + list = callbacksLists[name]; + if ( list ) { + list.bind.apply(list, arguments ); + } + return this; + }; + }); + + // Return the xhr emulation + return xhr; +}; + +// Create a callback list +function createCBList() { + + var functors = [], + autoFire = 0, + fireArgs, + list = { + + fire: function( flag , context ) { + + // Save info for later bindings + fireArgs = arguments; + + // Remove autoFire to keep bindings in order + autoFire = 0; + + var args = slice.call( fireArgs , 2 ); + + // Execute callbacks + while ( flag && functors.length ) { + flag = functors.shift().apply( context , args ) !== false; + } + + // Clean if asked to stop + if ( ! flag ) { + clean(); + } + + // Set autoFire + autoFire = 1; + }, + + bind: function() { + + var args = arguments, + i = 0, + length = args.length, + func; + + for ( ; i < length ; i++ ) { + + func = args[ i ]; + + if ( jQuery.isArray(func) ) { + + list.bind.apply( list , func ); + + } else if ( isFunction(func) ) { + + // Add if not already in + if ( ! pos( func ) ) { + functors.push( func ); + } + } + } + + if ( autoFire ) { + list.fire.apply( list , fireArgs ); + } + }, + + unbind: function() { + + var i = 0, + args = arguments, + length = args.length, + func, + position; + + if ( length ) { + + for( ; i < length ; i++ ) { + func = args[i]; + if ( jQuery.isArray(func) ) { + list.unbind.apply(list,func); + } else if ( isFunction(func) ) { + position = pos(func); + if ( position ) { + functors.splice(position-1,1); + } + } + } + + } else { + + functors = []; + + } + + } + + }; + + // Get the index of the functor in the list (1-based) + function pos( func ) { + for (var i = 0, length = functors.length; i < length && functors[i] !== func; i++) { + } + return i < length ? ( i + 1 ) : 0; + } + + // Clean the object + function clean() { + // Empty callbacks list + functors = []; + // Inhibit methods + for (var i in list) { + list[i] = jQuery.noop; + } + } + + return list; +} + +jQuery.extend(jQuery.xhr, { + + // Add new prefilter + prefilter: function (functor) { + if ( isFunction(functor) ) { + jQuery.ajaxSettings.prefilters.push( functor ); + } + return this; + }, + + // Bind a transport to one or more dataTypes + bindTransport: function () { + + var args = arguments, + i, + start = 0, + length = args.length, + dataTypes = [ "*" ], + functors = [], + functor, + first, + append, + list, + transports = jQuery.ajaxSettings.transports; + + if ( length ) { + + if ( ! isFunction( args[ 0 ] ) ) { + + dataTypes = args[ 0 ].toLowerCase().split(/\s+/); + start = 1; + + } + + if ( dataTypes.length && start < length ) { + + for ( i = start; i < length; i++ ) { + functor = args[i]; + if ( isFunction(functor) ) { + functors.push( functor ); + } + } + + if ( functors.length ) { + + jQuery.each ( dataTypes, function( _ , dataType ) { + + first = /^\+/.test( dataType ); + + if (first) { + dataType = dataType.substr(1); + } + + if ( dataType !== "" ) { + + append = Array.prototype[ first ? "unshift" : "push" ]; + + list = transports[ dataType ]; + + jQuery.each ( functors, function( _ , functor ) { + + if ( ! list ) { + + list = transports[ dataType ] = [ functor ]; + + } else { + + append.call( list , functor ); + } + } ); + } + + } ); + } + } + } + + return this; + } + + +}); + +// Select a transport given options +function selectTransport( s ) { + + var dataTypes = s.dataTypes, + transportDataType, + transportsList, + transport, + i, + length, + checked = {}, + flag; + + function initSearch( dataType ) { + + flag = transportDataType !== dataType && ! checked[ dataType ]; + + if ( flag ) { + + checked[ dataType ] = 1; + transportDataType = dataType; + transportsList = s.transports[ dataType ]; + i = -1; + length = transportsList ? transportsList.length : 0 ; + } + + return flag; + } + + initSearch( dataTypes[ 0 ] ); + + for ( i = 0 ; ! transport && i <= length ; i++ ) { + + if ( i === length ) { + + initSearch( "*" ); + + } else { + + transport = transportsList[ i ]( s , determineDataType ); + + // If we got redirected to another dataType + // Search there (if not in progress or already tried) + if ( typeof( transport ) === "string" && + initSearch( transport ) ) { + + dataTypes.unshift( transport ); + transport = 0; + } + } + } + + return transport; +} + +// Utility function that handles dataType when response is received +// (for those transports that can give text or xml responses) +function determineDataType( s , ct , text , xml ) { + + var autoDataType = s.autoDataType, + type, + regexp, + dataTypes = s.dataTypes, + transportDataType = dataTypes[0], + response; + + // Auto (xml, json, script or text determined given headers) + if ( transportDataType === "*" ) { + + for ( type in autoDataType ) { + if ( ( regexp = autoDataType[ type ] ) && regexp.test( ct ) ) { + transportDataType = dataTypes[0] = type; + break; + } + } + } + + // xml and parsed as such + if ( transportDataType === "xml" && + xml && + xml.documentElement /* #4958 */ ) { + + response = xml; + + // Text response was provided + } else { + + response = text; + + // If it's not really text, defer to dataConverters + if ( transportDataType !== "text" ) { + dataTypes.unshift( "text" ); + } + + } + + return response; +} + +})(jQuery); diff --git a/test/data/atom+xml.php b/test/data/atom+xml.php new file mode 100644 index 0000000..944591a --- /dev/null +++ b/test/data/atom+xml.php @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/test/data/css.php b/test/data/css.php new file mode 100644 index 0000000..9d079e7 --- /dev/null +++ b/test/data/css.php @@ -0,0 +1,15 @@ + + div# { margin-left: 27px } + \ No newline at end of file diff --git a/test/data/headers.php b/test/data/headers.php new file mode 100644 index 0000000..598cadc --- /dev/null +++ b/test/data/headers.php @@ -0,0 +1,5 @@ + diff --git a/test/index.html b/test/index.html index e668727..bd11ebb 100644 --- a/test/index.html +++ b/test/index.html @@ -20,6 +20,10 @@ + + + + diff --git a/test/unit/ajax.js b/test/unit/ajax.js index a0f3d49..87430c9 100644 --- a/test/unit/ajax.js +++ b/test/unit/ajax.js @@ -38,6 +38,216 @@ test("jQuery.ajax() - success callbacks", function() { }); }); +test("jQuery.ajax() - success callbacks - (url, options) syntax", function() { + expect( 8 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + start(); + }).ajaxSend(function(){ + ok( true, "ajaxSend" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete" ); + }).ajaxError(function(){ + ok( false, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( true, "ajaxSuccess" ); + }); + + jQuery.ajax( url("data/name.html") , { + beforeSend: function(){ ok(true, "beforeSend"); }, + success: function(){ ok(true, "success"); }, + error: function(){ ok(false, "error"); }, + complete: function(){ ok(true, "complete"); } + }); + }, 13); +}); + +test("jQuery.ajax() - success/error callbacks (remote)", function() { + + var supports = jQuery.support.cors; + + expect( supports ? 9 : 6 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + start(); + }).ajaxSend(function(){ + ok( supports , "ajaxSend" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete" ); + }).ajaxError(function(){ + ok( ! supports, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( supports, "ajaxSuccess" ); + }); + + jQuery.ajax({ + // JULIAN TODO: Get an url especially for jQuery + url: "http://rockstarapps.com/test.php", + dataType: "text", + beforeSend: function(){ ok(supports, "beforeSend"); }, + success: function( val ){ ok(supports, "success"); ok(supports && val.length, "data received"); }, + error: function(_ , a , b ){ ok(!supports, "error"); }, + complete: function(){ ok(true, "complete"); } + }); + }, 13); +}); + +test("jQuery.ajax() - success callbacks (late binding)", function() { + expect( 8 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + start(); + }).ajaxSend(function(){ + ok( true, "ajaxSend" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete" ); + }).ajaxError(function(){ + ok( false, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( true, "ajaxSuccess" ); + }); + + jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function(){ ok(true, "beforeSend"); } + }) + .complete(function(){ ok(true, "complete"); }) + .success(function(){ ok(true, "success"); }) + .error(function(){ ok(false, "error"); }); + }, 13); +}); + +test("jQuery.ajax() - success callbacks (oncomplete binding)", function() { + expect( 8 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + }).ajaxSend(function(){ + ok( true, "ajaxSend" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete" ); + }).ajaxError(function(){ + ok( false, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( true, "ajaxSuccess" ); + }); + + jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function(){ ok(true, "beforeSend"); }, + complete: function(xhr) { + xhr + .complete(function(){ ok(true, "complete"); }) + .success(function(){ ok(true, "success"); }) + .error(function(){ ok(false, "error"); }) + .complete(function(){ start(); }); + } + }) + }, 13); +}); + +test("jQuery.ajax() - success callbacks (very late binding)", function() { + expect( 8 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + }).ajaxSend(function(){ + ok( true, "ajaxSend" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete" ); + }).ajaxError(function(){ + ok( false, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( true, "ajaxSuccess" ); + }); + + jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function(){ ok(true, "beforeSend"); }, + complete: function(xhr) { + setTimeout (function() { + xhr + .complete(function(){ ok(true, "complete"); }) + .success(function(){ ok(true, "success"); }) + .error(function(){ ok(false, "error"); }) + .complete(function(){ start(); }); + },100); + } + }) + }, 13); +}); + +test("jQuery.ajax() - success callbacks (order)", function() { + expect( 1 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + var testString = ""; + + setTimeout(function(){ + jQuery.ajax({ + url: url("data/name.html"), + success: function( _1 , _2 , xhr ) { + xhr.success(function() { + xhr.success(function() { + testString += "E"; + }); + testString += "D"; + }); + testString += "A"; + }, + complete: function() { + strictEqual(testString, "ABCDE", "Proper order"); + start(); + } + }).success(function() { + testString += "B"; + }).success(function() { + testString += "C"; + }); + }, 13); +}); + test("jQuery.ajax() - error callbacks", function() { expect( 8 ); stop(); @@ -68,9 +278,35 @@ test("jQuery.ajax() - error callbacks", function() { }); }); +test(".ajax() - headers" , function() { + + // No multiple line headers in IE + expect( jQuery.browser.msie ? 2 : 4 ); + + stop(); + + jQuery.ajax({ + url: url("data/headers.php"), + success: function( _1 , _2 , xhr ){ + ok(true, "success"); + equals( xhr.getResponseHeader( "Single-Line" ) , "Hello World" , "Single line header" ); + // No multiple line headers in IE + if ( ! jQuery.browser.msie ) { + // Each browser has its own unique way to deal with spaces after line breaks + // in multiple line headers, so we use regular expressions + ok( /^Hello\s+World$/.test( xhr.getResponseHeader( "Multiple-Line" ) ) , "Multiple line" ); + ok( /^Hello\s+Beautiful\s+World$/.test( xhr.getResponseHeader( "Multiple-Multiple-Line" ) ) , "Multiple multiple line" ); + } + start(); + }, + error: function(){ ok(false, "error"); } + }); + +}); + test(".ajax() - hash", function() { expect(3); - + jQuery.ajax({ url: "data/name.html#foo", beforeSend: function( xhr, settings ) { @@ -78,15 +314,15 @@ test(".ajax() - hash", function() { return false; } }); - + jQuery.ajax({ url: "data/name.html?abc#foo", beforeSend: function( xhr, settings ) { - equals(settings.url, "data/name.html?abc", "Make sure that the URL is trimmed."); + equals(settings.url, "data/name.html?abc", "Make sure that the URL is trimmed."); return false; } }); - + jQuery.ajax({ url: "data/name.html?abc#foo", data: { "test": 123 }, @@ -100,7 +336,7 @@ test(".ajax() - hash", function() { test(".ajax() - 304", function() { expect( 1 ); stop(); - + jQuery.ajax({ url: url("data/notmodified.php"), success: function(){ ok(true, "304 ok"); }, @@ -163,6 +399,136 @@ test("jQuery.ajax() - abort", function() { equals( xhr.readyState, 0, "XHR readyState indicates successful abortion" ); }); +test("jQuery.ajax() - readyState (success)", function() { + expect( 1 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + var control = ""; + + setTimeout(function(){ + jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function( xhr ) { + xhr.onreadystatechange = function() { + control += xhr.readyState; + } + }, + complete: function(){ + setTimeout( function() { + equals( control , "1234" , "onreadystatechange was properly called" ); + }, 13 ); + start(); + } + }); + }, 13); +}); + +test("jQuery.ajax() - readyState (abort)", function() { + expect( 2 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + var control = ""; + + setTimeout(function(){ + + jQuery.ajaxSetup({ timeout: 500 }); + + jQuery.ajax({ + url: url("data/name.php?wait=5"), + beforeSend: function( xhr ) { + xhr.onreadystatechange = function() { + control += xhr.readyState; + } + }, + complete: function( xhr ){ + setTimeout( function() { + equals( control , "14" , "onreadystatechange was properly called" ); + equals( xhr.readyState, 0 , "readyState is 0" ); + }, 13 ); + start(); + } + }); + }, 13); +}); + +test("jQuery.xhr() - reuse", function() { + expect( 15 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + var number = 0; + + setTimeout(function(){ + jQuery('#foo').ajaxStart(function(){ + ok( true, "ajaxStart" ); + }).ajaxStop(function(){ + ok( true, "ajaxStop" ); + start(); + }).ajaxSend(function(){ + number++; + ok( true, "ajaxSend (" + number +")" ); + }).ajaxComplete(function(){ + ok( true, "ajaxComplete (" + number +")" ); + }).ajaxError(function(){ + ok( false, "ajaxError (" + number +")" ); + }).ajaxSuccess(function(){ + ok( true, "ajaxSuccess (" + number +")" ); + }); + + jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function(){ ok(true, "beforeSend (1)"); }, + success: function( _1 , _2 , xhr ){ + ok(true, "success (1)"); + xhr.complete(function() { + ok(true, "complete (1bis)"); + }); + xhr.open( "GET", url("data/name.html") ); + xhr.success( function(){ ok(true, "beforeSend (2)"); } ) + xhr.send( null, { + success: function(){ ok(true, "success (2)"); }, + error: function(){ ok(false, "error (2)"); }, + complete: function(){ ok(true, "complete (2)"); } + } ); + }, + error: function(){ ok(false, "error (1)"); }, + complete: function(){ ok(true, "complete (1)"); } + }); + }, 13); +}); + +test("jQuery.xhr() - early binding", function() { + expect( 2 ); + + jQuery.ajaxSetup({ timeout: 0 }); + + stop(); + + jQuery.xhr() + .success( function(){ ok(true, "success"); } ) + .error( function(){ ok(false, "error"); } ) + .complete( function(){ ok(true, "complete"); start(); } ) + .open( "GET", url("data/name.html") ) + .send(); +}); + +test("jQuery.xhr() - get native implementation", function() { + + var xhr = jQuery.xhr(true); + + ok( xhr.readyState !== undefined , "implements XMLHttpRequest" ); + ok( ! jQuery.isFunction( xhr.success ) , "is not jQuery's abstraction" ); + +}); + test("Ajax events with context", function() { expect(14); @@ -277,6 +643,34 @@ test("jQuery.ajax() - disabled globals", function() { }); }); +test("jQuery.xhr() - disabled globals through xhr.send(data , false)", function() { + expect( 2 ); + stop(); + + jQuery('#foo').ajaxStart(function(){ + ok( false, "ajaxStart" ); + }).ajaxStop(function(){ + ok( false, "ajaxStop" ); + }).ajaxSend(function(){ + ok( false, "ajaxSend" ); + }).ajaxComplete(function(){ + ok( false, "ajaxComplete" ); + }).ajaxError(function(){ + ok( false, "ajaxError" ); + }).ajaxSuccess(function(){ + ok( false, "ajaxSuccess" ); + }); + + jQuery.xhr() + .success(function(){ ok(true, "success"); }) + .error(function(){ ok(false, "error"); }) + .complete(function(){ + ok(true, "complete"); + setTimeout(function(){ start(); }, 13); + }) + .open("GET", url("data/name.html")).send(undefined, false); +}); + test("jQuery.ajax - xml: non-namespace elements inside namespaced elements", function() { expect(3); stop(); @@ -292,6 +686,21 @@ test("jQuery.ajax - xml: non-namespace elements inside namespaced elements", fun }); }); +test("jQuery.ajax - xml: non-namespace elements inside namespaced elements (over JSONP)", function() { + expect(3); + stop(); + jQuery.ajax({ + url: url("data/with_fries_over_jsonp.php"), + dataType: "jsonp xml", + success: function(resp) { + equals( jQuery("properties", resp).length, 1, 'properties in responseXML' ); + equals( jQuery("jsconf", resp).length, 1, 'jsconf in responseXML' ); + equals( jQuery("thing", resp).length, 2, 'things in responseXML' ); + start(); + } + }); +}); + test("jQuery.ajax - HEAD requests", function() { expect(2); @@ -315,7 +724,7 @@ test("jQuery.ajax - HEAD requests", function() { }); } }); - + }); test("jQuery.ajax - beforeSend", function() { @@ -359,6 +768,27 @@ test("jQuery.ajax - beforeSend, cancel request (#2688)", function() { ok( request === false, "canceled request must return false instead of XMLHttpRequest instance" ); }); +test("jQuery.ajax - beforeSend, cancel request manually", function() { + expect(2); + var request = jQuery.ajax({ + url: url("data/name.html"), + beforeSend: function(xhr) { + ok( true, "beforeSend got called, canceling" ); + xhr.abort(); + }, + success: function() { + ok( false, "request didn't get canceled" ); + }, + complete: function() { + ok( false, "request didn't get canceled" ); + }, + error: function() { + ok( false, "request didn't get canceled" ); + } + }); + ok( request === false, "canceled request must return false instead of XMLHttpRequest instance" ); +}); + window.foobar = null; window.testFoo = undefined; @@ -456,7 +886,7 @@ test("jQuery.param()", function() { equals( jQuery.param({"foo": {"bar": []} }), "foo%5Bbar%5D=", "Empty array param" ); equals( jQuery.param({"foo": {"bar": [], foo: 1} }), "foo%5Bbar%5D=&foo%5Bfoo%5D=1", "Empty array param" ); equals( jQuery.param({"foo": {"bar": {}} }), "foo%5Bbar%5D=", "Empty object param" ); - + jQuery.ajaxSetup({ traditional: true }); var params = {foo:"bar", baz:42, quux:"All your base are belong to us"}; @@ -841,12 +1271,13 @@ test("jQuery.ajax() - JSONP, Local", function() { }); }); -test("JSONP - Custom JSONP Callback", function() { +test("jQuery.ajax() - JSONP - Custom JSONP Callback", function() { expect(1); stop(); window.jsonpResults = function(data) { ok( data.data, "JSON results returned (GET, custom callback function)" ); + window.jsonpResults = undefined; start(); }; @@ -943,7 +1374,7 @@ test("jQuery.ajax() - script, Remote with POST", function() { expect(3); var base = window.location.href.replace(/[^\/]*$/, ""); - + stop(); jQuery.ajax({ @@ -1033,6 +1464,30 @@ test("jQuery.ajax() - json by content-type", function() { }); }); +test("jQuery.ajax() - json by content-type disabled with options", function() { + expect(6); + + stop(); + + jQuery.ajax({ + url: url("data/json.php"), + data: { header: "json", json: "array" }, + autoDataType: { + json: false + }, + success: function( text ) { + equals( typeof text , "string" , "json wasn't auto-determined" ); + var json = this.dataConverters["text => json"]( text ); + ok( json.length >= 2, "Check length"); + equals( json[0].name, 'John', 'Check JSON: first, name' ); + equals( json[0].age, 21, 'Check JSON: first, age' ); + equals( json[1].name, 'Peter', 'Check JSON: second, name' ); + equals( json[1].age, 25, 'Check JSON: second, age' ); + start(); + } + }); +}); + test("jQuery.getJSON(String, Hash, Function) - JSON array", function() { expect(5); stop(); @@ -1290,7 +1745,7 @@ test("jQuery.ajax - If-Modified-Since support", function() { ok(data == null, "response body should be empty") } start(); - }, + }, error: function() { // Do this because opera simply refuses to implement 304 handling :( // A feature-driven way of detecting this would be appreciated @@ -1298,10 +1753,11 @@ test("jQuery.ajax - If-Modified-Since support", function() { ok(jQuery.browser.opera, "error"); ok(jQuery.browser.opera, "error"); start(); - } + } }); }, error: function() { + equals(false, "error"); // Do this because opera simply refuses to implement 304 handling :( // A feature-driven way of detecting this would be appreciated // See: http://gist.github.com/599419 @@ -1336,8 +1792,8 @@ test("jQuery.ajax - Etag support", function() { ok(data == null, "response body should be empty") } start(); - }, - error: function() { + }, + error: function() { // Do this because opera simply refuses to implement 304 handling :( // A feature-driven way of detecting this would be appreciated // See: http://gist.github.com/599419 @@ -1357,6 +1813,60 @@ test("jQuery.ajax - Etag support", function() { }); }); +test("jQuery ajax - headers", function() { + + stop(); + + jQuery.ajax(url("data/css.php?wait=1&id=headers"), { + headers: { + testKey: "testValue" + }, + beforeSend: function( xhr ) { + equals( xhr.getRequestHeader("testKey") , "testValue" , "Headers properly set" ); + setTimeout( start , 13 ); + return false; + } + }); + +}); + +test("jQuery ajax - failing cross-domain", function() { + + expect( 2 ); + + stop(); + + var i = 2; + + jQuery.ajax({ + url: 'http://somewebsitethatdoesnotexist.com', + success: function(){ ok( false , "success" ); }, + error: function(xhr,_,e){ ok( true , "file not found: " + xhr.status + " => " + e ); }, + complete: function() { if ( ! --i ) start(); } + }); + + jQuery.ajax({ + url: 'http://www.google.com', + success: function(){ ok( false , "success" ); }, + error: function(xhr,_,e){ ok( true , "access denied: " + xhr.status + " => " + e ); }, + complete: function() { if ( ! --i ) start(); } + }); + +}); + +test("jQuery ajax - atom+xml", function() { + + stop(); + + jQuery.ajax({ + url: url( 'data/atom+xml.php' ), + success: function(){ ok( true , "success" ); }, + error: function(){ ok( false , "error" ); }, + complete: function() { start(); } + }); + +}); + test("jQuery.ajax - active counter", function() { ok( jQuery.active == 0, "ajax active counter should be zero: " + jQuery.active ); }); @@ -1374,4 +1884,4 @@ test( "jQuery.ajax - Location object as url (#7531)", 1, function () { } -//} +//} \ No newline at end of file -- 1.7.10.4