Added deferred to core. Used internally for DOM readyness and ajax callbacks.
[jquery.git] / src / xhr.js
1 (function( jQuery ) {
2
3 var rquery_xhr = /\?/,
4         rhash = /#.*$/,
5         rheaders = /^(.*?):\s*(.*?)\r?$/mg, // IE leaves an \r character at EOL
6         rnoContent = /^(?:GET|HEAD)$/,
7         rts = /([?&])_=[^&]*/,
8         rurl = /^(\w+:)?\/\/([^\/?#]+)/,
9
10         sliceFunc = Array.prototype.slice,
11
12         isFunction = jQuery.isFunction;
13
14 // Creates a jQuery xhr object
15 jQuery.xhr = function( _native ) {
16
17         if ( _native ) {
18                 return jQuery.ajaxSettings.xhr();
19         }
20         
21         function reset( force ) {
22                 
23                 // We only need to reset if we went through the init phase
24                 // (with the exception of object creation)
25                 if ( force || internal ) {
26                         
27                         // Reset callbacks lists
28                         deferred = jQuery.deferred();
29                         completeDeferred = jQuery._deferred();
30                         
31                         xhr.success = xhr.then = deferred.then;
32                         xhr.error = xhr.fail = deferred.fail;
33                         xhr.complete = completeDeferred.then;
34                         
35                         // Reset private variables
36                         requestHeaders = {};
37                         responseHeadersString = responseHeaders = internal = done = timeoutTimer = s = undefined;
38
39                         // Reset state
40                         xhr.readyState = 0;
41                         sendFlag = 0;
42
43                         // Remove responseX fields
44                         for ( var name in xhr ) {
45                                 if ( /^response/.test(name) ) {
46                                         delete xhr[name];
47                                 }
48                         }
49                 }
50         }
51
52         function init() {
53
54                 var // Options extraction
55
56                         // Remove hash character (#7531: first for string promotion)
57                         url = s.url = ( "" + s.url ).replace( rhash , "" ),
58
59                         // Uppercase the type
60                         type = s.type = s.type.toUpperCase(),
61
62                         // Determine if request has content
63                         hasContent = s.hasContent = ! rnoContent.test( type ),
64
65                         // Extract dataTypes list
66                         dataType = s.dataType,
67                         dataTypes = s.dataTypes = dataType ? jQuery.trim(dataType).toLowerCase().split(/\s+/) : ["*"],
68
69                         // Determine if a cross-domain request is in order
70                         parts = rurl.exec( url.toLowerCase() ),
71                         loc = location,
72                         crossDomain = s.crossDomain = !!( parts && ( parts[1] && parts[1] != loc.protocol || parts[2] != loc.host ) ),
73
74                         // Get other options locally
75                         data = s.data,
76                         originalContentType = s.contentType,
77                         prefilters = s.prefilters,
78                         accepts = s.accepts,
79                         headers = s.headers,
80
81                         // Other Variables
82                         transportDataType,
83                         i;
84
85                 // Convert data if not already a string
86                 if ( data && s.processData && typeof data != "string" ) {
87                         data = s.data = jQuery.param( data , s.traditional );
88                 }
89
90                 // Apply option prefilters
91                 for ( i = 0; i < prefilters.length; i++ ) {
92                         prefilters[i](s);
93                 }
94
95                 // Get internal
96                 internal = selectTransport( s );
97
98                 // Re-actualize url & data
99                 url = s.url;
100                 data = s.data;
101
102                 // If internal was found
103                 if ( internal ) {
104
105                         // Get transportDataType
106                         transportDataType = dataTypes[0];
107
108                         // More options handling for requests with no content
109                         if ( ! hasContent ) {
110
111                                 // If data is available, append data to url
112                                 if ( data ) {
113                                         url += (rquery_xhr.test(url) ? "&" : "?") + data;
114                                 }
115
116                                 // Add anti-cache in url if needed
117                                 if ( s.cache === false ) {
118
119                                         var ts = jQuery.now(),
120                                                 // try replacing _= if it is there
121                                                 ret = url.replace(rts, "$1_=" + ts );
122
123                                         // if nothing was replaced, add timestamp to the end
124                                         url = ret + ((ret == url) ? (rquery_xhr.test(url) ? "&" : "?") + "_=" + ts : "");
125                                 }
126
127                                 s.url = url;
128                         }
129
130                         // Set the correct header, if data is being sent
131                         if ( ( data && hasContent ) || originalContentType ) {
132                                 requestHeaders["content-type"] = s.contentType;
133                         }
134
135                         // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
136                         if ( s.ifModified ) {
137                                 if ( jQuery_lastModified[url] ) {
138                                         requestHeaders["if-modified-since"] = jQuery_lastModified[url];
139                                 }
140                                 if ( jQuery_etag[url] ) {
141                                         requestHeaders["if-none-match"] = jQuery_etag[url];
142                                 }
143                         }
144
145                         // Set the Accepts header for the server, depending on the dataType
146                         requestHeaders.accept = transportDataType && accepts[ transportDataType ] ?
147                                 accepts[ transportDataType ] + ( transportDataType !== "*" ? ", */*; q=0.01" : "" ) :
148                                 accepts[ "*" ];
149
150                         // Check for headers option
151                         for ( i in headers ) {
152                                 requestHeaders[ i.toLowerCase() ] = headers[ i ];
153                         }
154                 }
155
156                 callbackContext = s.context || s;
157                 globalEventContext = s.context ? jQuery(s.context) : jQuery.event;
158                 
159                 for ( i in { success:1, error:1, complete:1 } ) {
160                         xhr[ i ]( s[ i ] );
161                 }
162
163                 // Watch for a new set of requests
164                 if ( s.global && jQuery.active++ === 0 ) {
165                         jQuery.event.trigger( "ajaxStart" );
166                 }
167
168                 done = whenDone;
169         }
170
171         function whenDone(status, statusText, response, headers) {
172
173                 // Called once
174                 done = undefined;
175
176                 // Reset sendFlag
177                 sendFlag = 0;
178
179                 // Cache response headers
180                 responseHeadersString = headers || "";
181
182                 // Clear timeout if it exists
183                 if ( timeoutTimer ) {
184                         clearTimeout(timeoutTimer);
185                 }
186
187                 var // Reference url
188                         url = s.url,
189                         // and ifModified status
190                         ifModified = s.ifModified,
191
192                         // Is it a success?
193                         isSuccess = 0,
194                         // Stored success
195                         success,
196                         // Stored error
197                         error = statusText;
198
199                 // If not timeout, force a jQuery-compliant status text
200                 if ( statusText != "timeout" ) {
201                         statusText = ( status >= 200 && status < 300 ) ?
202                                 "success" :
203                                 ( status === 304 ? "notmodified" : "error" );
204                 }
205
206                 // If successful, handle type chaining
207                 if ( statusText === "success" || statusText === "notmodified" ) {
208
209                         // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
210                         if ( ifModified ) {
211                                 var lastModified = xhr.getResponseHeader("Last-Modified"),
212                                         etag = xhr.getResponseHeader("Etag");
213
214                                 if (lastModified) {
215                                         jQuery_lastModified[url] = lastModified;
216                                 }
217                                 if (etag) {
218                                         jQuery_etag[url] = etag;
219                                 }
220                         }
221
222                         if ( ifModified && statusText === "notmodified" ) {
223
224                                 success = null;
225                                 isSuccess = 1;
226
227                         } else {
228                                 // Chain data conversions and determine the final value
229                                 // (if an exception is thrown in the process, it'll be notified as an error)
230                                 try {
231
232                                         function checkData(data) {
233                                                 if ( data !== undefined ) {
234                                                         var testFunction = s.dataCheckers[srcDataType];
235                                                         if ( isFunction( testFunction ) ) {
236                                                                 testFunction(data);
237                                                         }
238                                                 }
239                                         }
240
241                                         function convertData (data) {
242                                                 var conversionFunction = dataConverters[srcDataType+" => "+destDataType] ||
243                                                                 dataConverters["* => "+destDataType],
244                                                         noFunction = ! isFunction( conversionFunction );
245                                                 if ( noFunction ) {
246                                                         if ( srcDataType != "text" && destDataType != "text" ) {
247                                                                 // We try to put text inbetween
248                                                                 var first = dataConverters[srcDataType+" => text"] ||
249                                                                                 dataConverters["* => text"],
250                                                                         second = dataConverters["text => "+destDataType] ||
251                                                                                 dataConverters["* => "+destDataType],
252                                                                         areFunctions = isFunction( first ) && isFunction( second );
253                                                                 if ( areFunctions ) {
254                                                                         conversionFunction = function (data) {
255                                                                                 return second( first ( data ) );
256                                                                         };
257                                                                 }
258                                                                 noFunction = ! areFunctions;
259                                                         }
260                                                         if ( noFunction ) {
261                                                                 jQuery.error( "no data converter between " + srcDataType + " and " + destDataType );
262                                                         }
263
264                                                 }
265                                                 return conversionFunction(data);
266                                         }
267
268                                         var dataTypes = s.dataTypes,
269                                                 i,
270                                                 length,
271                                                 data = response,
272                                                 dataConverters = s.dataConverters,
273                                                 srcDataType,
274                                                 destDataType,
275                                                 responseTypes = s.xhrResponseFields;
276
277                                         for ( i = 0, length = dataTypes.length ; i < length ; i++ ) {
278
279                                                 destDataType = dataTypes[i];
280
281                                                 if ( !srcDataType ) { // First time
282
283                                                         // Copy type
284                                                         srcDataType = destDataType;
285                                                         // Check
286                                                         checkData(data);
287                                                         // Apply dataFilter
288                                                         if ( isFunction( s.dataFilter ) ) {
289                                                                 data = s.dataFilter(data, s.dataType);
290                                                                 // Recheck data
291                                                                 checkData(data);
292                                                         }
293
294                                                 } else { // Subsequent times
295
296                                                         // handle auto
297                                                         // JULIAN: for reasons unknown to me === doesn't work here
298                                                         if (destDataType == "*") {
299
300                                                                 destDataType = srcDataType;
301
302                                                         } else if ( srcDataType != destDataType ) {
303
304                                                                 // Convert
305                                                                 data = convertData(data);
306                                                                 // Copy type & check
307                                                                 srcDataType = destDataType;
308                                                                 checkData(data);
309
310                                                         }
311
312                                                 }
313
314                                                 // Copy response into the xhr if it hasn't been already
315                                                 var responseDataType,
316                                                         responseType = responseTypes[srcDataType];
317
318                                                 if ( responseType ) {
319
320                                                         responseDataType = srcDataType;
321
322                                                 } else {
323
324                                                         responseType = responseTypes[ responseDataType = "text" ];
325
326                                                 }
327
328                                                 if ( responseType !== 1 ) {
329                                                         xhr[ "response" + responseType ] = data;
330                                                         responseTypes[ responseType ] = 1;
331                                                 }
332
333                                         }
334
335                                         // We have a real success
336                                         success = data;
337                                         isSuccess = 1;
338
339                                 } catch(e) {
340
341                                         statusText = "parsererror";
342                                         error = "" + e;
343
344                                 }
345                         }
346
347                 } else { // if not success, mark it as an error
348
349                                 error = error || statusText;
350
351                 }
352
353                 // Set data for the fake xhr object
354                 xhr.status = status;
355                 xhr.statusText = statusText;
356
357                 // Keep local copies of vars in case callbacks re-use the xhr
358                 var _s = s,
359                         _deferred = deferred,
360                         _completeDeferred = completeDeferred,
361                         _callbackContext = callbackContext,
362                         _globalEventContext = globalEventContext;
363                         
364                         
365                 // Set state if the xhr hasn't been re-used
366                 function _setState( value ) {
367                         if ( xhr.readyState && s === _s ) {
368                                 setState( value );
369                         }
370                 }
371
372                 // Really completed?
373                 if ( status && s.async ) {
374                         setState( 2 );
375                         _setState( 3 );
376                 }
377
378                 // We're done
379                 _setState( 4 );
380                 
381                 // Success/Error
382                 if ( isSuccess ) {
383                         _deferred.fire( _callbackContext , [ success , statusText , xhr ] );
384                 } else {
385                         _deferred.fireReject( _callbackContext , [ xhr , statusText , error ] );
386                 }
387                 
388                 if ( _s.global ) {
389                         _globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ) , [ xhr , _s , isSuccess ? success : error ] );
390                 }
391                 
392                 // Complete
393                 _completeDeferred.fire( _callbackContext, [ xhr , statusText ] );
394                 
395                 if ( _s.global ) {
396                         _globalEventContext.trigger( "ajaxComplete", [xhr, _s] );
397                         // Handle the global AJAX counter
398                         if ( ! --jQuery.active ) {
399                                 jQuery.event.trigger( "ajaxStop" );
400                         }
401                 }
402         }
403
404         // Ready state control
405         function checkState( expected , test ) {
406                 if ( expected !== true && ( expected === false || test === false || xhr.readyState !== expected ) ) {
407                         jQuery.error("INVALID_STATE_ERR");
408                 }
409         }
410
411         // Ready state change
412         function setState( value ) {
413                 xhr.readyState = value;
414                 if ( isFunction( xhr.onreadystatechange ) ) {
415                         xhr.onreadystatechange();
416                 }
417         }
418
419         var // jQuery lists
420                 jQuery_lastModified = jQuery.lastModified,
421                 jQuery_etag = jQuery.etag,
422                 // Options object
423                 s,
424                 // Callback stuff
425                 callbackContext,
426                 globalEventContext,
427                 deferred,
428                 completeDeferred,
429                 // Headers (they are sent all at once)
430                 requestHeaders,
431                 // Response headers
432                 responseHeadersString,
433                 responseHeaders,
434                 // Done callback
435                 done,
436                 // transport
437                 internal,
438                 // timeout handle
439                 timeoutTimer,
440                 // The send flag
441                 sendFlag,
442                 // Fake xhr
443                 xhr = {
444                         // state
445                         readyState: 0,
446
447                         // Callback
448                         onreadystatechange: null,
449
450                         // Open
451                         open: function(type, url, async, username, password) {
452
453                                 xhr.abort();
454                                 reset();
455
456                                 s = {
457                                         type: type,
458                                         url: url,
459                                         async: async,
460                                         username: username,
461                                         password: password
462                                 };
463
464                                 setState(1);
465
466                                 return xhr;
467                         },
468
469                         // Send
470                         send: function(data, moreOptions) {
471
472                                 checkState(1 , !sendFlag);
473
474                                 s.data = data;
475
476                                 s = jQuery.extend( true,
477                                         {},
478                                         jQuery.ajaxSettings,
479                                         s,
480                                         moreOptions || ( moreOptions === false ? { global: false } : {} ) );
481
482                                 if ( moreOptions ) {
483                                         // We force the original context
484                                         // (plain objects used as context get extended)
485                                         s.context = moreOptions.context;
486                                 }
487
488                                 init();
489
490                                 // If not internal, abort
491                                 if ( ! internal ) {
492                                         done( 0 , "transport not found" );
493                                         return false;
494                                 }
495
496                                 // Allow custom headers/mimetypes and early abort
497                                 if ( s.beforeSend ) {
498
499                                         var _s = s;
500
501                                         if ( s.beforeSend.call(callbackContext, xhr, s) === false || ! xhr.readyState || _s !== s ) {
502
503                                                 // Abort if not done
504                                                 if ( xhr.readyState && _s === s ) {
505                                                         xhr.abort();
506                                                 }
507
508                                                 // Handle the global AJAX counter
509                                                 if ( _s.global && ! --jQuery.active ) {
510                                                         jQuery.event.trigger( "ajaxStop" );
511                                                 }
512
513                                                 return false;
514                                         }
515                                 }
516
517                                 sendFlag = 1;
518
519                                 // Send global event
520                                 if ( s.global ) {
521                                         globalEventContext.trigger("ajaxSend", [xhr, s]);
522                                 }
523
524                                 // Timeout
525                                 if ( s.async && s.timeout > 0 ) {
526                                         timeoutTimer = setTimeout(function(){
527                                                 xhr.abort("timeout");
528                                         }, s.timeout);
529                                 }
530
531                                 if ( s.async ) {
532                                         setState(1);
533                                 }
534
535                                 try {
536
537                                         internal.send(requestHeaders, done);
538                                         return xhr;
539
540                                 } catch (e) {
541
542                                         if ( done ) {
543
544                                                 done(0, "error", "" + e);
545
546                                         } else {
547
548                                                 jQuery.error(e);
549
550                                         }
551                                 }
552
553                                 return false;
554                         },
555
556                         // Caches the header
557                         setRequestHeader: function(name,value) {
558                                 checkState(1, !sendFlag);
559                                 requestHeaders[ name.toLowerCase() ] = value;
560                                 return xhr;
561                         },
562
563                         // Raw string
564                         getAllResponseHeaders: function() {
565                                 return xhr.readyState <= 1 ? "" : responseHeadersString;
566                         },
567
568                         // Builds headers hashtable if needed
569                         getResponseHeader: function( key ) {
570
571                                 if ( xhr.readyState <= 1 ) {
572
573                                         return null;
574
575                                 }
576
577                                 if ( responseHeaders === undefined ) {
578
579                                         responseHeaders = {};
580
581                                         if ( typeof responseHeadersString === "string" ) {
582
583                                                 var match;
584
585                                                 while( ( match = rheaders.exec( responseHeadersString ) ) ) {
586                                                         responseHeaders[ match[ 1 ].toLowerCase() ] = match[ 2 ];
587                                                 }
588                                         }
589                                 }
590                                 return responseHeaders[ key.toLowerCase() ];
591                         },
592
593                         // Cancel the request
594                         abort: function(statusText) {
595                                 if (internal) {
596                                         internal.abort( statusText || "abort" );
597                                 }
598                                 xhr.readyState = 0;
599                         }
600                 };
601
602         // Init data (so that we can bind callbacks early
603         reset(1);
604
605         // Return the xhr emulation
606         return xhr;
607 };
608
609 jQuery.extend(jQuery.xhr, {
610
611         // Add new prefilter
612         prefilter: function (functor) {
613                 if ( isFunction(functor) ) {
614                         jQuery.ajaxSettings.prefilters.push( functor );
615                 }
616                 return this;
617         },
618
619         // Bind a transport to one or more dataTypes
620         bindTransport: function () {
621
622                 var args = arguments,
623                         i,
624                         start = 0,
625                         length = args.length,
626                         dataTypes = [ "*" ],
627                         functors = [],
628                         functor,
629                         first,
630                         append,
631                         list,
632                         transports = jQuery.ajaxSettings.transports;
633
634                 if ( length ) {
635
636                         if ( ! isFunction( args[ 0 ] ) ) {
637
638                                 dataTypes = args[ 0 ].toLowerCase().split(/\s+/);
639                                 start = 1;
640
641                         }
642
643                         if ( dataTypes.length && start < length ) {
644
645                                 for ( i = start; i < length; i++ ) {
646                                         functor = args[i];
647                                         if ( isFunction(functor) ) {
648                                                 functors.push( functor );
649                                         }
650                                 }
651
652                                 if ( functors.length ) {
653
654                                         jQuery.each ( dataTypes, function( _ , dataType ) {
655
656                                                 first = /^\+/.test( dataType );
657
658                                                 if (first) {
659                                                         dataType = dataType.substr(1);
660                                                 }
661
662                                                 if ( dataType !== "" ) {
663
664                                                         append = Array.prototype[ first ? "unshift" : "push" ];
665
666                                                         list = transports[ dataType ];
667
668                                                         jQuery.each ( functors, function( _ , functor ) {
669
670                                                                 if ( ! list ) {
671
672                                                                         list = transports[ dataType ] = [ functor ];
673
674                                                                 } else {
675
676                                                                         append.call( list , functor );
677                                                                 }
678                                                         } );
679                                                 }
680
681                                         } );
682                                 }
683                         }
684                 }
685
686                 return this;
687         }
688
689
690 });
691
692 // Select a transport given options
693 function selectTransport( s ) {
694
695         var dataTypes = s.dataTypes,
696                 transportDataType,
697                 transportsList,
698                 transport,
699                 i,
700                 length,
701                 checked = {},
702                 flag;
703
704         function initSearch( dataType ) {
705
706                 flag = transportDataType !== dataType && ! checked[ dataType ];
707
708                 if ( flag ) {
709
710                         checked[ dataType ] = 1;
711                         transportDataType = dataType;
712                         transportsList = s.transports[ dataType ];
713                         i = -1;
714                         length = transportsList ? transportsList.length : 0 ;
715                 }
716
717                 return flag;
718         }
719
720         initSearch( dataTypes[ 0 ] );
721
722         for ( i = 0 ; ! transport && i <= length ; i++ ) {
723
724                 if ( i === length ) {
725
726                         initSearch( "*" );
727
728                 } else {
729
730                         transport = transportsList[ i ]( s , determineDataType );
731
732                         // If we got redirected to another dataType
733                         // Search there (if not in progress or already tried)
734                         if ( typeof( transport ) === "string" &&
735                                 initSearch( transport ) ) {
736
737                                 dataTypes.unshift( transport );
738                                 transport = 0;
739                         }
740                 }
741         }
742
743         return transport;
744 }
745
746 // Utility function that handles dataType when response is received
747 // (for those transports that can give text or xml responses)
748 function determineDataType( s , ct , text , xml ) {
749
750         var autoDataType = s.autoDataType,
751                 type,
752                 regexp,
753                 dataTypes = s.dataTypes,
754                 transportDataType = dataTypes[0],
755                 response;
756
757         // Auto (xml, json, script or text determined given headers)
758         if ( transportDataType === "*" ) {
759
760                 for ( type in autoDataType ) {
761                         if ( ( regexp = autoDataType[ type ] ) && regexp.test( ct ) ) {
762                                 transportDataType = dataTypes[0] = type;
763                                 break;
764                         }
765                 }
766         }
767
768         // xml and parsed as such
769         if ( transportDataType === "xml" &&
770                 xml &&
771                 xml.documentElement /* #4958 */ ) {
772
773                 response = xml;
774
775         // Text response was provided
776         } else {
777
778                 response = text;
779
780                 // If it's not really text, defer to dataConverters
781                 if ( transportDataType !== "text" ) {
782                         dataTypes.unshift( "text" );
783                 }
784
785         }
786
787         return response;
788 }
789
790 })( jQuery );