X-Git-Url: http://git.asbjorn.biz/?a=blobdiff_plain;f=src%2Fxhr.js;h=b9db717712f3839776cf8377b16a56a73288249e;hb=6f53be1839b10486c624e07595533d57c602eda5;hp=66be46362b84a1e8d03b3b2e39ed3ccba3d65c04;hpb=a500d33c5d63aa22f024472b2cc8c15c624a342e;p=jquery.git diff --git a/src/xhr.js b/src/xhr.js index 66be463..b9db717 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -1,44 +1,43 @@ (function( jQuery ) { -var rquery = /\?/, +var rquery_xhr = /\?/, rhash = /#.*$/, rheaders = /^(.*?):\s*(.*?)\r?$/mg, // IE leaves an \r character at EOL rnoContent = /^(?:GET|HEAD)$/, rts = /([?&])_=[^&]*/, rurl = /^(\w+:)?\/\/([^\/?#]+)/, - sliceFunc = Array.prototype.slice, - - isFunction = jQuery.isFunction; + sliceFunc = Array.prototype.slice; // Creates a jQuery xhr object jQuery.xhr = function( _native ) { - + if ( _native ) { return jQuery.ajaxSettings.xhr(); } - function reset(force) { + 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() - }; + deferred = jQuery.Deferred(); + completeDeferred = jQuery._Deferred(); + + xhr.success = xhr.then = deferred.then; + xhr.error = xhr.fail = deferred.fail; + xhr.complete = completeDeferred.then; // 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) ) { @@ -47,134 +46,129 @@ jQuery.xhr = function( _native ) { } } } - + 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 ); + internal = jQuery.xhr.prefilter( s ).transport( 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; + url += (rquery_xhr.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 : ""); + url = ret + ((ret == url) ? (rquery_xhr.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] ) { + 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 for ( i in headers ) { requestHeaders[ i.toLowerCase() ] = headers[ i ]; - } + } } - + callbackContext = s.context || s; globalEventContext = s.context ? jQuery(s.context) : jQuery.event; - for ( i in callbacksLists ) { - callbacksLists[i].bind(s[i]); + for ( i in { success:1, error:1, complete:1 } ) { + xhr[ i ]( 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 || ""; @@ -182,12 +176,12 @@ jQuery.xhr = function( _native ) { if ( timeoutTimer ) { clearTimeout(timeoutTimer); } - + var // Reference url url = s.url, // and ifModified status ifModified = s.ifModified, - + // Is it a success? isSuccess = 0, // Stored success @@ -197,19 +191,19 @@ jQuery.xhr = function( _native ) { // If not timeout, force a jQuery-compliant status text if ( statusText != "timeout" ) { - statusText = ( status >= 200 && status < 300 ) ? + 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; } @@ -217,176 +211,133 @@ jQuery.xhr = function( _native ) { 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, + var i, + current, + prev, + checker, + conv1, + conv2, + oneConv, + convertion, + dataTypes = s.dataTypes, dataConverters = s.dataConverters, - srcDataType, - destDataType, - responseTypes = s.xhrResponseFields; + dataFilter = s.dataFilter, + responses = { + "xml": "XML", + "text": "Text" + }; + + for( i = 0 ; i < dataTypes.length ; i++ ) { - for ( i = 0, length = dataTypes.length ; i < length ; i++ ) { - - destDataType = dataTypes[i]; + current = 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); - - } - + if ( responses[ current ] ) { + xhr[ "response" + responses[ current ] ] = response; + responses[ current ] = 0; } - - // Copy response into the xhr if it hasn't been already - var responseDataType, - responseType = responseTypes[srcDataType]; - if ( responseType ) { - - responseDataType = srcDataType; - - } else { + if ( i ) { - responseType = responseTypes[ responseDataType = "text" ]; + if ( prev !== "*" && current !== "*" && prev !== current ) { - } + oneConv = conv1 = + dataConverters[ ( conversion = prev + " " + current ) ] || + dataConverters[ "* " + current ]; + + if ( oneConv !== true ) { + + if ( ! oneConv && prev !== "text" && current !== "text" ) { + conv1 = dataConverters[ prev + " text" ] || dataConverters[ "* text" ]; + conv2 = dataConverters[ "text " + current ]; + } + + if ( oneConv || conv1 && conv2 ) { + response = oneConv ? conv1( response ) : conv2( conv1( response ) ); + } else { + throw "no " + conversion; + } + } + } + } else if ( dataFilter ) { - if ( responseType !== 1 ) { - xhr[ "response" + responseType ] = data; - responseTypes[ responseType ] = 1; + response = s.dataFilter( response ); + dataTypes = s.dataTypes; } + prev = current; } - + // We have a real success - success = data; + success = response; 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, + _deferred = deferred, + _completeDeferred = completeDeferred, _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] ); + // Success/Error + if ( isSuccess ) { + _deferred.fire( _callbackContext , [ success , statusText , xhr ] ); + } else { + _deferred.fireReject( _callbackContext , [ xhr , statusText , error ] ); } - // Error - _callbacksLists.error.fire( ! isSuccess , _callbackContext , xhr, statusText, error); - if ( !isSuccess && _s.global ) { - _globalEventContext.trigger( "ajaxError", [xhr, _s, error] ); + + if ( _s.global ) { + _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] ); } + // Complete - _callbacksLists.complete.fire( 1 , _callbackContext, xhr, statusText); + _completeDeferred.fire( _callbackContext, [ xhr , statusText ] ); + if ( _s.global ) { _globalEventContext.trigger( "ajaxComplete", [xhr, _s] ); // Handle the global AJAX counter @@ -395,22 +346,22 @@ jQuery.xhr = function( _native ) { } } } - + // 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 ) ) { + if ( jQuery.isFunction( xhr.onreadystatechange ) ) { xhr.onreadystatechange(); } } - + var // jQuery lists jQuery_lastModified = jQuery.lastModified, jQuery_etag = jQuery.etag, @@ -419,7 +370,8 @@ jQuery.xhr = function( _native ) { // Callback stuff callbackContext, globalEventContext, - callbacksLists, + deferred, + completeDeferred, // Headers (they are sent all at once) requestHeaders, // Response headers @@ -437,16 +389,16 @@ jQuery.xhr = function( _native ) { xhr = { // state readyState: 0, - + // Callback onreadystatechange: null, - + // Open open: function(type, url, async, username, password) { - + xhr.abort(); reset(); - + s = { type: type, url: url, @@ -454,128 +406,128 @@ jQuery.xhr = function( _native ) { 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; }, - + // 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 ]; } @@ -583,7 +535,7 @@ jQuery.xhr = function( _native ) { } return responseHeaders[ key.toLowerCase() ]; }, - + // Cancel the request abort: function(statusText) { if (internal) { @@ -592,304 +544,154 @@ jQuery.xhr = function( _native ) { 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 = sliceFunc.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 = sliceFunc.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 ) { +// Execute or select from functions in a given structure of options +function xhr_selectOrExecute( structure , s ) { var dataTypes = s.dataTypes, transportDataType, - transportsList, - transport, + list, + selected, i, length, checked = {}, - flag; + flag, + noSelect = structure !== "transports"; function initSearch( dataType ) { flag = transportDataType !== dataType && ! checked[ dataType ]; - + if ( flag ) { - + checked[ dataType ] = 1; transportDataType = dataType; - transportsList = s.transports[ dataType ]; + list = s[ structure ][ dataType ]; i = -1; - length = transportsList ? transportsList.length : 0 ; + length = list ? list.length : 0 ; } return flag; } - + initSearch( dataTypes[ 0 ] ); - for ( i = 0 ; ! transport && i <= length ; i++ ) { + for ( i = 0 ; ( noSelect || ! selected ) && i <= length ; i++ ) { if ( i === length ) { - + initSearch( "*" ); - + } else { - transport = transportsList[ i ]( s , determineDataType ); + selected = list[ 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 ) ) { + if ( typeof( selected ) === "string" && + initSearch( selected ) ) { - dataTypes.unshift( transport ); - transport = 0; + dataTypes.unshift( selected ); + selected = 0; } } } - return transport; + return noSelect ? jQuery.xhr : selected; } + +// Add an element to one of the xhr structures in ajaxSettings +function xhr_addElement( structure , args ) { + + var i, + j, + start = 0, + length = args.length, + dataTypes = [ "*" ], + dLength = 1, + dataType, + functors = [], + first, + append, + list; + + if ( length ) { + + first = jQuery.type( args[ 0 ] ); + + if ( first === "object" ) { + return xhr_selectOrExecute( structure , args[ 0 ] ); + } + + structure = jQuery.ajaxSettings[ structure ]; + + if ( first !== "function" ) { + + dataTypes = args[ 0 ].toLowerCase().split(/\s+/); + dLength = dataTypes.length; + start = 1; + + } + + if ( dLength && start < length ) { + + functors = sliceFunc.call( args , start ); + + length -= start; + + for( i = 0 ; i < dLength ; i++ ) { + + dataType = dataTypes[ i ]; + + first = /^\+/.test( dataType ); + + if (first) { + dataType = dataType.substr(1); + } + + if ( dataType !== "" ) { + + append = Array.prototype[ first ? "unshift" : "push" ]; + + list = structure[ dataType ] = structure[ dataType ] || []; + + for ( j = 0; j < length; j++ ) { + append.call( list , functors[ j ] ); + } + } + } + } + } + + return jQuery.xhr; +} + +// Install prefilter & transport methods +jQuery.each( [ "prefilter" , "transport" ] , function( _ , name ) { + _ = name + "s"; + jQuery.xhr[ name ] = function() { + return xhr_addElement( _ , arguments ); + }; +} ); // 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 === "*" ) { @@ -898,29 +700,29 @@ function determineDataType( s , ct , text , xml ) { 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 );