/***************************************************************************************************************\
	Requires jQuery
	
	Usage:
		Given HTML similar to this:
			- 	The ID on the hidden input field is not required for basic usage (see below).  The ID does 
				not need to be any particular value or pattern when used.
	
									<div class="select">
										<a class="label" href="#">Country</a>
										<input id="country_select" type="hidden" name="country" value="" />
										<div class="options">
											<a class="option" href="#" value="1">United States</a>
											<a class="option" href="#" value="2">Canada</a>
											<a class="option" href="#" value="3">England</a>
										</div>
									</div>
		
		Script it like so:
			- 	Simply include this file for basic usage.
			-	If "onchange" events are needed:
					- It is recommended to set an ID attribute for the hidden form field -- though any
					  appropriate selector may be used, ID just seems easiest.
					- The "change" event is triggered on the hidden form field when a value is set. This 
					  may be used to trigger scripts which make use of the user's input.
					- The variable "value" is set in the example below giving the current "value" of the 
					  hidden field.
					- If new options are loaded for a select, the select_clear function may be used to
					  clear any previously selected value and (optionally) to change the default text
					  label (see notes within that function).  This is not required...it's just a 
					  convenience.
			
									//using the hidden form field's ID from the HTML example above
									jQuery(function($){
										$('#country_select').change(function(){
											var value = $(this).val();
											//use the value to do whatever:
											//alert(value);
										});
									});
			
	Pre-populated forms:
		-	Simply set the value of the hidden field and the text label will be selected from the matching
			"option".  This also works for "remembering" the selection if the page is refreshed (on F5 but
			not Ctrl+F5 -- just like the real thing).
			
	Notes:
		-	"Options" with a blank or missing value attribute will reset the menu to its original values.
			This may not be optimal for a non-required field on a pre-populated form...needs some thought.
		-	Should work with a form reset button (reverting to original label text and hidden field value).
		-	If entire div.select menus are added after DOM ready, select_init() must be run again.  This 
			will only initialize new .select menus.
		-	"Options" may be added or removed without rerunning the init function.
		-	Keyboard navigation should be similar to that of an HTML select element.  The "options" are
			outside of the normal tab index, though you can tab through the options when they are visible.
			There are visual cues for the element with the current focus (be it the closed "select" or the
			current "option").  Otherwise, the arrow keys may be used to navigate options.  The enter or 
			space keys may be used to select options or to open menus.  The Esc key may be used to close a 
			menu with making a selection.
		-	The menu elements should "blur" properly -- closing when open and losing visual cues when 
			appropriate.

\***************************************************************************************************************/
jQuery(function(){
	select_init();
});
function select_init(){
	var $ = jQuery; // $ == jQuery within function's scope only
	//clear old inline click handlers, if present
	$('div.select a').removeAttr('onclick');
	$('div.select a.option').attr('tabindex','-1');
	//close any open selects on "blur" (selects stop click propagation)
	$('body').click(function(){
		$('div.select_open').each(function(){select_close($(this))});
		$('div.select a.label_focus').blur();
	});
	//set up each select
	$('div.select:not(.ready)').each(function(){
		var $this = $(this), $label = $this.find('a.label:first'), $field = $this.find('input[type="hidden"]:first');
		$this.addClass('ready'); //allows new .select menus to be initialized wihout interfering with existing ones
		$this.click(function(e){e.stopPropagation()}); //(don't allow internal clicks to propagate)
		$this.find('.options').css('width',($this.outerWidth()-2) + 'px');
		//events
			//  delegation allows options to be changed without needing to reset event listeners
			//	provided the parent .select element exists on DOM ready
		//focus/blur
			$this.delegate('a.label', 'focus', function(){$(this).addClass('label_focus')});
			$this.delegate('a.label', 'blur', select_label_blur);
			$this.delegate('a.option', 'focus', function(){$(this).addClass('option_focus'); $('a.label_focus').removeClass('label_focus');});
			$this.delegate('a.option', 'blur', function(){$(this).removeClass('option_focus')});
		//hover
			$this.delegate('.options', 'mouseenter', function(){$(this).addClass('options_hover')});
			$this.delegate('.options', 'mouseleave', function(){$(this).removeClass('options_hover')});
		//click
			$this.delegate('a.label', 'click', select_label_click);
			$this.delegate('a.option', 'click', select_option_click);
		//keydown
			$this.delegate('a.label', 'keydown', select_label_keydown);
			$this.delegate('a.option', 'keydown', select_option_keydown);
		//parent form reset (if used within a form)
			var $form = $this.parents('form:first');
			if($form.length > 0) $form[0].onreset = function(){select_form_reset($form)};
		
		//deal with preset and/or default values
		var val = $.trim($field.val()); //current hidden field value
		$field.data('default_value', val); //save as default value
		if(val != "") { //if there is a value set, update the label to match
			var $cur = $this.find('a.option[value="' + val + '"]:first');
			if($cur.length > 0) $label.html($cur.html());
		}
		$label.data('default_text', $label.html()); //save the current label text as the default
	});
}
function select_by_letter($select, e){ //select next by letter w/o opening options
	var chr = select_get_character(e);
	if(!chr) return;
	e.preventDefault();
	e.stopPropagation();
		var $ = jQuery, $o = $select.find('a.option'), $label = $select.find('a.label:first'), $field = $select.find('input[type="hidden"]:first');
	//save starting value if not already set
	if(!$select.data('opening_val')) $select.data('opening_val', $field.val());
	//find the current option, if there is one
	var $a = select_current_option($, $select);
	//get the current option's index (or set to search from 0)
	var oi = ($a) ? $o.index($a) : -1;
	//find the next matching option's index
	var ni = select_next_by_letter($, $select, $o, oi, chr);
	if(ni == -1) return;
	//if new match found set field and label values
	var $c = $o.eq(ni);
	var v = $c.attr('value'); 
	if(v == undefined) {
		$label.html($label.data('default_text'));
		$field.val($field.data('default_value'));
	} else {
		$label.html($c.html());
		$field.val(v);
	}
}
function select_goto_letter($a, e){ //just focus next option by first letter
	var chr = select_get_character(e);
	if(!chr) return;
	e.preventDefault();
	e.stopPropagation();
	var $ = jQuery, $select = $a.parents('div.select:first'), $o = $select.find('a.option'), $label = $select.find('a.label:first'), $field = $select.find('input[type="hidden"]:first');
	//make sure this select is open
	if(!$select.hasClass('select_open')) select_open($select);
	//find the next matching option's index
	var ni = select_next_by_letter($, $select, $o, $o.index($a), chr);
	if(ni == -1) return;
	var $c = $o.eq(ni);
	$c.focus();
	$a.blur();
}
function select_next_by_letter($, $select, $o, oi, chr){
	var ca = select_chr_array($, $select, $o);
	//first try for the options after the current option
	for(var i = oi+1; i < ca.length; ++i) if(chr == ca[i]) return i;
	//if none after, start at the beginning of the list
	for(var i = 0; i < oi; ++i) if(chr == ca[i]) return i;
	return -1; //if not found or current index is only option
}
function select_chr_array($, $select, $o) {
	var ca = $select.data('chr_array'), nca = [];
	if(ca) return ca;
	$o.each(function(){nca.push($.trim($(this).text()).charAt(0).toLowerCase())});
	$select.data('chr_array', nca);
	return nca;
}
function select_get_character(e){
	var chr = (String.fromCharCode(e.keyCode)).toLowerCase();
	if(chr.match(/[a-z0-9]/)) return chr;
	return false;
}
function select_current_option($, $select) {
	//return selected option (or first one, if none selected)
	var $cur = null, val = $select.find('input[type="hidden"]:first').val();
	if($.trim(val) != "") $cur = $select.find('a.option[value="' + val + '"]:first');
	if(!$cur || $cur.length == 0) return false;
	return $cur;
}
function select_open($select) {
	var $ = jQuery, $field = $select.find('input[type="hidden"]:first');
	//store starting value to determine onchange firing
	$select.data('opening_val', $field.val());
	//store parent's starting z-index
	var $p = $select.offsetParent();
	$select.data('pz', $p.css('z-index'));
	$p.css('z-index','50');
	//open select
	$select.addClass('select_open');
	//focus current option
	var $cur = select_current_option($, $select);
	if(!$cur) $cur = $select.find('a.option:first');
	$cur.focus();
}
function select_close($select) {
	var $ = jQuery, $field = $select.find('input[type="hidden"]:first');
	//close select
	$select.removeClass('select_open');
	//remove any active classes
	$select.find('a.option_focus').removeClass('option_focus');
	$select.find('div.options_hover').removeClass('options_hover');
	$select.find('a.label_focus').removeClass('label_focus');
	//reset parent's z-index
	$select.offsetParent().css('z-index', $select.data('pz'));
	//if the value has changed, fire change event
	if($field.val() != $select.data('opening_val')) $field.change();
	$select.removeData('opening_val');
}
function select_clear(sel, label) {
	//sel is anything you can pass to $() that will target the appropriate .select menu(s) -- including
	//a selector(text), an HTML element, an array of HTML elements, and previously populated jQuery objects
		//you may reset multiple select menus at once, if desired
	//label (text) is optional -- use if the select's value was pre-populated or if this select is being repurposed
		//not recommended for use when multiple selects are cleared at once
	var $select = $(sel);
	$select.find('input[type="hidden"]').each(function(){$(this).val("")});
	$select.find('a.label').each(function(){
		if(label) $(this).html(label);
		else $(this).html($(this).data('default_text'));
	});
}
/**** events ****/
function select_label_click(e){
	e.preventDefault();
	e.stopPropagation();
	var $ = jQuery, $this = $(this), $select = $this.parents('div.select:first');
	//close any currently open selects
	$('div.select_open').not($select[0]).each(function(){select_close($(this))});
	//if it was open, then close it and return focus to the label, otherwise open it
	if($select.hasClass('select_open')) {
		select_close($select);
		$this.focus();
	} else {
		select_open($select);
	}
}
function select_label_keydown(e){
	var $this = jQuery(this);
	if(jQuery.inArray(e.keyCode,[13,32,37,38,39,40]) > -1) {
		e.preventDefault();
		e.stopPropagation();
		$this.click();
		return;
	}
	select_by_letter($this.parents('div.select:first'), e);
}
function select_label_blur(){
	var $ = jQuery, $this = $(this), $select = $this.parents('div.select:first'), $field = $select.find('input[type="hidden"]:first'), sval = $select.data('opening_val');
	$(this).removeClass('label_focus');
	if(sval && sval != $field.val()) $field.change();
	$select.removeData('opening_val');
}
function select_option_click(e){
	e.preventDefault();
	e.stopPropagation();
	var $ = jQuery, $this = $(this), $select = $this.parents('div.select:first'), $label = $select.find('a.label:first'), $field = $select.find('input[type="hidden"]:first');
	var v = $this.attr('value'); 
	if(v == undefined) {
		$label.html($label.data('default_text'));
		$field.val($field.data('default_value'));
	} else {
		$label.html($this.html());
		$field.val(v);
	}
	select_close($select);
	$this.blur();
	$label.focus();
}
function select_option_keydown(e){
	var k = e.keyCode;
	var $this = jQuery(this);
	if(jQuery.inArray(k,[9,13,27,32,37,38,39,40]) > -1) {
		e.preventDefault();
		e.stopPropagation();
		if(k == 13 || k == 32) { //enter or space
			$this.click();
		} else if((k == 9 && e.shiftKey) || k == 37 || k == 38) { //shift+tab, left or up arrow
			var $a = $this.prev();
			if($a.length > 0) $a.focus();
			else $this.parents('div.select:first').find('a.label').click();
		} else if((k == 9 && !e.shiftKey) || k == 39 || k == 40) { //tab, right or down arrow
			var $a = $this.next();
			if($a.length > 0) $a.focus();
			else $this.parents('div.select:first').find('a.label').click();
		} else if(k == 27) {
			$this.parents('div.select:first').find('a.label').click();
		}
		$this.blur();
		return;
	}
	select_goto_letter($this, e);
}
function select_form_reset($form) {
	var $ = jQuery;
	$form.find('div.select').each(function(){
		var $this = $(this), $label = $this.find('a.label:first'), $field = $this.find('input[type="hidden"]:first');
		$label.html($label.data('default_text'));
		$field.val($field.data('default_value'));
	});
}
