1 /** ajax.AsyncRequest is an XHR Adapter that fires these events: 
  2  *  onsucceed, onfail, onabort, oncomplete
  3  * 
  4  * @author Garrett Smith
  5  *
  6  * Usage: 
  7  *   var req = APE.ajax.AsyncRequest("data.json");
  8  *   req.onsucceed = function( req ) { 
  9  *     alert( req.responseText ); 
 10  *   };
 11  *   req.send();
 12  *
 13  * This file has no dependencies.
 14  * Assign multiple callbacks using EventPublisher, if desired.
 15  */
 16 
 17  /**
 18   * TODO: 
 19   * queue
 20   *
 21   */
 22 
 23 APE.namespace("APE.ajax");
 24 
 25 /** 
 26  * @constructor
 27  * @throws {URIError} if <code>formConfig.action</code> is undefined.
 28  * @param {Object} formConfig like:
 29  * <pre> {
 30  *   action : "foo.jsp",
 31  *   enctype : "multipart/form-data",
 32  *   method : "post"
 33  * }</pre>
 34  * In most cases, passing in a <code>FORM</code> will work.
 35  */
 36 APE.ajax.AsyncRequest = function(formConfig) {
 37     this.httpMethod = formConfig.method && formConfig.method.toLowerCase()||"get";
 38     this.uri = formConfig.action;
 39     if(!this.uri) throw URIError("formConfig.action = undefined");
 40     this.enctype = formConfig.enctype;
 41     if(!this.enctype && this.httpMethod == "post") {
 42         this.enctype = 'application/x-www-form-urlencoded';
 43     }
 44     if(window.XMLHttpRequest) {
 45         this.req = new XMLHttpRequest();
 46     }
 47     else if(window.ActiveXObject) {
 48         this.req = new ActiveXObject('Microsoft.XMLHTTP');
 49     }
 50 };
 51 
 52 APE.ajax.AsyncRequest.toString = function() {
 53     return"[object ajax.AsyncRequest]";
 54 };
 55 
 56 APE.ajax.AsyncRequest.prototype = { 
 57     
 58     /**@event fires before oncomplete() */
 59     onabort : function(){},
 60 
 61     /**@event fires before onsucceed() */
 62     oncomplete : function(){},
 63 
 64     /**@event*/
 65     onsucceed : function(){},
 66 
 67     /**@event oncomplete fires before onfail() */
 68     onfail : function(){},
 69 
 70     /**@event*/
 71     ontimeout : function(){},
 72 
 73     /**@type {uint}*/
 74     timeoutMillis : 0,
 75     
 76     /** Sends the call.
 77      * @param {String|Array} [data] post data. If an array, it is assumed that the 
 78      * request is an unencoded, multipart/form-data. The array is joined on a boundary.
 79      * @return {ajax.AsyncRequest} The AsyncRequest wrapper object is returned.
 80      */
 81     send : function( data, timeoutMillis ) {
 82         if(typeof timeoutMillis == "number") {
 83             this.timeoutMillis = timeoutMillis;
 84         }
 85 
 86         this._setUpReadyStateChangeHandler();
 87         this.req.open(this.httpMethod, this.uri, true);
 88         if(this.req.setRequestHeader) {
 89             this.req.setRequestHeader('X-REQUESTED-WITH', 'XMLHttpRequest');
 90             if(this.httpMethod == "post") {
 91                 if(typeof data == "string") {
 92                     this.req.setRequestHeader('Content-Type', this.enctype);
 93                 }
 94                 else if(data && typeof data.unshift == "function" && this.enctype == "multipart/form-data") {
 95                     var boundary = "DATA"+(new Date-0),
 96                        n = '\r\n';
 97                        this.req.setRequestHeader('Content-Type', 
 98                          this.enctype + "; boundary=" + boundary);
 99                        boundary = n + "--" + boundary;
100                        data = boundary + n + data.join(boundary+n) + boundary+'--'+n + n;
101 
102                 }
103             }
104         }
105         try {
106             this.req.send( data||null );
107             return this; // internet explorer does not support |finally| properly.
108         }
109         catch(ex) {
110             return this;
111         }
112     },
113     
114     /** Aborts call. Fires "onabort", passing the request,
115      * then fires "oncomplete" with {successful : false}
116      */
117     abort : function() {
118         this.req.abort();
119 
120         // cancel the readyState poll.
121         APE.ajax.AsyncRequest._cancelPoll(this._pollId);
122 
123         // Clear the timeout timer.
124         window.clearInterval(this.timeoutID);
125 
126         this.onabort(this.req); // others can know.
127         this.oncomplete({successful : false});
128     },
129     
130     toString : function() {
131         var s = "ajax.AsyncRequest: \n" 
132             + "uri: " + this.uri
133             + "\nhttpMethod: " + this.httpMethod
134             + "\n----------------------\n"
135             + "req: \n",
136             prop;
137         for(prop in this.req)
138             try {
139                 if(typeof this.req[prop] == "string") {
140                     s.concat(prop + ": " + this.req[prop] + "\n");
141                 }
142             } catch(mozillaSecurityError) { }
143         return s;
144     },
145 
146     /** sets up poll for readyState change.
147      * fires 'oncomplete', followed by either 'onsucceed' or 'onfail'.
148      * onsucceed passes the request,
149      * onfail passes the request.
150      * @private for internal use only.
151      */
152     _setUpReadyStateChangeHandler : function() {
153         var asyncRequest = this;
154         this._pollId = window.setInterval( readyStateChangePoll, 50 );
155         if(this.timeoutMillis > 0) {
156 
157             var userTimeout = function() {
158                 APE.ajax.AsyncRequest._cancelPoll(asyncRequest._pollId);
159                 asyncRequest.ontimeout(/* Should we pass anything here? */);
160             };
161             this.timeoutID = setTimeout( userTimeout, this.timeoutMillis );
162         }
163 
164         /** Called repeatedly until readyState i== 4, then calls processResponse,
165          * @private.
166          */
167         function readyStateChangePoll() {
168             if( asyncRequest.req.readyState == 4 ) {
169                 processResponse();
170             }
171         }
172 
173         /** 
174          * processes a response after readyState == 4.
175          * @private
176          */
177         function processResponse() {
178             APE.ajax.AsyncRequest._cancelPoll( asyncRequest._pollId );
179             var httpStatus = asyncRequest.req.status;
180 
181             var succeeded = httpStatus >= 200 && httpStatus < 300 || httpStatus == 304 || httpStatus == 1223;
182 
183             // if the request was successful,
184             if(succeeded) {
185                 // fire oncomplete, then onsucceed.
186                 asyncRequest.oncomplete({successful:true});
187                 asyncRequest.onsucceed(asyncRequest.req);
188             }
189             else {
190                 // fire oncomplete, then onfail.
191                 asyncRequest.oncomplete({successful:false});
192                 asyncRequest.onfail(asyncRequest.req);
193             }
194             // The call is complete, cancel the timeout..
195             clearInterval(asyncRequest.timeoutID);
196         }
197     }
198 };
199 
200 /** 
201  * cancels the readyState poll.
202  * @private
203  * setTimeout calling object's context is always window, and 
204  * this is needed by abort.
205  * 
206  */
207 APE.ajax.AsyncRequest._cancelPoll = function(pollId) {
208     window.clearInterval( pollId );
209 };