1 /**
  2  * @fileOverview AJAX Functions & Classes
  3  * @author <a href="http://www.bobs-bits.com">Stephen Reay</a>
  4  */
  5 
  6 /**
  7  * @namespace AJAX functionality
  8  */
  9 BTM.AJAX = {};
 10 
 11 /**
 12  * @class AJAX Request class
 13  * @param {String} url the URL to request
 14  * @param {Function|Function[]} callbacks Function(s) to call, either a single function to call on "Success", or an object containing Functions with keys representing the HTTP codes/status names and/or AJAX pseudo event names.
 15  * If a HTTP status code is provided, the generic handler for the corresponding status type will not run.
 16  * @param {Function} [callbacks.onBeforeStart] Function to be called when the AJAX request is created (before the internal call to the open() Method)
 17  * @param {Function} [callbacks.onLoading] Function to be called when the AJAX request is started (after the internal call to the open() Method)
 18  * @param {Function} [callbacks.onLoaded] Function to be when the AJAX request has been sent, but before the response starts to download (after the internal call to the send() Method)
 19  * @param {Function} [callbacks.onInteractive] Function to be called when the AJAX request starts to download the response.
 20  * @param {Function} [callbacks.onComplete] Function to be called when the AJAX request has completed (after HTTP-status specific callbacks)
 21  * @param {Function} [callbacks.onSuccess] Function to be called when the AJAX request is sucessful (i.e. any of the HTTP 2xx codes)
 22  * @param {Function} [callbacks.onError] Function to be called when the AJAX request fails (i.e. any of the HTTP 4xx or 5xx codes)
 23  * @param {Object} [options] the options Object
 24  * @param {String} [options.method='POST'] the HTTP method to use for the request, either 'POST' or 'GET'
 25  * @param {Boolean} [options.forceJSON=false] force the responseText to be treated as JSON, ignoring content-type
 26  * @param {Boolean} [options.forceXML=false] force the responseText to be treated as XML, ignoring content-type
 27  * @param {Boolean} [options.autoRun=true] flag to make the XMLHttpRequest when the object is created
 28  */
 29 BTM.AJAX.Request = function Request(url, callbacks, options) {
 30 	this.request = false;
 31 	this.url = url || false;
 32 	
 33 	this.JSONregexp = /json/gi;
 34 	
 35 	this.callbacks = typeof callbacks !== 'undefined' ? Object.isFunction(callbacks) ? {'onSuccess':callbacks} : callbacks : false;
 36 	
 37 	if (this.callbacks) {
 38 		for(var i in this.callbacks) {
 39 			if(this.callbacks.hasOwnProperty(i) && !Object.isFunction(this.callbacks[i])) {
 40 				throw new TypeError();
 41 			}
 42 		}
 43 	}
 44 	
 45 	this.options = Object.update({
 46 				'method' : 'POST',
 47 				'forceJSON' : false,
 48 				'forceXML' : false,
 49 				'autoRun' : true
 50 	}, options);
 51 	
 52 	this.statusNames = {
 53 		200:'onSuccess',
 54 		400:'onError',
 55 		500:'onError'
 56 	};
 57 	
 58 	/**
 59 	 * The custom response object for the Request
 60 	 * @type {Object}
 61 	 */
 62 	this.response = {
 63 		'responseText':'',
 64 		'responseXML' : null,
 65 		'responseJSON': null
 66 	};
 67 	
 68 	var XHRMembersToCopy = [
 69 		'responseText',
 70 		'responseXML',
 71 		'status',
 72 		'statusText',
 73 		'abort'
 74 	];
 75 				   
 76 	/**
 77 	 * Exposed event listener method to allow the onReadyStateChanged event to make calls to this Request
 78 	 */
 79 	this.readyStateChanged = function readyStateChanged() {
 80 		buildResponse.call(this);
 81 		switch (this.request.readyState) {
 82 			case 0:
 83 				if (this.callbacks.onBeforeStart) {
 84 					this.callbacks.onBeforeStart(this.response);
 85 				}
 86 				break;
 87 
 88 			case 1:
 89 				if (this.callbacks.onLoading) {
 90 					this.callbacks.onLoading(this.response);
 91 				}
 92 				break;
 93 				
 94 			case 2:
 95 				if (this.callbacks.onLoaded) {
 96 					this.callbacks.onLoaded(this.response);
 97 				}
 98 				break;
 99 			
100 			case 3:
101 				if (this.callbacks.onInteractive) {
102 					this.callbacks.onInteractive(this.response);
103 				}
104 				break;
105 			
106 			case 4:
107 				if (this.callbacks[this.request.status]) {
108 					this.callbacks[this.request.status](this.response);
109 				}
110 				else if (this.callbacks[this.statusNames[Math.floor(this.request.status/100)*100]]) {
111 					this.callbacks[this.statusNames[Math.floor(this.request.status/100)*100]](this.response);
112 				}
113 				if (this.callbacks.onComplete) {
114 					this.callbacks.onComplete(this.response);
115 				}
116 				break;
117 		}
118 		
119 	};
120 	
121 	function buildResponse() {
122 		for (var i = 0; i < XHRMembersToCopy.length; i++) {
123 			try {
124 				if (Object.isFunction(XHRMembersToCopy[i])) {
125 					this.response[XHRMembersToCopy[i]] = this.request[XHRMembersToCopy[i]].bind(this.request);
126 				}
127 				else {
128 					this.response[XHRMembersToCopy[i]] = this.request[XHRMembersToCopy[i]];
129 				}
130 			}
131 			catch (e) {
132 			
133 			}
134 		}
135 		
136 		//If response is complete, and ready for processing
137 		if (this.request.readyState === 4) {
138 			this.response.contentType = this.request.getResponseHeader('Content-Type');
139 			if (this.options.forceJSON || this.JSONregexp.test(this.response.contentType)) {
140 				this.response.responseJSON = JSON.parse(this.request.responseText);
141 			}
142 			else if (this.options.forceXML && !this.response.responseXML) {
143 				try {
144 					 var parser = new DOMParser();
145 					 this.response.responseXML = parser.parseFromString(this.request.responseText, "text/xml");
146 				
147 				}
148 				catch(e) {
149 					this.response.responseXML = new ActiveXObject("Microsoft.XMLDOM");
150 					this.response.responseXML.async="false";
151 					this.response.responseXML.loadXML(this.request.responseText);
152 				}
153 			}
154 		}
155 	}
156 	
157 	// Apparently IE7's XHR support is crappy
158 	if (window.ActiveXObject) {
159 		this.request = new ActiveXObject("Microsoft.XMLHTTP");
160 	}
161 	else {
162 		this.request = new XMLHttpRequest();
163 	}
164 	
165 	/**
166 	 * Initiate the AJAX request. Normally runs automatically
167 	 */
168 	this.run = function run() {
169 		this.request.open(this.options.method.toUpperCase(), url, true);
170 		this.request.onreadystatechange = this.readyStateChanged.bind(this);
171 		this.request.send(null);
172 	};
173 	
174 	if (this.url && this.request && this.options.autoRun) {
175 		this.run();
176 	}
177 	
178 };
179 
180 /**
181  * @class Special-use class to update an Element with the response of an AJAX request. After the update is performed, all Mappings {@link BTM.Mapping} are re-run for the target element 
182  * @augments BTM.AJAX.Request
183  * @param {HTMLElement|String} selector element ID or element reference to update with response from AJAX call
184  * @param {String} url the URL to request
185  * @param {Function|Function[]} callbacks Function(s) to call, either a single function to call on "Success", or an object containing Functions with keys representing the HTTP codes/status names and/or AJAX pseudo event names.
186  * If a HTTP status code is provided, the generic handler for the corresponding status type will not run.
187  * @param {Function} [callbacks.onBeforeStart] Function to be called when the AJAX request is created (before the internal call to the open() Method)
188  * @param {Function} [callbacks.onLoading] Function to be called when the AJAX request is started (after the internal call to the open() Method)
189  * @param {Function} [callbacks.onLoaded] Function to be when the AJAX request has been sent, but before the response starts to download (after the internal call to the send() Method)
190  * @param {Function} [callbacks.onInteractive] Function to be called when the AJAX request starts to download the response.
191  * @param {Function} [callbacks.onComplete] Function to be called when the AJAX request, and the Element update has completed (after HTTP-status specific callbacks)
192  * @param {Function} [callbacks.onSuccess] Function to be called when the AJAX request is sucessful (i.e. any of the HTTP 2xx codes)
193  * @param {Function} [callbacks.onError] Function to be called when the AJAX request fails (i.e. any of the HTTP 4xx or 5xx codes)
194  * @param {Object} [options] the options Object
195  * @param {String} [options.method='POST'] the HTTP method to use for the request, either 'POST' or 'GET'
196  * @param {Boolean} [options.forceJSON=false] force the responseText to be treated as JSON, ignoring content-type
197  * @param {Boolean} [options.forceXML=false] force the responseText to be treated as XML, ignoring content-type
198  * @param {Boolean} [options.autoRun=true] flag to make the XMLHttpRequest when the object is created
199  */
200 BTM.AJAX.Updater = function Updater(element, url, callbacks, options) {
201 	this.element = BTM.$(element);
202 	callbacks = callbacks || {};
203 	/**
204 	 * Exposed method to allow Element to be updated based on response
205 	 */
206 	this.update = function update(response) {
207 		BTM.DOM.update(element, response.responseText);
208 		BTM.Mapping.init(element);
209 		if (oldComplete) {
210 			oldComplete(response);
211 		}
212 	};
213 	
214 	if (Object.isObject(callbacks) && callbacks.onComplete) {
215 		var oldComplete = callbacks.onComplete;
216 	}
217 	
218 	callbacks.onComplete = this.update.bind(this);
219 	
220 	this.inherits(BTM.AJAX.Request, url, callbacks, options);
221 };
222 BTM.AJAX.Updater.inherits(BTM.AJAX.Request);
223 
224