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 };/**
210  * @fileoverview
211  * Form
212  *
213  * For serializing a Form for use with Ajax.
214  *
215  * Disabled elements and elements with no name are not serialized.
216  * Select multiple and checkboxes or any form elements with the same name are supported.
217  * 
218  * @author Garrett Smith
219  */
220 APE.namespace("APE.form");
221 
222 /**
223  * @constructor
224  * Creates a Form wrapper that can be used to serialize the data 
225  * on submit.
226  * @param {HTMLFormElement} form the form to be used. A fieldset or other element that contains 
227  * form elements may also be passed in.
228  * 
229  */
230 APE.form.Form = function(form) {
231     this.form = form;
232 };
233 
234 APE.form.Form.prototype = {
235 
236     /**
237      * Serializes the form according to HTML 4.01
238      * http://www.w3.org/TR/html401/interact/forms.html#successful-controls
239      *
240      * @param {HTMLInputElement} [submit] input that needs to be included in 
241      * the successful controls.
242      * @return {Object} An object whose property names are element names and 
243      * values are arrays.
244      */
245     toObject : function(submit) {
246         var form = this.form,
247            elements = form.elements, i, len, 
248            target,
249            element, type, name, ontype = /^(?:rad|che)/,
250            json = {},
251            options,
252            option,
253            j, jlen,
254            p, plen,
255            par, sib, doc, node,
256             // Although no browsers actually include "image" 
257             // in elements collection.
258            submitTypeExp = /^(?:submit|image)$/;
259 
260         // If no elements property, it is not a form.
261         // Create a form and grab the elements off it.
262         if(!elements) {
263             node = form;
264             par = node.parentNode;
265             sib = node.nextSibling;
266             doc = node.ownerDocument||document;
267             form = doc.createElement('form');
268             form.appendChild(node);
269             elements = []; // IE 8 can't slice(elements).
270             len = form.elements.length;
271             for(i = 0; i < len; i++) 
272                 elements[i] = form.elements[i];
273             par.insertBefore(node, sib);
274         }
275 
276         len = elements.length;
277         for(i = 0; i < len; i++) {
278             element = elements[i];
279             type = element.type;
280             name = element.name;
281             
282             // Skip unsuccessful controls.
283             if(!name || element.disabled || type == "reset" 
284               || type == "button" || submitTypeExp.test(type) // only on event.
285               || (ontype.test(type) && !element.checked)
286               || (type == "object" && type.declared) 
287                   // IE can't get value from select-multiple when option value attr is empty.
288               || type == "select-multiple" && element.selectedIndex == -1) continue;
289           
290             p = json[name];
291 
292             if(!p) json[name] = p = [];
293             plen = p.length;
294 
295             options = element.options;
296             if(options) {
297                 if(type == "select-multiple") {
298                     for(j = 0, jlen = options.length; j < jlen; j++) {
299                         option = options[j];
300                         if(option.selected && !option.disabled) {
301                             p[plen++] = option.value || option.text;
302                         }
303                     }
304                 }
305                 else {
306                     option = options[element.selectedIndex];
307                     if(option && !option.disabled)
308                         p[plen] = option.value || option.text;
309                 }
310             }
311         
312           // https://bugzilla.mozilla.org/show_bug.cgi?id=371432
313           // http://www.w3.org/TR/file-upload/
314           //
315           // Just take the first file.
316             else if(type == "file") { 
317                 var file = element.files;
318                 file = file && file[0];
319                 if(file) {
320                     p[plen] = file;
321                 }
322             }
323             else {
324                 p[plen] = element.value;
325             }
326         }
327 
328         name = !!submit && submit.name;
329         if(name) {
330             if(!json[name]) p = json[name] = [];
331             p[plen++] = submit.value;
332         }
333         return json;
334     },
335 
336     /**
337      * @return {Array} array of strings for successful data.
338      * @param {HTMLInputElement} [submit] input that needs to be included.
339      */
340     getMultipartFormData : function(submit) {
341     // Doesn't encode data.
342     // http://groups.google.com/group/comp.lang.javascript/browse_thread/thread/eada69993b5ae08a/5943a32b5ecca6e6?lnk=gst&q=encodeURIComponent+post+xhr#5943a32b5ecca6e6
343     // http://www.devx.com/Java/Article/17679/1954
344     var json = this.toObject(submit), prop, value, file, i, len, result = [],
345         nn = '\r\n\r\n';
346     for(prop in json) {
347         value = json[prop];
348         for(i = 0, len = value.length; i < len; i++) {
349             file = value[i];
350             if(!file) continue;
351             result[result.length] = "Content-Disposition: form-data; " +
352               'name="'+prop +'";'
353               + (file.fileName && file.getAsBinary ? ' filename="'+file.fileName +'"'
354                 + nn + file.getAsBinary() : nn + file);
355             }
356         }
357         return result;
358     },
359 
360     /** 
361      * @return {String} the query string, including the "?".
362      * @param {HTMLInputElement} [submit] button that was clicked.
363      */
364     getQueryString : function(submit) {
365         var json = this.toObject(submit), prop, i, value, encodedProp, result = [], ws = /%20/g;
366         for(prop in json) {
367             value = json[prop];
368             encodedProp = encodeURIComponent(prop);
369             for(i = 0, len = value.length; i < len; i++) {
370                 result[result.length] = encodedProp +
371                   "=" + encodeURIComponent(value[i]).replace(ws,'+');
372             }
373         }
374         return result.join("&");
375     },
376 
377     /** 
378      * Builds a query string for GET or POST.
379      * @return {String} GET or POST data.
380      * @throws {Error} if the enctype is "multipart/form-data"
381      * @param {HTMLInputElement} [submit] input that needs to be included.
382      */
383     serialize : function(submit) {
384         var method = this.form.method.toLowerCase();
385         if(method == "get") {
386             return this.form.action + "?" + this.getQueryString(submit);  
387         }
388         if(method == "post") {
389             if(this.form.enctype == "multipart/form-data")
390                 throw Error("multipart/form-data: Use getMultipartFormData()");
391             return this.getQueryString(submit);
392         }
393     }
394 };