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