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 };