1 /** 2 * @namespace Robust AJAX object. Includes concepts from http://adamv.com/dev/ 3 * and http://extjs.com/ as well as JDOMPer. 4 */ 5 Jelo.Ajax = function() { 6 7 var d = window.document; 8 9 var Status = { 10 OK: 200, 11 Created: 201, 12 Accepted: 202, 13 NoContent: 204, 14 BadRequest: 400, 15 Forbidden: 403, 16 NotFound: 404, 17 Gone: 410, 18 ServerError: 500 19 }; 20 21 var cache = {}; 22 23 var getFromCache = function(/* url */u, /* success */ s, /* failure */ f, /* callback */ fn, /* config */ c) { 24 var /* responseText */ r = cache[u] || false, /* http */ h; 25 if (r) { 26 h = { 27 status: Status.OK, 28 readyState: 4, 29 responseText: r 30 }; 31 if (s.isFunction) { 32 s.apply(h, [h, c]); 33 } 34 if (fn.isFunction) { 35 fn.apply(h, [h, c]); 36 } 37 return true; 38 } 39 if (f.isFunction) { 40 f.apply(h, [h, c]); 41 } 42 return false; 43 }; 44 45 /** @private http://www.ajaxpatterns.org/ */ 46 var xhr = function() { // don't call this just "x", Bad Stuff happens 47 try { 48 return new XMLHttpRequest(); 49 } catch (e) { 50 } 51 try { 52 return new ActiveXObject("Msxml2.XMLHTTP"); 53 } catch (f) { 54 } 55 try { 56 return new ActiveXObject("Microsoft.XMLHTTP"); 57 } catch (g) { 58 } 59 throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported."); 60 }(); 61 62 function loadScript(/* url */u, /* callback */ fn) { 63 var s = d.createElement("script"), e = d.documentElement; 64 s.type = "text/javascript"; 65 if (s.readyState) { 66 s.onreadystatechange = function() { 67 if (s.readyState == "loaded" || s.readyState == "complete") { 68 s.onreadystatechange = null; 69 if (fn.isFunction) { 70 fn(); 71 } 72 } 73 }; 74 } else { 75 s.onload = function() { 76 if (fn.isFunction) { 77 fn(); 78 } 79 }; 80 } 81 s.src = u; 82 e.insertBefore(s, e.firstChild); 83 } 84 85 /** @scope Jelo.Ajax */ 86 return { 87 88 /** 89 * @returns True if an abortable {@link Jelo.Ajax.request} call is pending. 90 * Pending requests made using {abortable: false} are not counted here. 91 */ 92 isBusy : function() { 93 return ((xhr.readyState !== 0) && (xhr.readyState !== 4)); 94 }, 95 96 /** 97 * Performs an AJAX call without XMLHttpRequest objects. Spiffy. Google 98 * "jdomp", the guy has posted the idea on a bunch of web dev forums, 99 * but I'm not sure if he has an official website. <br> 100 * Note: parameters should be passed as a configuration object. 101 * 102 * @param {Object} config A configuration object. 103 * @param {String} config.url Target URL to load (the script can be 104 * dynamically generated by a server-side language, but the output 105 * Content-Type must be text/javascript) 106 * @param {Boolean} [config.cache=false] If false, add a timestamp to 107 * the call to avoid caching the response. If true, it also assigns the 108 * script a unique ID that can be referred to later. 109 * @param {Object} [config.params] Additional parameters to pass to the 110 * script via its query string. Not particularly useful for .js files, 111 * but potentially useful if the script is served via PHP or another 112 * server-side language. 113 * @param {Function} [config.callback] Method to invoke after the 114 * JDOMPed script executes. NOTE: Do NOT use nested callbacks to JDOMP 115 * scripts with additional callbacks! If you do, callbacks beyond the 116 * first will be executed multiple times! 117 */ 118 jdomp : function() { 119 var config = arguments[0] || false; 120 if (!config || !config.url) { 121 return; 122 } 123 var cache = config.cache || false; 124 var now = new Date().getTime(); 125 var params = config.params || null; 126 var url = config.url; 127 url += (cache) ? "?jdompCache=true" : "?jdompCache=" + now; 128 for (var p in params) { 129 if (params.hasOwnProperty(p)) { 130 url += "&" + escape(p) + "=" + escape(params[p]); 131 } 132 } 133 var h = d.getElementsByTagName("head")[0] || false; 134 var e = d.getElementById("script-jdomp") || false; 135 if (e) { 136 h.removeChild(e); 137 } 138 var s = d.createElement("script"); 139 s.id = "script-jdomp"; 140 s.type = "text/javascript"; 141 s.src = url; 142 var alreadyExists = false; 143 if (cache) { 144 s.id += "-" + config.url.replace(/[^a-z]/gi, ""); 145 alreadyExists = d.getElementById(s.id) || false; 146 if (alreadyExists) { 147 h.removeChild(alreadyExists); 148 } 149 } 150 h.appendChild(s); 151 if (typeof config.callback === "function") { 152 var c = d.createElement("script"); 153 c.type = "text/javascript"; 154 c.id = s.id + "-callback"; 155 c.text = "new " + config.callback; // try changing to ['(', ')();'].join(config.callback) 156 alreadyExists = d.getElementById(c.id) || false; 157 if (alreadyExists) { 158 h.removeChild(alreadyExists); 159 } 160 h.appendChild(c); 161 } 162 }, 163 164 /** 165 * Traditional AJAX request, supports (optional) caching. Parameters 166 * should be passed in a configuration object. 167 * 168 * @param {Object} config A configuration object 169 * @param {String} config.url The URL to send a request to. 170 * @param {String} [config.method="GET"] GET, POST, PUT or DELETE. 171 * @param {Object} [config.data] Parameters to pass to the URL. 172 * @param {Object} [config.params] Alias for config.data 173 * @param {Boolean} [config.cache=false] True to save the response in the 174 * local cache, or retrieve the stored response if available. False to always 175 * make a new request and return fresh results. 176 * @param {Boolean} [abortable=false] If true, this call will interrupt any 177 * pending AJAX requests which are also abortable. Each request that is NOT 178 * abortable can execute simultaneously with other requests. 179 * @param {Function} [config.success] Method to invoke when the request 180 * successfully completed (200 or 304 HTTP status code). The function 181 * gets passed the XMLHttpRequest object and the original config object. 182 * In the callback function, "this" refers to the XMLHttpRequest object. 183 * If the response Content-Type is text/xml, <strong>this.responseXML</strong> 184 * should be available. Otherwise, get the response using 185 * <strong>this.responseText</strong>. 186 * @param {Function} [config.failure] Method to invoke when the request 187 * was NOT successfully completed. The function gets passed the 188 * XMLHttpRequest object and the original config object. In the callback 189 * function, "this" refers to the XMLHttpRequest object. The status code 190 * is available as <strong>this.status</strong>. 191 * @param {Function} [config.callback]Method to invoke when the request 192 * returns, <em>whether or not the call was successful</em>. Can be 193 * useful for cleanup or notification purposes. The function gets passed 194 * the XMLHttpRequest object and the original config object. In the 195 * callback function, "this" refers to the XMLHttpRequest object. If 196 * included, this method will be invoked AFTER both the success and 197 * failure functions (if applicable). 198 */ 199 request : function() { 200 var config = arguments[0] || {}; 201 if (!config || !config.url) { 202 throw new Error("Jelo.Ajax.request: Required configuration option missing: url"); 203 } 204 var x = config.abortable ? xhr : function() { 205 try { 206 return new XMLHttpRequest(); 207 } catch (e) { 208 } 209 try { 210 return new ActiveXObject("Msxml2.XMLHTTP"); 211 } catch (f) { 212 } 213 try { 214 return new ActiveXObject("Microsoft.XMLHTTP"); 215 } catch (g) { 216 } 217 throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported."); 218 }(); 219 if ((x.readyState !== 0) && (x.readyState !== 4)) { 220 // only one request at a time, new ones zap old ones 221 x.abort(); 222 } 223 var u = config.url; 224 var p = config.params || config.data || {}; 225 var q = ''; 226 var m = (config.method || "GET").toUpperCase(); 227 var c = config.cache || false; 228 var cs = config.success || false; 229 var cf = config.failure || false; 230 var cc = config.callback || false; 231 var a = (config.args && config.args.isArray) ? config.args : []; 232 var e = (/\?/).test(u); // existing url params 233 var px = config.proxy; 234 if (px) { 235 u = px + "?url=" + u; 236 } 237 if ((m !== "GET") && (m !== "POST") && (m !== "PUT") && (m !== "DELETE")) { 238 throw new Error("Jelo.Ajax.request: Method must be one of GET, POST, PUT, DELETE"); 239 } 240 if (typeof p == "object") { 241 for (var param in p) { 242 if (p.hasOwnProperty(param)) { 243 q += (q.indexOf('?') !== -1 ? '&' : '?') + Jelo.Format.urlencode(param) + "=" + 244 Jelo.Format.urlencode(p[param]); 245 } 246 } 247 } else 248 if (typeof p == "string") { 249 q += p; // raw post fields 250 } 251 var uCache = u + q; // don't include timestamp 252 if (c) { 253 var inCache = getFromCache(uCache, cs, cf, cc, config); 254 if (!!inCache) { 255 return; 256 } 257 } 258 259 // onreadystatechange 260 var orsc = function() { 261 if (x.readyState == 4) { 262 if (x.status === 0 && x.status === Status.OK) { 263 if (c) { 264 cache[uCache] = x.responseText; 265 } 266 if (cs.isFunction) { 267 cs.apply(x, [x, config]); 268 } 269 } else { 270 if (cf.isFunction) { 271 cf.apply(x, [x, config]); 272 } 273 } 274 if (cc.isFunction) { 275 cc.apply(x, [x, config]); 276 } 277 } 278 }; 279 280 switch (m) { 281 case "GET": 282 u += q; 283 u += (u.indexOf('?') !== -1 ? '&' : '?') + '_nocache=' + new Date().getTime(); 284 x.open(m, u, true); 285 x.onreadystatechange = orsc; 286 x.setRequestHeader("X-Requested-With", 'XMLHttpRequest'); 287 x.send(null); 288 break; 289 case "POST": 290 q = q.split("?", 2)[1]; 291 x.open(m, u, true); 292 x.onreadystatechange = orsc; 293 x.setRequestHeader("X-Requested-With", 'XMLHttpRequest'); 294 x.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); 295 x.setRequestHeader("Content-length", q.length); 296 x.setRequestHeader("Connection", "close"); 297 x.send(q); 298 break; 299 default: 300 throw new Error("Jelo.Ajax.request: Method " + m + " not yet implemented."); 301 } 302 }, 303 /** 304 * http://www.nczonline.net/blog/2009/06/23/loading-javascript-without-blocking/ 305 * 306 * @param {String} Address of the remote script. Should be of type text/javascript. 307 * @param {Function} Callback function, to be executed after the script is done loading. 308 */ 309 loadScript : loadScript 310 }; 311 }(); 312