﻿// AJAX Autocomplete class for input-textfields
// 
// Copyright 2007-2008 René Lønstrup @ RLdesign.dk
// 
// May not be used commercially without permission from the author.
//
// Version: 1.0.0.2
//
//
// CHANGELOG:
//
// 27/04/2008
//     1.0.0.1 -> 1.0.0.2
//           Changed eventargs name of e.SelectedName to e.SelectedValue, which makes more sense when being context-independant
//           Changed event-setting behaviour on Init to use the RLdesign.Events.SetEventHandler function thus allowing multiple events on the objects
// 13/04/2008
//     1.0.0.0 -> 1.0.0.1
//           Added function FocusTextbox
// 22/03/2008
//     1.0.0.0
//           First public release
//



if (!RLdesign) {
	var RLdesign = function() { };
}
if (!RLdesign.Utils) {
	RLdesign.Utils = function() {
		return {
			// returns a crossbrowser event object
			DefineEvent: function(e) {
				return e || window.event;
			},
			// returns the target for the event in question, typically a click-event
			// ie. in the case of a click-event, the target is the element being clicked on
			DefineEventTarget: function(e) {
				var ev = RLdesign.Utils.DefineEvent(e);
				var element = ev.srcElement || ev.target;
				if (element.nodeType == 3) element = element.parentNode; // defeat Safari bug
				return element;
			},
			// returns the keycode for the key that was pressed during the event		
			DefineEventKeyCode: function(e) {
				var ev = RLdesign.Utils.DefineEvent(e);
				var keycode = 0;
				if (ev.which) keycode = ev.which; // Netscape4
				else if (ev.keyCode) keycode = ev.keyCode; // IE
				else if (ev.charCode) keycode = ev.charCode; // Gecko
				return keycode;
			},
			// evaluates whether the given argument object is an array, returning a boolean
			IsArray: function(a) {
				return (a && a.length && typeof(a) != "string" && !a.tagName && !a.alert && typeof(a[0]) != "undefined");
			},
			// evaluates whether the given argument object is an object, returning a boolean
			IsObject: function(o) {
				return (o && o.length == undefined && typeof(o) == "object" && !o.alert);
			},
			// tries to return an object from an arbitrary argument that can be a string or an object
			// if successfully finding an object, either from a search for the object-id or the object itself, 
			// returns the object. If unsuccessful, returns null
			DefineObject: function(el) {
				var elem = null;
				if (typeof(el) == "object") elem = el;
				else if (document.getElementById(el)) elem = document.getElementById(el);
				else if (typeof(el) == "string" && (el.indexOf("(") || el.indexOf("["))) eval("elem = " + el);
				else if (typeof(el) == "string") eval("elem = " + el);
				return elem;
			},
			// creates a given html element in memory and returns the element
			MakeElement: function(tag, attrs, text) {
				var e = document.createElement(tag);
				if (attrs) {
					for (var key in attrs) {
						if (typeof(attrs[key]) == "function") continue; // defeat bug caused by adding custom functions to arrays
						if (key == "class") { e.className = attrs[key]; }
						else if (key == "id") { e.id = attrs[key]; }
						else { e.setAttribute(key, attrs[key]); }
					}
				}
				if (text) e.appendChild(document.createTextNode(text));
				return e;
			},
			// returns the z-index value of the given object
			GetZIndex: function(obj) {
				var elem = RLdesign.Utils.DefineObject(obj);
				if (elem == null) return;
				var cStyle = null;
				if (elem.currentStyle) {
					cStyle = elem.currentStyle;
				}
				else if (document.defaultView && document.defaultView.getComputedStyle) {
					cStyle = document.defaultView.getComputedStyle(elem, "");
				}
				var sNum;
				if (cStyle) {
					sNum = Number(cStyle.zIndex);
				}
				else {
					sNum = Number(elem.style.zIndex);
				}
				return sNum;
			},
			// returns an object of the top, bottom, left and right margins of the given object
			GetMargins: function(obj) {
				var elem = RLdesign.Utils.DefineObject(obj);
				var aReturn = new Object();
					aReturn["top"] = 0;
					aReturn["bottom"] = 0;
					aReturn["left"] = 0;
					aReturn["right"] = 0;
				if (elem == null) return aReturn; // returns default values if no correct object was specified

				var cStyle = null;
				if (elem.currentStyle) {
					cStyle = elem.currentStyle;
				}
				else if (document.defaultView && document.defaultView.getComputedStyle) {
					cStyle = document.defaultView.getComputedStyle(elem, "");
				}
				if (cStyle) {
					aReturn["top"] = Number(cStyle.marginTop);
					aReturn["bottom"] = Number(cStyle.marginBottom);
					aReturn["left"] = Number(cStyle.marginLeft);
					aReturn["right"] = Number(cStyle.marginRight);
				}
				else {
					aReturn["top"] = Number(elem.style.marginTop);
					aReturn["bottom"] = Number(elem.style.marginBottom);
					aReturn["left"] = Number(elem.style.marginLeft);
					aReturn["right"] = Number(elem.style.marginRight);
				}
				return aReturn;
			},
			// returns an object of the top, bottom, left and right paddings of the given object
			GetPaddings: function(obj) {
				var elem = RLdesign.Utils.DefineObject(obj);
				var aReturn = new Object();
					aReturn["top"] = 0;
					aReturn["bottom"] = 0;
					aReturn["left"] = 0;
					aReturn["right"] = 0;
				if (elem == null) return aReturn;
				var cStyle = null;
				if (elem.currentStyle) {
					cStyle = elem.currentStyle;
				}
				else if (document.defaultView && document.defaultView.getComputedStyle) {
					cStyle = document.defaultView.getComputedStyle(elem, "");
				}
				if (cStyle) {
					aReturn["top"] = Number(cStyle.paddingTop);
					aReturn["bottom"] = Number(cStyle.paddingBottom);
					aReturn["left"] = Number(cStyle.paddingLeft);
					aReturn["right"] = Number(cStyle.paddingRight);
				}
				else {
					aReturn["top"] = Number(elem.style.paddingTop);
					aReturn["bottom"] = Number(elem.style.paddingBottom);
					aReturn["left"] = Number(elem.style.paddingLeft);
					aReturn["right"] = Number(elem.style.paddingRight);
				}
				return aReturn;
			},
			// returns the height and width of the current browser window
			GetWindowSize: function() {
				var size = new Object();
					size["width"] = null;
					size["height"] = null;
				var y, x = null;
				if (self.innerHeight && self.innerWidth) { // all except Explorer
					y = self.innerHeight;
					x = self.innerWidth;
				}
				else if (document.documentElement && document.documentElement.clientHeight && document.documentElement.clientWidth) { // Explorer 6 Strict Mode
					y = document.documentElement.clientHeight;
					x = document.documentElement.clientWidth;
				}
				else if (document.body) { // other Explorers
					y = document.body.clientHeight;
					x = document.body.clientWidth;
				}
				size["height"] = parseInt(y);
				size["width"] = parseInt(x);
				return size;
			}, 
			// returns the width and height of the current page (or more precisely, the width/height of the body element of the page)
			GetPageSize: function() {
				var size = new Object();
					size["width"] = null;
					size["height"] = null;
				var scrollWidth = parseInt(document.body.scrollWidth);
				var offsetWidth = parseInt(document.body.offsetWidth);
				var scrollHeight = parseInt(document.body.scrollHeight);
				var offsetHeight = parseInt(document.body.offsetHeight);
				size["width"] = (scrollWidth > offsetWidth) ? scrollWidth : offsetWidth;
				size["height"] = (scrollHeight > offsetHeight) ? scrollHeight : offsetHeight;
				return size;
			}
		}
	} ();
}


if (!RLdesign.Web) {
	RLdesign.Web = function() { };
}
if (!RLdesign.Web.UI) {
	RLdesign.Web.UI = function() { };
}
if (!RLdesign.Web.UI.Forms) {
	RLdesign.Web.UI.Forms = function() { };
}


/// <summary>
/// Adds AJAX-utilizing auto-suggest functionality to a selected textbox
/// </summary>
/// <param name="iSelectLimit">Optional limit on the amount of suggestions returned from the database. Default value is -1 for unlimited</param>
RLdesign.Web.UI.Forms.AutoComplete = function(varArray) {
	this.cur = -1;
	this.layer = null;
	this.provider = null;
	this.textbox = null;
	this.selected = new Object();
		this.selected.value = "";
		this.selected.id = null;
	this.selectlimit = null;
	this.userinput = new Object();
	this.suggestionsshown = false;
	this.hassuggestions = false;
	this.keyuptimeout = null;
	this.keyuptimeoutlength = 250; // length of timeout in milliseconds: 1000 = 1 second
	this.addbutton = null;
	this.containercssclass = "RLdesignAutoCompleteContainer";
	this.selecteditemcssclass = "RLdesignAutoCompleteSelectedItem";
	


	// initiates the class
	this.init = function(VariableArray) {
		this.textbox = RLdesign.Utils.DefineObject(VariableArray.sTextboxID);
		this.textbox.setAttribute("autocomplete","off");
		if (VariableArray.AjaxProvider != null) {
			this.provider = VariableArray.AjaxProvider;
		}
		else if (VariableArray.sAjaxProviderUrl != null) {
			this.provider = new RLdesign.Xml.GenericRemoteProvider(VariableArray.sAjaxProviderUrl);
		}
		
		this.containercssclass = (VariableArray.ContainerCssClass != null && VariableArray.ContainerCssClass != "") ? VariableArray.ContainerCssClass : this.containercssclass;
		this.selecteditemcssclass = (VariableArray.SelectedItemCssClass != null && VariableArray.SelectedItemCssClass != "") ? VariableArray.SelectedItemCssClass : this.selecteditemcssclass;
		
		this.selectlimit = (VariableArray.iSuggestionLimit != null && parseInt(VariableArray.iSuggestionLimit) != NaN) ? VariableArray.iSuggestionLimit : -1;
		this.addbutton = RLdesign.Utils.DefineObject(VariableArray.sAddButtonID);
		this.keyuptimeoutlength = (VariableArray.ShowSuggestionsTimeout != null && parseInt(VariableArray.ShowSuggestionsTimeout) != NaN) ? VariableArray.ShowSuggestionsTimeout : 250;
		
		var oThis = this;
		this.resetUserInput();
		if (this.addbutton != null) {
			RLdesign.Events.SetEventHandler(this.addbutton, "click", function(oEvent) {
				var oE = RLdesign.Utils.DefineEvent(oEvent);
				oThis._onaddclick(oE);
				return false;
			});
		}
		
		RLdesign.Events.SetEventHandler(this.textbox, "keyup", function(oEvent) {
			var oE = RLdesign.Utils.DefineEvent(oEvent);
			oThis.handleKeyUp(oThis, oE);
		});
		
		RLdesign.Events.SetEventHandler(this.textbox, "keydown", function(oEvent) {
			var oE = RLdesign.Utils.DefineEvent(oEvent);
			return RLdesign.Utils.DefineReturnCode(oEvent,oThis.handleKeyDown(oThis, oE));
		});
		
		RLdesign.Events.SetEventHandler(this.textbox, "blur", function() {
			oThis.hideSuggestions();
			oThis.resetUserInput();
		});
		this.createDropdown();
	}



	// selects range of text in textbox
	this.selectRange = function(iStart, iLength) {
		if (this.textbox.createTextRange) {
			var oRange = this.textbox.createTextRange();
			oRange.moveStart("character", iStart);
			oRange.moveEnd("character", iLength - this.textbox.value.length);
			oRange.select();
		}
		else if (this.textbox.setSelectionRange) {
			this.textbox.setSelectionRange(iStart, iLength);
		}
		this.textbox.focus();
	}
	// inserts suggestion as typeahead text to textbox
	this.typeAhead = function(sSuggestion) {
		if ((this.textbox.createTextRange || this.textbox.setSelectionRange) && sSuggestion) {
			var iLen = this.textbox.value.length;
			this.textbox.value = sSuggestion;
			this.selectRange(iLen, sSuggestion.length);
		}
	}
	// gets providerdata, executes typeahead functionality if selected, and prepares to show suggestions
	this.GetProviderData = function(sJSON, oThis) {
		if (sJSON == null || typeof(sJSON) == "undefined") return;
		var aSuggestions = (typeof(sJSON) == "object" || typeof(sJSON) == "array") ? sJSON : eval(sJSON);
		
		//alert(aSuggestions + "\r\n" + sJSON)
		
		if (aSuggestions && aSuggestions.length && aSuggestions.length > 0) {
//			if (bTypeAhead) {
//				this.typeAhead(aSuggestions[0][0]);
//			}
			oThis.showSuggestions(aSuggestions);
		}
		else {
			oThis.hideSuggestions();
			oThis.clearSuggestions();
		}
	}
	// handles key up event and gets the providers to return a suitable suggestion set according to the keys pressed
	this.handleKeyUp = function(oThis, oEvent) {
		var iKeyCode = RLdesign.Utils.DefineEventKeyCode(oEvent);
		if (iKeyCode != 27 && iKeyCode != 38 && iKeyCode != 40 && iKeyCode != 13 && iKeyCode != 9) { // escape, up, down, enter and tab
			oThis.userinput["userinput"] = oThis.textbox.value;
			oThis.provider.AbortConnection();
			if (oThis.keyuptimeoutlength > 0) clearTimeout(oThis.keyuptimeout);
		}
		if (iKeyCode == 8 || iKeyCode == 46) { // backspace or delete
			oThis.provider.AbortConnection();
			if (oThis.keyuptimeoutlength > 0) {
				clearTimeout(oThis.keyuptimeout);
				oThis.keyuptimeout = setTimeout(function() { oThis.provider.OpenConnection(oThis.GetProviderData, oThis.userinput, oThis); }, oThis.keyuptimeoutlength );
			}
			else {
				oThis.provider.OpenConnection(oThis.GetProviderData, oThis.userinput, oThis);
			}
		}
		else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode <= 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
			// ignore non-alphanumeric characters / keys
			if (iKeyCode == 27) {
				this.textbox.value = this.userinput["userinput"];
			}
		}
		else {
			oThis.provider.AbortConnection();
			if (oThis.keyuptimeoutlength > 0) {
				clearTimeout(oThis.keyuptimeout);
				oThis.keyuptimeout = setTimeout(function() {oThis.provider.OpenConnection(oThis.GetProviderData, oThis.userinput, oThis); }, oThis.keyuptimeoutlength );
			}
			else {
				oThis.provider.OpenConnection(oThis.GetProviderData, oThis.userinput, oThis);
			}
		}
	}
	// handles key down event
	this.handleKeyDown = function(oThis, oEvent) {
		var iKeyCode = RLdesign.Utils.DefineEventKeyCode(oEvent);
		var bReturnCode = true;
		switch (iKeyCode) {
			case 38: // up
				if (this.hassuggestions) {
					if (!oThis.suggestionsshown) oThis.unhideSuggestions();
					oThis.previousSuggestion();
				}
				bReturnCode = false;
				break;
			case 40: // down
				if (oThis.hassuggestions) {
					if (!oThis.suggestionsshown) oThis.unhideSuggestions();
					oThis.nextSuggestion();
				}
				bReturnCode = false;
				break;
			case 13: // enter
				this.hideSuggestions();
				if (oThis.cur >= 0 && oThis.hassuggestions) {
					if (oThis.cur >= 0) oThis.selectSuggestion(oThis.layer.childNodes[oThis.cur]);
					oThis.resetUserInput();
				}
				bReturnCode = false;
				break;
			case 9: // tab
				this.resetUserInput();
				break;
			case 27: // escape
				oThis.hideSuggestions();
				oThis.clearSuggestion();
				bReturnCode = false;
				break;
			default:
				if (iKeyCode == 8 || iKeyCode == 32 || (iKeyCode > 45 && iKeyCode < 112) || iKeyCode > 123) {
//					if (oThis.hasidreceiver) oThis.idreceiver.value = "";
					bReturnCode = true;
				}
				break;
		}
		return bReturnCode;
	}
	// hides the suggestion list
	this.hideSuggestions = function() {
		this.suggestionsshown = false;
		this.layer.style.visibility = "hidden";
	}
	// shows the suggestion list but does not reenter information in it
	this.unhideSuggestions = function() {
		this.suggestionsshown = true;
		this.layer.style.visibility = "visible";
	}
	// highlights a selected suggestion in the list
	this.highlightSuggestion = function(oSuggestionNode) {
		var oNode = null;
		for (var i = 0; i < this.layer.childNodes.length; i++) {
			oNode = this.layer.childNodes[i];
			if (oNode == oSuggestionNode) {
				oNode.className = this.selecteditemcssclass;
			}
			else if (oNode.className == this.selecteditemcssclass) {
				oNode.className = "";
			}
		}
	}
	// creates the suggestionlist container and adds eventhandlers to it
	this.createDropdown = function() {
		var arrAttributes = new Object();
			arrAttributes["class"] = this.containercssclass;
		this.layer = RLdesign.Utils.MakeElement("div", arrAttributes);
		this.layer.style.visibility = "hidden";
		this.layer.style.width = (parseInt(this.textbox.offsetWidth) - 2) + "px";
		document.body.appendChild(this.layer);
		var oThis = this;
		this.layer.onmousedown = this.layer.onmouseup = this.layer.onmouseover = function(oEvent) {
			var oE = RLdesign.Utils.DefineEvent(oEvent);
			var oTarget = RLdesign.Utils.DefineEventTarget(oEvent);
			switch (oE.type) {
				case "mousedown":
					oThis.selectSuggestion(oTarget);
					oThis.hideSuggestions();
					break;
				case "mouseover":
					oThis.highlightSuggestion(oTarget);
					break;
				default:
					oThis.textbox.focus();
					break;
			}
		}
	}
	// selecting a suggestion
	this.selectSuggestion = function(oTarget) {
		var oThis = this;
		oThis.selected.value = oThis.textbox.value = oTarget.firstChild.nodeValue;
		oThis.selected.id = oTarget.getElementsByTagName("input")[0].value;
		oThis.provider.AbortConnection();
		clearTimeout(oThis.keyuptimeout);
		this.clearSuggestions();
		// hands over the rest of the selectionprocess to the onselectitem delegate
		this._onselectitem();
	}
	// clears selected suggestion from memory
	this.clearSuggestion = function() {
		var oThis = this;
		oThis.selected.value = oThis.textbox.value = "";
		oThis.selected.id = null;
	}
	// gets left position of textbox
	this.getLeft = function() {
		var oNode = this.textbox;
		var iLeft = 0;
		if (oNode.getBoundingClientRect) { // IE only method of finding elements position / dimension
			iLeft = oNode.getBoundingClientRect().left - 2; // minus two to counteract IE upper-left corner anomaly
		}
		else {
			while (oNode.tagName.toLowerCase() != "body") {
				iLeft += parseInt(oNode.offsetLeft);
				oNode = oNode.offsetParent;
			}
		}
		return iLeft;
	}
	// gets top position of textbox
	this.getTop = function() {
		var oNode = this.textbox;
		var iTop = 0;
		if (oNode.getBoundingClientRect) {
			iTop = oNode.getBoundingClientRect().top - 2; // minus two to counteract IE upper-left corner anomaly
		}
		else {
			while (oNode.tagName.toLowerCase() != "body") {
				iTop += parseInt(oNode.offsetTop);
				oNode = oNode.offsetParent;
			}
		}
		iTop += parseInt(this.textbox.offsetHeight);
		return iTop;
	}
	// show and fill out suggestion list
	this.showSuggestions = function(aSuggestions) {
		var oDiv = null;
		var oHiddenField = null;
		var value = "";
		var arrAttributes = null;
		this.suggestionsshown = true;
		this.layer.innerHTML = "";
		this.cur = -1;
		if (RLdesign.Utils.IsArray(aSuggestions) && aSuggestions.length > 0) {
			this.hassuggestions = true;
			for (var i = 0; i < aSuggestions.length; i++) {
				oDiv = document.createElement("div");
				value = (RLdesign.Utils.IsArray(aSuggestions[i])) ? aSuggestions[i][0] : aSuggestions[i];
				oDiv.appendChild(document.createTextNode(value));
				arrAttributes = new Object();
					arrAttributes["type"] = "hidden"; // should be hidden in final build
					arrAttributes["value"] = (RLdesign.Utils.IsArray(aSuggestions[i])) ? aSuggestions[i][1] : aSuggestions[i];
				oHiddenField = RLdesign.Utils.MakeElement("input", arrAttributes);
				oDiv.appendChild(oHiddenField);
				this.layer.appendChild(oDiv);
			}
			this.layer.style.left = this.getLeft() + "px";
			this.layer.style.top = this.getTop() + "px";
			this.layer.style.visibility = "visible";
		}
	}
	// flushes suggestion list
	this.clearSuggestions = function() {
		this.hassuggestions = false;
		this.suggestionsshown = false;
		this.layer.innerHTML = "";
		this.cur = -1;
	}
	// selects next available suggestion
	this.nextSuggestion = function() {
		var cSuggestionNodes = this.layer.childNodes;
		if (cSuggestionNodes.length > 0) {
			if (this.cur >= cSuggestionNodes.length-1) this.cur = -1;
			var oNode = cSuggestionNodes[++this.cur];
			this.highlightSuggestion(oNode);
		}
	}
	// selects previous suggestion
	this.previousSuggestion = function() {
		var cSuggestionNodes = this.layer.childNodes;
		if (cSuggestionNodes.length > 0) {
			if (this.cur <= 0) this.cur = cSuggestionNodes.length;
			var oNode = cSuggestionNodes[--this.cur];
			this.highlightSuggestion(oNode);
		}
	}
	// resets the object storing user inputs
	this.resetUserInput = function() {
		this.userinput = new Object();
		this.userinput["userinput"] = "";
		this.userinput["precision"] = "startswith";
		this.userinput["limit"] = this.selectlimit;
	}
	
	// focuses on the textbox
	this.FocusTextbox = function() {
		this.textbox.focus();
	}
	
	// user define event handler triggered on selecting a suggestion item
	this.onSelectItem = null;
	this._onselectitem = function(e) {
		var oE = new Object();
		oE.SelectedItem = new Object();
			oE.SelectedItem["value"] = this.selected.value;
			oE.SelectedItem["id"] = this.selected.id;
		oE.SelectedValue = oE.SelectedItem["value"];
		oE.SelectedID = oE.SelectedItem["id"];
		oE.Textbox = this.textbox;
		
		if (typeof(this.onSelectItem) == "function") {
			this.onSelectItem(oE);
		}
	}

	// user define event handler triggered on selecting a suggestion item
	this.onAddClick = null;
	this._onaddclick = function(e) {
		var oE = new Object();
			oE.Value = this.textbox.value;
			oE.Control = this.textbox;

		if (typeof(this.onAddClick) == "function") {
			this.onAddClick(oE);
		}
	}



	this.init(varArray);
};
