/*******************************
*
*	EzJx: an "easy" Ajax class
*
	This class is designed to provide a lightweight and efficient wrapper for the XmlHTTP
	object in Javascript.  Current features include the ability to declare callback functions,
	a simple "Loading..." dialogue display, and alerts when requests fail.

	NOTES:
		The "Loading..." display only appears if a call takes more than 200ms to load (to prevent
		a flicker) in the upper right corner of the window.

		In the event of a failure, a dialogue box (javascript: alert) will notify the user that either
		(a) 'Error 404: URL not found.' when a 404 header is returned, or
		(b) 'Unable to connect to server.  Please try again in a few minutes.' in the event of any other error

		If a request results in an error, the callback function will NOT be called.  This behavior thus
		dictates that no changes should be made prior to the firing of the callback function.  Although
		this behavior can be changed, it is recommended that you do not.  This is because if changes are
		made outside of a callback function (prior to a successful request) the client and server data
		can become out of sync if a server-side error occurs.

		The W3C DOM does not allow requests to be made to domains other than the domain where the script
		resides.  In the event that your code makes such a request, the request will be terminated and the user
		will be told that:
			Illegal request made to another domain.
			Your site may have been hacked.
		You can change this if need be.


	USES:
		There are a number of function in this class, the following outlines only a subset

		SIMPLE USAGE:
		// create an instance of EzJx
		var ajax = new EzJx();

		// submitting data via GET or POST with no client-side effects:

			// optionally create some data to submit in the form of a Key, Value array:
			var data = {'username' : 'admin', 'password' : 'abcd'};

			// submitting via GET
			ajax.getRequest("http://url", data);
			// submitting via POST
			ajax.postRequest("http://url", data);


		// replace the content of an HTML element with server response text
		// this requires that the page called returns text (can be HTML or plain text)
		// also requires the id of the element in question (e.g. <div id="div1">contents</div>
		// NOTE: this call uses the POST method

			ajax.replaceHTML("http://url", data, "div1");


		// for more complex queries, use a callback function:
		// simply include the function as the third parameter to getRequest or postRequest
		// NOTE: pass the function explicitly, do NOT use the function name in a string
		// your callback function will be passed the XmlHTTP object upon success
		// this object has a few parameters including responseText and responseXML.
		// see http://developer.apple.com/internet/webcontent/xmlhttpreq.html
		//
		// for example, you could output the response with:

			function handleResponse(XmlResponse) {
				alert(XmlResponse.responseText);
			}
			ajax.postRequest("http://url", data, handleResponse);


		// you can also request that certain data is passed to the callback function
		// this class allows one additional parameter (callback_data) to be passed.
		// this is useful when a page has many objects doing similar tasks.
		//
		// suppose you have 3 DIV tags, each of which can seperately update their
		// content via AJAX.  you will need to know which DIV tag set off the event,
		// so pass the DIV tag ID as an additional callback parameter:

			function divClicked(divObj) {
				// ...
				ajax.postRequest("http://url", data, handleResponse, divObj.id);
			}

			function handleResponse(XmlResponse, divId) {
				alert('Div ' + divId + ' wants to tell you ' + XmlResponse.responseText);
			}

			// from the html:
			<div id="bob" onclick="divClicked(this)">content</div>
*
*
*
********************************/




/*****
*           CLASS MEMBERS
*/


// the instance counter allows us to store unique id's for each
// instance of this class
var _ezjx__instance_counter = 1;



// Constructor
//
// this.connection -> the XmlHTTP object used to make our AJAX calls
// this.status -> the current status of the XmlHTTP as defined by XmlHTTP.readyState (0-4)
// uniqueid -> the current value of the instance counter
function EzJx() {
	this.connection = null;
	this.status = 0;
	try {
		// Firefox, Opera 8.0+, Safari
		this.connection = new XMLHttpRequest();
	}
	catch(e) {
		try {
			// Internet Explorer
			this.connection = new ActiveXObject("Msxml2.XMLHTTP");
		}
		catch(e) {
			try {
				this.connection = new ActiveXObject("Microsoft.XMLHTTP");
			}
			catch(e) {
				// in the event of no AJAX support, alert the user that there is a problem!
				alert("Your browser is not supported.  Try Firefox!\r\nhttp://www.mozilla.com/en-US/firefox/");
				return 0;
			}
		}
	}

	this.uniqueid = _ezjx__instance_counter++;
}



// Sets the XmlHTTP.onreadystatechange event
//
// establishes the onreadystatechange function in the XmlHTTP object
//   this first ensures that a "Loading..." message is displayed if a request takes more than 200ms to load.
//   it also checks for an .onload function (set with .setOnLoad( func ), to call when the request is completed
// must be called *after* any XmlHTTP.open call to prevent a bug in IE which kills the object's connection
function _ezjx__set_onreadystatechange() {
	var _this = this;
	this.connection.onreadystatechange = function() {
			_this.status = _this.connection.readyState;

			// if this is the first time we've done this, we need to create the "Loading" element:
			if(null == _this.message_obj) {
				try {
					_this.message_obj = document.createElement("SPAN");
					_this.message_obj.appendChild(document.createTextNode("Loading..."));
					_this.message_obj.id = 'ezjx_loading_span' + _this.uniqueid;
					_this.message_obj.style.visibility = "hidden";
					_this.message_obj.style.position = "absolute";
					_this.message_obj.style.backgroundColor = "#FF3333";
					_this.message_obj.style.padding = "3px";
					document.getElementsByTagName("BODY").item(0).appendChild(_this.message_obj);
					_this.message_obj.ezjxStatus = 0;
					_this.message_obj.ezjxLoading = false;
				}
				catch(e) {
					_this.message_obj = null;
				}
			}

			if(_this.message_obj) {
				_this.message_obj.ezjxStatus = _this.status;

				// handle loading display..

				// the request is complete, remove the "Loading..." message
				if(_this.status == 0 || _this.status == 4) {
					_this.message_obj.style.visibility = 'hidden';
					_this.message_obj.ezjxLoading = false;
				}

				// position the message and wait 200ms.  if the page is still loading, display the message
				else {
					// attempt to position the "Loading..." message in the upper right hand corner,
					// regardless of current scroll position
					_this.message_obj.style.top = _ezjx__window_scrolled_top() + 3 + 'px';
					_this.message_obj.style.left = _ezjx__window_scrolled_right() - _this.message_obj.offsetWidth - 11 + 'px';

					if(false == _this.message_obj.ezjxLoading) {
						_this.message_obj.ezjxLoading = true;
						setTimeout("_ezjx__show_load_message('" + _this.message_obj.id + "')", 200);
					}
				}
			}

			// on completion of the request (status == 4), verify that the request
			// was successful and if so, call the callback function if one was set
			// otherwise report an error
			if(_this.status == 4) {

				// error checking...
				if(_this.connection.status == 404) {
					_this.connection.abort();
					alert('Error 404: URL not found.');
					return 0;
				}
				// general errors
				if(_this.connection.status != 200) {
					_this.connection.abort();
					// alert('Unable to connect to server.  Please try again in a few minutes.');
					return 0;
				}

				// if a callback function was used
				if(_this.onload) {
					if(_this.onload_data) _this.onload(_this.connection, _this.onload_data);
					else _this.onload(_this.connection);
				}
				// reset the event
				_this.onload = null;
				// reset status
				_this.status = 0;
			}
		};
}
EzJx.prototype.setOnReadyStateChange = _ezjx__set_onreadystatechange;


// Submit data via GET method
//
// @url - the location to set the data
// @data - an array of Key, Element pairs to submit as part of the URL query string
// @callback - OPTIONALLY takes a callback function which will receive the XmlHTTP object upon success
// @callback_data - extra parameters to be passed to the callback function.  helpful if running many
//                  instances of EzJX in a single page
function _ezjx__get_request(url, data, callback, callback_data) {
	// data is an optional parameter
	if(null == data) data = [];

	var query = _ezjx__array_to_query(data);
	if(query.length > 1) url += "?" + query;

	if(callback) {
		this.setOnLoad(callback, callback_data);
	}
	return this.ajaxRequest("GET", url, [], []);
}
EzJx.prototype.getRequest = _ezjx__get_request;



// Submit data via POST method
//
// @url - the location to set the data
// @data - an array of Key, Element pairs to submit as POST data
// @callback - OPTIONALLY takes a callback function which will receive the XmlHTTP object upon success
// @callback_data - extra parameters to be passed to the callback function.  helpful if running many
//                  instances of EzJX in a single page
function _ezjx__post_request(url, data, callback, callback_data) {
	// data is an optional parameter
	if(null == data) data = [];

	if(callback) {
		this.setOnLoad(callback, callback_data);
	}
	return this.ajaxRequest("POST", url, data, {'Content-Type' : 'application/x-www-form-urlencoded'});
}
EzJx.prototype.postRequest = _ezjx__post_request;


// Replace the content of an HTML element with the response text of a POST
// USES INNERHTML
//
// @elem_id - the HTML element whose content is to be replaced
// @url - the location to set the data
// @data - an array of Key, Element pairs to submit as POST data
function _ezjx__request_fill_element(url, data, elem_id) {
	var callback = function(response_object) {
			_ezjx__getElement(elem_id).innerHTML = response_object.responseText;
		};
	this.setOnLoad(callback);
	this.postRequest(url, data);
}
EzJx.prototype.replaceHTML = _ezjx__request_fill_element;



// General AJAX request method
//
// @mode - the type of connection being opened (e.g. GET, POST, HEADER)
// @url - the location to set the data
// @data - an array of Key, Element pairs to submit as data
// @header_requests - an array of Key, Element pairs which correspond to Request Headers and values
function _ezjx__ajax_request(mode, url, data, header_requests) {
	var clear_status = this.verifyStatus();
	if(0 == clear_status) return 0;

	// verify that the request is legal before executing
	try {
		this.connection.open(mode, url, true);
	}
	catch(e) {
		if('Permission denied to call method XMLHttpRequest.open' == e) {
			alert('Illegal request made to another domain.\r\nYour site may have been hacked.');
			return;
		}
	}

	this.setOnReadyStateChange();
	// always declare the user-agent
	this.connection.setRequestHeader('User-Agent','XMLHTTP/1.0');
	// send all request headers
	for(var key in header_requests) {
		this.connection.setRequestHeader(key, header_requests[key]);
	}
	this.connection.send(_ezjx__array_to_query(data));
	return 1;
}
EzJx.prototype.ajaxRequest = _ezjx__ajax_request;


// Set a function to be called when the request is completed
//
// @load_function - the function to be called when the request is completed successfully
function _ezjx__set_onload(load_function, load_data) {
	var clear_status = this.verifyStatus();
	if(0 == clear_status) return 0;

	this.onload = load_function;
	this.onload_data = load_data;
}
EzJx.prototype.setOnLoad = _ezjx__set_onload;


// Determine if a connection is still executing a thread
//
// returns 1 if the connection is free, 0 otherwise
function _ezjx__verify_connection_status() {
	if(this.status != 0) {
		return 0;
	}
	return 1;
}
EzJx.prototype.verifyStatus = _ezjx__verify_connection_status;








/****
*      HELPER FUNCTIONS WHICH ARE NOT MEMBER FUNCTIONS OF EzJx
*/

function _ezjx__getElement(element_id) {
	var elementObj = false;

	if (document.getElementById)
	{
		// this is the way the standards work
		elementObj = document.getElementById(element_id);
	}
	else if (document.all)
	{
		// this is the way old msie versions work
		elementObj = document.all[element_id];
	}
	else if (document.layers)
	{
		// this is the way nn4 works
		elementObj = document.layers[element_id];
	}

	return elementObj;
}


// return the Y-coordinate of the top of the window (accounts for scrolling by the user)
function _ezjx__window_scrolled_top() {
	var val = 0;
	if(document.body && document.body.scrollTop) {
		val = document.body.scrollTop;
	}
	if(document.documentElement && document.documentElement.scrollTop) {
		val = document.documentElement.scrollTop;
	}
	return val;
}


// return the X-coordinate of the right of the window (accounts for scrolling by the user)
function _ezjx__window_scrolled_right() {
	var r = 0;
	if(document.body.offsetWidth) {
		r = document.body.offsetWidth;
	}
	else if(window.innerWidth) {
		r = window.innerWidth;
	}

	if(document.body && document.body.scrollLeft) {
		r += document.body.scrollLeft;
	}
	else if(document.documentElement && document.documentElement.scrollLeft) {
		r += document.documentElement.scrollLeft;
	}

	return r;
}


// displays the "Loading..." by setting its visibility
// used because the visibility is delayed with a setTimeout call
// to prevent flickering of the message
function _ezjx__show_load_message(objId) {
	// get object
	var message_obj = _ezjx__getElement(objId);

	// verify that the job is still loading
	if(message_obj.ezjxLoading) {
		message_obj.style.visibility = "visible";
	}
}

// given an array of Key, Element pairs, this function
// returns an escaped HTML query string containing &Key=Element
function _ezjx__array_to_query(arr) {
	var val = "";
	for(var key in arr) {
		if(val.length > 1) val += "&";
		val += escape(key) + "=" + escape(arr[key]);
	}
	return val;
}
