﻿/// <reference path="jquery-latest-vsdoc.js" />
/*
Generic suggestion popup.

To use:

$(textbox).suggest(url, [options]);

textbox: a jQuery selector or variable
url: the absolute path to the resource that returns the results in JSON format
options: optional, the options to use.

Example 1:
==========
$("#search").suggest("/Admin/Services/Search.aspx", { minChars : 4, showColHeaders : false });

options:
	minChars : the minimum number of characters in the text field before retrieving suggestions
	showColHeaders: whether to show column headers for suggestions
	columns: array of columns to show and optional formatting (see e.g.)
	valueField: the field used for the value when an option is selected.
	textField: the text field to show when an option is selected.
	params: additional values to be passed in querystring to the URL
	hiddenField: jQuery for the hidden fields to be updated with the valueField
	itemSelected: event handler for when an item in the suggest list is selected
		takes the selected value and text as paramaters
	itemDeselected: event handler for when an item in the suggest list is deselected
		takes the text as a paramater

Example 2:
==========
$(document).ready(function() { $("#search input:text").suggest(
	'<%= Page.ResolveUrl("~/Admin/Services/Search.aspx") %>',
	{
		valueField : "Id",
		hiddenField : "#search input:hidden"
		columns : [
			{ "Name" : function(result){ result["FirstName"]+' '+result["LastName"]} },
			{ "Student No." : "ExternalId" },
			"Email"
		],
		textField : function(result){ result["FirstName"]+' '+result["LastName"]} ,
		params : { type : "JobSeeker" }
	});
});

To get the selected value (if the valueField option has been set), use suggestVal()
		
*/

(function ($) {
	$.fn.suggest = function (url, options) {
		var opts = $.extend({}, $.fn.suggest.defaults, options);

		return this.each(function () {
			new suggest(this, url, opts);
		});
	};

	$.fn.suggestVal = function () {
		if (this.length == 0) return null;
		return this.get(0).suggest.value;
	};

	$.fn.suggest.defaults = {
		minChars: 1,  // the minimum number of characters to be typed before suggestions are shown
		delay: 100,  // the milliseconds to delay searching once typing commences
		showColHeaders: true, // whether to show column headers
		popupClassName: "suggest" // the CSS class name to use for the popup suggestions
	};

	// suggest object
	suggest = function (textbox, url, options) {
		// only allow one suggest object on the textbox
		if (textbox.suggest != null) textbox.suggest.destroy();

		var KEYCODE_UP = 38;
		var KEYCODE_LEFT = 37;
		var KEYCODE_DOWN = 40;
		var KEYCODE_RIGHT = 39;
		var KEYCODE_ENTER = 13;
		var KEYCODE_ESC = 27;
		var KEYCODE_TAB = 9;

		this.textbox = textbox;
		this.url = url;
		this.options = options;
		this.highlighted = null;
		this.popupVisible = false;
		this.value = null;
		this.lastValue = null;
		this.lastText = "";
		this.timeoutid = null;
		textbox.suggest = this;

		var $this = this; // used in event handlers

		textbox.setAttribute("autocomplete", "off");

		// popup
		var $popup = $("<div class='" + options.popupClassName + "'><table></table></div>");
		var $popup_table = $popup.find("table");

		var $textbox = $(textbox);
		$textbox.focus(textbox_focus);
		$textbox.blur(textbox_blur);
		$textbox.keydown(textbox_keydown);
		$textbox.keyup(textbox_keyup);
		$textbox.keypress(function (e) { if (e.keyCode == 13) { e.preventDefault(); return false; } });

		var $processing = $("<div class='suggest-list-processing' style='position: absolute; display: none;'></div>");

		this.toString = function () {
			return "[suggest]";
		};

		this.destroy = function () {
			$popup.remove();
			$textbox.unbind('focus', textbox_focus);
			$textbox.unbind('blur', textbox_blur);
			$textbox.unbind('keydown', textbox_keydown);
			$textbox.unbind('keyup', textbox_keyup);
			if (this.request && this.request.abort) this.request.abort();
			window.clearTimeout(this.timeoutid);
			$textbox[0].suggest = null;
		}

		this.getSuggestions = function (val) {
			val = val.toLowerCase();

			if ($this.request) {
				if ($this.request.val == val) return;
				$this.request.xhr.abort();
			}
			$this.request = { val: val };

			var pos = $textbox.offset();
			var height = $textbox.outerHeight();
			var width = $textbox.width();

			if (!$processing.parent().length) $processing.appendTo(document.body);

			$processing.css({
				display: "block",
				left: pos.left + width - 20,
				top: pos.top + height / 2 - 10
			});

			var params = $.extend($this.options.params, { search: get_text_value() });

			$this.request.xhr = $.getJSON(
				$this.url,
				params,
				function (data) {
					$this.request = null;
					$processing.hide();
					$this.showSuggestions(data);
				}
			);
		};

		this.showSuggestions = function (data) {
			if (ch.hasJsonError(data) || !data) {
				this.hideSuggestions();
				return;
			}

			var results = data.results;
			if (results.length == 0) {
				this.hideSuggestions();
				return;
			}

			// clear previous suggestions
			$popup_table.empty();
			var table = $popup_table[0];

			var columns = this.options.columns;
			if (columns == null) {
				var firstResult = results[0];
				columns = new Array(0);

				// auto-generate columns base on the first result;
				for (var colName in firstResult) {
					columns.push(colName);
				}
			}

			// create column headers
			if (this.options.showColHeaders) {
				var thead = document.createElement("thead");
				table.appendChild(thead);
				var tr = document.createElement("tr");
				thead.appendChild(tr);

				// create the column headers
				for (var i = 0; i < columns.length; i++) {
					var column = columns[i];

					var columnName = null;
					if (typeof (column) == "string") {
						columnName = column;
					} else {
						columnName = getFirstPropName(column);
					}

					var th = document.createElement("th");
					$(th).text(columnName);
					tr.appendChild(th);
				}
			}

			var tbody = document.createElement("tbody");
			table.appendChild(tbody);

			// results
			for (var i = 0; i < results.length; i++) {
				var tr = document.createElement("tr");

				var result = results[i];

				for (var j = 0; j < columns.length; j++) {
					var column = columns[j];

					var columnVal = null;
					if (typeof (column) == "string") {
						columnVal = result[column];
					} else {
						var format = null;
						for (var prop in column) {
							format = column[prop];
							break;
						}

						if (typeof (format) == "string")
							columnVal = result[format];
						else columnVal = format(result);
					}

					var td = document.createElement("td");
					if (columnVal == null) {
						$(td).text("");
					} else if (columnVal.html != null) {
						$(td).html(columnVal.html);
					} else {
						$(td).text(columnVal);
					}
					tr.appendChild(td);
				}

				tr.result = result;
				tbody.appendChild(tr);

				$(tr)
					.mouseover(tr_mouseover)
					.mouseup(tr_mouseup)
					.mousedown(tr_mousedown);
			}

			// check if the user has typed the result
			for (var i = 0; i < results.length; i++) {
				if (this.getText(results[i]).toLowerCase() == this.textbox.value.toLowerCase()) {
					this.highlight($popup_table.find("tbody tr:eq(" + i + ")")[0]);
					this.selectHighlighted();
					break;
				}
			}

			// position the suggest box
			var offset = $(textbox).offset();
			offset.top += $(textbox).outerHeight();

			if (!$popup.parent().length) {
				$popup.appendTo(document.body);
				var $parent = $textbox.parent();
				var zIndex = 0;
				while ($parent != null && $parent[0] != document.body && (zIndex = $parent.css("z-index")) == "auto") $parent = $parent.parent();
				$popup.css("z-index", isNaN(zIndex) ? "auto" : parseFloat(zIndex) + 1);
			}

			$popup.css({
				position: "absolute",
				top: offset.top,
				left: offset.left,
				width: "auto",
				display: "block"
			});

			var textwidth = $(textbox).width();
			if (textwidth > $popup.width()) {
				$popup.css("width", textwidth);
			}

			this.popupVisible = true;
		};

		this.hideSuggestions = function () {
			$popup.hide();
			this.popupVisible = false;
		};

		this.highlight = function (tr) {
			if (this.highlighted != null) {
				$(this.highlighted).removeClass("hl");
			}
			if (tr != null) $(tr).addClass("hl");
			this.highlighted = tr;
		};

		this.highlightDown = function () {
			var next
			if (this.highlighted == null) {
				next = $popup_table.find("tbody tr:first")[0];
			} else {
				next = $(this.highlighted).next("tr")[0];
			}
			this.highlight(next);
		};

		this.highlightUp = function () {
			var prev;
			if (this.highlighted == null) {
				prev = $popup_table.find("tbody tr:last")[0];
			} else {
				prev = $(this.highlighted).prev("tr")[0];
			}
			this.highlight(prev);
		};

		this.userInput = "";
		this.selectHighlighted = function () {
			this.textbox.focus(); // do this first, otherwise IE moves focus to start of text

			if (this.highlighted == null) {
				this.textbox.value = this.userInput;
				this.value = null;

			} else {
				var result = this.highlighted.result;
				this.textbox.value = this.getText(this.highlighted.result);
				this.value = this.getValue(this.highlighted.result);
			}

			if (this.options.hiddenField != undefined) {
				$(this.options.hiddenField).val(this.value);
			}

			this.lastText = this.textbox.value;
		};

		this.fireItemSelected = function () {
			if (this.options.itemSelected != null) {
				this.options.itemSelected(this.value, $textbox.val(), this.highlighted.result);
			}
			this.lastValue = this.value;

			$this.hideSuggestions();
			// Chrome won't fire change event, so do it manually
			if ($this.highlighted != null) $($this.textbox).change();
		}

		this.fireItemDeselected = function () {
			if (this.options.itemDeselected != null && this.value == null && this.lastValue != null) {
				this.options.itemDeselected($textbox.val());
			}
			this.lastValue = null;
		}

		// helper functions
		this.getText = function (result) {
			var column;
			if (this.options.textField != null) {
				if (typeof (this.options.textField) == "string") {
					column = result[this.options.textField];
					if (column == undefined) throw "Column does not exist in results: " + this.options.textField;
				} else {
					column = this.options.textField(result);
				}
			} else {
				column = result[getFirstPropName(result)];
			}

			return (column.html != null ? column.value : column);
		}

		this.getValue = function (result) {
			if (this.options.valueField != null) {
				return result[this.options.valueField];
			} else {
				return this.getText(result);
			}
		}

		function getFirstPropName(obj) {
			for (var propName in obj) {
				return propName;
			}
		};

		function get_text_value() {
			if ($($this.textbox).hasClass("example")) return "";
			return $this.textbox.value;
		}

		// events
		function tr_mouseover(event) {
			$this.highlight(this);
		}

		var mouseDown = false;
		function tr_mouseup(event) {
			$this.selectHighlighted();
			$this.fireItemSelected();
			mouseDown = false;
		}

		function tr_mousedown(event) {
			mouseDown = true;
			return false;
		}

		function textbox_focus(event) {
			var val = get_text_value();
			if (!$this.popupVisible && val.length >= $this.options.minChars) {
				$this.getSuggestions(val);
			}
		}

		function textbox_blur(event) {
			if ($this.popupVisible && !mouseDown) {
				$this.hideSuggestions();
			}
		}

		function textbox_keydown(event) {
			if (!$this.popupVisible) return;

			switch (event.keyCode) {
				case KEYCODE_UP:
					$this.highlightUp();
					//$this.selectHighlighted();
					return false;
				case KEYCODE_DOWN:
					$this.highlightDown();
					//$this.selectHighlighted();
					return false;
				case KEYCODE_ENTER:
					$this.selectHighlighted();
					$this.fireItemSelected();
					event.preventDefault();
					return false;
				case KEYCODE_TAB:
					$this.selectHighlighted();
					$this.fireItemSelected();
					return true;
				case KEYCODE_ESC:
					$this.highlight(null);
					$this.selectHighlighted();
					$this.fireItemDeselected();
					$this.hideSuggestions();
					return false;
			}
		}

		function textbox_keyup(event) {
			var val = get_text_value();

			if ($this.lastText != val) {
				window.clearTimeout($this.timeoutid);
				$this.timeoutid = window.setTimeout(function () {
					if (val.length < $this.options.minChars) {
						$this.hideSuggestions();
					} else {
						$this.getSuggestions(val);
					}

				}, $this.options.delay)

				$this.fireItemDeselected();
				$this.lastText = val; // save to keep track of text changes
				$this.userInput = val;
				$this.highlighted = null;
				$this.selectHighlighted();
			}
		}

		// done on init
		this.hideSuggestions();
	};
})(jQuery);
