/*
usuage:
ajax = new Ajax();
ajax.setRoot('page.php?query=');
//use ajax to request page.php?query=getuser&id=1001&name=Tom, then calls the function callbackFunction when it's done.
ajax.request('getuser','GET',{'id': 1001, 'name': 'Tom'},callbackFunction);

function callbackFunction(response, xml, text) {
//response is a JavaScript Object version of the XML document (provided the xml is valid)
alert(text);
}


Supports POST in the same way:


//send id=1001 & name=Tom as postdata to page.php
ajax.request('page.php','POST',{'id': 1001, 'name': 'Tom'},callbackFunction);




Post entire form:


params = new Array();
for (i=0; i < document.forms[0].elements.length; i++) {
if (document.forms[0].elements[i].type == 'checkbox') {
if (document.forms[0].elements[i].checked)
params[document.forms[0].elements[i].name] = encodeURI(document.forms[0].elements[i].value);

}
else {
params[document.forms[0].elements[i].name] =  encodeURI(document.forms[0].elements[i].value);
}
}
*/
var ajax;
var ajaxobject;
Ajax.prototype.xmlhttp = null;
Ajax.prototype.root = null;
Ajax.prototype.callback = null;
Ajax.prototype.queue = null;
Ajax.prototype.onqueueempty = function() {}
Ajax.prototype.newrequest = false;
Ajax.prototype.readyState = null;
Ajax.prototype.onreadystatechange = function() { }
Ajax.prototype.ver = 0;

function Ajax() {
	this.reset();
	this.root = '';
	this.queue = new Array();
}

var ver =0;
Ajax.prototype.reset = function() {
	try {    // Firefox, Opera 8.0+, Safari
		this.xmlhttp = new XMLHttpRequest();
		this.ver = ver+1;
		ver++;
	}
	catch (e) {    // Internet Explorer
		try {
			this.xmlhttp = new ActiveXObject('Msxml2.XMLHTTP');
		}
		catch (e) {
			try {
				this.xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
			}
			catch (e) {
				alert('An Error Occurred or your web browser is not supported.\nIf you Keep seeing this errror, please email technical@retaileyes.co.uk\n');
				window.location.reload(true);
				return false;
			}
		}
	}
	return true;
}


Ajax.prototype.abort = function() {
	this.xmlhttp.abort();
}

//Useful for adding a "Loading..." div whenever there is a request in progress or setting a boolean variable when it's still running
Ajax.prototype.onstart = function() {}
Ajax.prototype.oncomplete = function() {}

Ajax.prototype.requestCompleted = function() {
	return this.newrequest;
}

Ajax.prototype.getParams = function(form) {
	var params = {};
	
	for (var i = 0; i < form.elements.length; i++) {
		if (form.elements[i].type && (form.elements[i].type == 'radio')) {
			if (form.elements[i].checked) {
				params[form.elements[i].name] = form.elements[i].value;
			}
		}
		else if (form.elements[i].type == 'checkbox') {
			if (form[form.elements[i].name].length) {
				
				if (!params[form.elements[i].name]) params[form.elements[i].name] = new Array();

				if (form.elements[i].checked) params[form.elements[i].name].push(form.elements[i].value);
							
			}
			else {
				if (form.elements[i].checked) params[form.elements[i].name] = form.elements[i].value;
			}			
		}
		else if (form.elements[i].tagName.toLowerCase() == 'select' && form.elements[i].multiple == true) {
			var options = form.elements[i].getElementsByTagName('option');
			
			params[form.elements[i].name] = new Array();
			for (var j = 0; j < options.length; j++) {
				if (options[j].selected) params[form.elements[i].name].push(options[j].value);
			}

		}		
		
		else params[form.elements[i].name] = form.elements[i].value;
	}
	
	return params;
}

Ajax.prototype.postForm = function(action, method, form, callback) {
	
	this.request(action, method, this.getParams(form), callback);

}

Ajax.prototype.request = function(action, method, params, callback, dontbuild, requesttype) {
	if (!requesttype) requesttype = 'application/x-www-form-urlencoded';
	var self = this;
	this.newrequest = true;

	if (this.queue.length == 0) {
		this.onstart();
	}
	
	//check there is no request in progress
	//this.readyStage = this.xmlhttp.readyState;	
	if (this.xmlhttp.readyState == 4 || this.xmlhttp.readyState == 0) {
		if (requesttype == 'application/x-www-form-urlencoded') {
		var paramstr = '';
		if(params) {
			if (!params[0]) {
				for (var p in params) {
					if (is_array(params[p])) {
						for (var j = 0; j < params[p].length; j++) {
							paramstr += ((paramstr.charAt(paramstr.length-1) == '?') ? '' : '&') + p + '[]=' + encodeURIComponent(params[p][j]);					
						}
					}
					else paramstr += ((paramstr.charAt(paramstr.length-1) == '?') ? '' : '&') + p + '=' +  encodeURIComponent(params[p]);
				}
			}
			else {
				for (var i=0;i<params.length;i++) {
					for (var p in params[i]) {
						paramstr += ((paramstr.charAt(paramstr.length-1) == '?') ? '' : '&')  + p + '[]' + '=' +  encodeURIComponent(params[i][p]);
					}
				}
			}
		}
		}
		else paramstr = params;

		if (this.root.indexOf('[action]') > -1) {
			var page = this.root.replace('[action]', action);
		}
		else {
			var page = this.root + action;
		}

		if (action.indexOf('?') < 0 && this.root.indexOf('?') < 0) {
			page += '?'
		}

		this.callback = callback || function() {};

		try { this.xmlhttp.open(method, page + ((method == 'GET') ? paramstr : '') + '&ajaxrand=' + parseInt(Math.random()*10000),true); }
		catch (e) {
			this.reset();
			this.xmlhttp.open(method, page + ((method == 'GET') ? paramstr : '') + '&ajaxrand=' + parseInt(Math.random()*10000),true);
		}

		this.xmlhttp.onreadystatechange = function() {
			self.readyState = self.xmlhttp.readyState;
			self.onreadystatechange();

			if (self.xmlhttp.readyState == 4) {
				self.newrequest = false;
				self.callback(((self.xmlhttp.responseXML == null) ? null : self.responseObject(self.xmlhttp.responseXML.documentElement)), ((self.xmlhttp.responseXML == null) ? null :self.xmlhttp.responseXML.documentElement), self.xmlhttp.responseText);
				//perform next request from the queue

				if (self.queue.length > 0) {
					var req = self.queue.shift();
					self.request(req[0], req[1], req[2], req[3]);
				}
				else {
					self.oncomplete();
					self.onqueueempty();
				}
			}
		}


	
		if (method == 'POST') {
			this.xmlhttp.setRequestHeader('Content-type', requesttype + '; charset=UTF-8');
			this.xmlhttp.setRequestHeader('Content-length', paramstr.length);
			this.xmlhttp.setRequestHeader('Connection', 'close');
		}
		this.xmlhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
		this.xmlhttp.send((method == 'POST') ? paramstr : null);
	}
	else { //there is already a request in progress, add the request to the queue
		this.queue[this.queue.length] = new Array(action, method, params, callback);
	}
}

Ajax.prototype.xmlRequest = function(page, xmldoc, callback, dontbuild) {
	var xml = '<?xml version="1.0" encoding="utf-8"?>\n';
	
	if (xmldoc.xml) {
		xml += xmldoc.xml;
	}
	else {
		var serializer = new XMLSerializer();
		xml += serializer.serializeToString(xmldoc);
	}
	this.request(page, 'POST', xml, callback, dontbuild, 'text/xml');
}


Ajax.prototype.soapRequest = function(page, request, callback) {	
	var paramstr = request.getXml();
	var self = this;
	this.callback = callback || function() {};

	try { this.xmlhttp.open('POST', page + '?ajaxrand=' + parseInt(Math.random()*10000),true); }
	catch (e) {
		this.reset();
		this.xmlhttp.open('POST', page + '?ajaxrand=' + parseInt(Math.random()*10000),true);
	}
	

	this.xmlhttp.onreadystatechange = function() {
		self.readyState = self.xmlhttp.readyState;
		self.onreadystatechange();

		if (self.xmlhttp.readyState == 4) {
			self.newrequest = false;
			self.callback(((self.xmlhttp.responseXML == null) ? null : self.responseObject(self.xmlhttp.responseXML.documentElement)), ((self.xmlhttp.responseXML == null) ? null :self.xmlhttp.responseXML.documentElement), self.xmlhttp.responseText);
			//perform next request from the queue

			if (self.queue.length > 0) {
				var req = self.queue.shift();
				self.request(req[0], req[1], req[2], req[3]);
			}
			else {
				self.oncomplete();
				self.onqueueempty();
			}
		}
	}


	
	this.xmlhttp.setRequestHeader('Content-type',  'application/soap+xml; charset=utf-8');
	this.xmlhttp.setRequestHeader('Content-length', paramstr.length);
	this.xmlhttp.setRequestHeader('Connection', 'close');
	this.xmlhttp.send(paramstr);
}

 
Ajax.prototype.setRoot = function(root) {
	this.root = root;
}

Ajax.prototype.removeWhitespace = function(xmlnode) {
	for (var node=0; node <= xmlnode.childNodes.length-1; node++) {
		if (xmlnode.childNodes[node].nodeType == 3) {
			if (xmlnode.childNodes[node].nodeValue.indexOf("\n") == 0) {
				xmlnode.removeChild(xmlnode.childNodes[node]);
			}

		}
		else {
			this.removeWhitespace(xmlnode.childNodes[node]);
		}
	}
	return xmlnode;
}


Ajax.prototype.responseObject = function(xmlnode) {
	var newObj = new Object();

	if (xmlnode) {

		//loop through all the child nodes for the current node
		for (var node=0; node <= xmlnode.childNodes.length-1; node++) {
			if (xmlnode.childNodes[node].nodeType != 3) {
				//ignore (remove) whitespace at the start of lines.
				if (xmlnode.childNodes[node].hasChildNodes()) {
					if (xmlnode.childNodes[node].firstChild.nodeType == 3) {
						if (xmlnode.childNodes[node].firstChild.nodeValue.indexOf("\n") == 0 || xmlnode.childNodes[node].firstChild.nodeValue.indexOf("\t") == 0) {
							xmlnode.childNodes[node].removeChild(xmlnode.childNodes[node].firstChild);
						}
					}
				}
				//old way -- causes problems when elements have children or grandchildren with the same nodeName as themselves
				//var ncount = xmlnode.childNodes[node].parentNode.getElementsByTagName(xmlnode.childNodes[node].nodeName).length;

				//find out how many sibling nodes there are with the same name as the current one
				var ncount = 0;
				for (var i=0;i<xmlnode.childNodes.length;i++) {
					if (xmlnode.childNodes[node].nodeName == xmlnode.childNodes[i].nodeName) {
						ncount++;
					}
				}
				//ncount should be > 0. It should at least find itself.
				//get the value for the current node
				var nodeVal = (xmlnode.childNodes[node].hasChildNodes()) ? ((xmlnode.childNodes[node].firstChild.nodeType == 3) ? xmlnode.childNodes[node].firstChild.nodeValue : this.responseObject(xmlnode.childNodes[node])) : '';


				//if there is  more than one tag with this name, create an array
				if (ncount > 1) {
					if (!newObj[xmlnode.childNodes[node].nodeName])	{
						newObj[xmlnode.childNodes[node].nodeName] = new Array();
						newObj[xmlnode.childNodes[node].nodeName].isArray = true;
					}
					newObj[xmlnode.childNodes[node].nodeName].type = 'array';
					newObj[xmlnode.childNodes[node].nodeName][newObj[xmlnode.childNodes[node].nodeName].length] = nodeVal;
				}
				//if there is only one tag of the kind, dont use an array.
				else {
					newObj[xmlnode.childNodes[node].nodeName] =  (typeof(nodeVal) == 'object') ? cloneObject(nodeVal) : nodeVal;
					//Even though there is no array, make this accessible as node[0] in case the script is expecting 1 or more items.
					newObj[xmlnode.childNodes[node].nodeName][0] = nodeVal;


					//Give the user a way of checking whether an array has been returned.
					newObj[xmlnode.childNodes[node].nodeName].isArray = false;

					//report length so it is still loopable
					newObj[xmlnode.childNodes[node].nodeName].length = 1;
					//This will cause problems if push()/splice()/etc/other array methods are used on the result though.
				}
			}
		}
		return newObj;
	}
	else return null;
}



Ajax.prototype.xmlToHtmlElement = function(xmlnode, oldnode) {
	//IE hacks. Can't just construct a table in IE for some reason.
	if (xmlnode.tagName.toLowerCase() == 'tr') {
		//if (!oldnode.parentNode) {
		var newNode = oldnode.insertRow(-1);
		if (!newNode) {
			var table = document.createElement('table');
			var newNode = table.insertRow(-1);
		}
	}
	else if (xmlnode.tagName.toLowerCase() == 'td') var newNode = oldnode.insertCell(-1);
	else if (navigator.appName.indexOf('Explorer') > -1 && xmlnode.tagName == 'input' && xmlnode.getAttribute('type') == 'radio') {
		var newNode = document.createElement('<input type="radio" name="' + xmlnode.getAttribute('name') + '" />');
	}
	else var newNode = document.createElement(xmlnode.tagName);

	if (xmlnode.attributes) {
		for (var attribute = 0; attribute <= xmlnode.attributes.length-1; attribute++) {
			//No if-elses needed in FF, but IE's setAttrbibute doesn't seem to work on anything

			//For firefox + all other attributes

			if (navigator.appName.indexOf('Explorer') == -1) {
				try {
					newNode.setAttribute(xmlnode.attributes[attribute].nodeName, xmlnode.attributes[attribute].nodeValue);
				}
				catch (e) {
					//	alert(newNode + '\n\n' + oldnode.tagName + '\n\n' + xmlnode.tagName + '\n\n' + x);
				}
			}
			else {
				//Needed for IE. setAttribute doesn't work on a lot of attributes and they need to be set directly.
				//if (xmlnode.attributes[attribute].nodeName.toLowerCase() == 'class') newNode.setAttribute('className',xmlnode.attributes[attribute].nodeValue);
				//alert(xmlnode.attributes[attribute].nodeName);
				switch (xmlnode.attributes[attribute].nodeName.toLowerCase()) {
					case 'class':  		newNode.setAttribute('className',xmlnode.attributes[attribute].nodeValue); newNode.className 		= xmlnode.attributes[attribute].nodeValue;	break;
					case 'style':  		newNode.style.cssText 	= xmlnode.attributes[attribute].nodeValue;		break;
					case 'colspan':		newNode.colSpan 		= xmlnode.attributes[attribute].nodeValue;		break;
					case 'rowspan':		newNode.rowSpan 		= xmlnode.attributes[attribute].nodeValue;		break;
					case 'onmousedown':	newNode.onmousedown		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onmousemove':	newNode.onmousemove 	= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onmouseup':	newNode.onmouseup 		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onclick':		newNode.onclick 		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onchange':	newNode.onchange		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onkeyup':		newNode.onkeyup 		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'onkeydown':	newNode.onkeydown 		= new Function('e', 'var event = e || window.event;  ' + xmlnode.attributes[attribute].nodeValue); break;
					case 'name':		newNode.name 			= xmlnode.attributes[attribute].nodeValue;		break;
					case 'value':		newNode.value 			= xmlnode.attributes[attribute].nodeValue;		break;
					default: newNode.setAttribute(xmlnode.attributes[attribute].nodeName, xmlnode.attributes[attribute].nodeValue);
				}				
			}		
		}
	}
	//Another IE hack, TABLE elements need TBODY elements if they dont have them already or the table is not displayed.
	var tbl = null;
	if (newNode.tagName.toLowerCase() == 'table') {
		//if there are tbody's or theads on the table already, don't add a default one.
		if (xmlnode.getElementsByTagName('tbody').length == 0 && xmlnode.getElementsByTagName('thead').length == 0) {
			//If there is no tbody, create one.
			if (!oldnode) oldnode = document.createElement('div');
			oldnode.appendChild(newNode);
			tbl = newNode;
			var tbody = document.createElement('tbody');
			newNode.appendChild(tbody);
			var newNode = tbody;
		}
	}
	else if (newNode.tagName.toLowerCase() == 'form' && newNode.name) {
		document.forms[newNode.name] = newNode;
	}
	
	if (xmlnode.childNodes.length == 0) {
		//newNode.appendChild(document.createTextNode(''));
	}
	else {
		for (var node=0; node <= xmlnode.childNodes.length-1; node++) {
			if (xmlnode.childNodes[node].nodeType === 3 || xmlnode.childNodes[node].nodeType === 1) { //to ignore comments
				newNode.appendChild((xmlnode.childNodes[node].nodeType === 3) ? document.createTextNode(xmlnode.childNodes[node].nodeValue) : ajax.xmlToHtmlElement(xmlnode.childNodes[node], newNode));
			}
		}
	}
	return tbl || newNode;
}