/*
 * JQuery MultiSelect
 *
 * Copyright 2008--2010 A Beautiful Site, LLC.
 * Copyright 2011--2012 Robert Katzki <robert@katzki.de>
 *
 * This plugin is dual-licensed under the GNU General Public License and the MIT License
 *
 * Based on the work of A Beautiful site. <http://www.abeautifulsite.net/blog/2008/04/jquery-multiselect/>
 *
 * Changes:
 *     - Made compatible with jQuery 1.6+
 *     - Auto-Hiding when mouse moves away and stays away
 *
 */
if(jQuery) (function($){
	
	// render the html for a single option
	function renderOption(id, option) {
		var html = '<label><input type="checkbox" name="' + id + '[]" value="' + option.value + '"';
		if( option.selected ){
			html += ' checked="checked"';
		}
		html += ' />' + option.text + '</label>';
		
		return html;
	}
	
	// render the html for the options/optgroups
	function renderOptions(id, options, o) {
		var html = "";
		
		for(var i = 0; i < options.length; i++) {
			if(options[i].optgroup) {
				html += '<label class="optGroup">';
				
				if(o.optGroupSelectable) {
					html += '<input type="checkbox" class="optGroup" />' + options[i].optgroup;
				}
				else {
					html += options[i].optgroup;
				}
				
				html += '</label><div class="optGroupContainer">';
				
				html += renderOptions(id, options[i].options, o);
				
				html += '</div>';
			}
			else {
				html += renderOption(id, options[i]);
			}
		}
		
		return html;
	}
	
	// Building the actual options
	function buildOptions(options) {
		var multiselect = $(this);
		var multiselectOptions = multiselect.next('.multiSelectOptions');
		var o = multiselect.data("config");
		var callback = multiselect.data("callback");

		// clear the existing options
		multiselectOptions.html("");
		var html = "";

		// if we should have a select all option then add it
		if( o.selectAll ) {
			html += '<label class="selectAll"><input type="checkbox" class="selectAll" />' + o.selectAllText + '</label>';
		}

		// generate the html for the new options
		html += renderOptions(multiselect.attr('id'), options, o);
		
		multiselectOptions.html(html);
		
		// variables needed to account for width changes due to a scrollbar
		var initialWidth = multiselectOptions.width();
		var hasScrollbar = false;
		
		// set the height of the dropdown options
		if(multiselectOptions.height() > o.listHeight) {
			multiselectOptions.css("height", o.listHeight + 'px');
			hasScrollbar = true;
		} else {
			multiselectOptions.css("height", '');
		}
		
		// if the there is a scrollbar and the browser did not already handle adjusting the width (i.e. Firefox) then we will need to manaually add the scrollbar width
		var scrollbarWidth = hasScrollbar && (initialWidth == multiselectOptions.width()) ? 17 : 0;

		// set the width of the dropdown options
		if((multiselectOptions.width() + scrollbarWidth) < multiselect.outerWidth()) {
			multiselectOptions.css("width", multiselect.outerWidth() - 2/*border*/ + 'px');
		} else {
			multiselectOptions.css("width", (multiselectOptions.width() + scrollbarWidth) + 'px');
		}
		
		// Handle selectAll oncheck
		if(o.selectAll) {
			multiselectOptions.find('INPUT.selectAll').click( function() {
				// update all the child checkboxes
				multiselectOptions.find('INPUT:checkbox').prop('checked', $(this).prop('checked'));
				multiselectOptions.find("LABEL").toggleClass('checked', $(this).prop('checked'));
			});
		}
		
		// Handle OptGroup oncheck
		if(o.optGroupSelectable) {
			multiselectOptions.addClass('optGroupHasCheckboxes');
		
			multiselectOptions.find('INPUT.optGroup').click( function() {
				// update all the child checkboxes
				$(this).parent().next().find('INPUT:checkbox').prop('checked', $(this).prop('checked'));
				$(this).parent().next().find("LABEL").toggleClass('checked', $(this).prop('checked'));
			});
		}
		
		// Handle all checkboxes
		multiselectOptions.find('INPUT:checkbox').click( function() {
			// set the label checked class
			$(this).parent("LABEL").toggleClass('checked', $(this).prop('checked'));
			
			updateSelected.call(multiselect);
			multiselect.focus();
			if($(this).parent().parent().hasClass('optGroupContainer')) {
				updateOptGroup.call(multiselect, $(this).parent().parent().prev());
			}
			if( callback ) {
				callback($(this));
			}
		});
		
		// Initial display
		multiselectOptions.each( function() {
			$(this).find('INPUT:checked').parent().addClass('checked');
		});
		
		// Initialize selected and select all
		updateSelected.call(multiselect);
		
		// Initialize optgroups
		if(o.optGroupSelectable) {
			multiselectOptions.find('LABEL.optGroup').each( function() {
				updateOptGroup.call(multiselect, $(this));
			});
		}
		
		// Handle hovers
		multiselectOptions.find('LABEL:has(INPUT)').hover( function() {
			$(this).parent().find('LABEL').removeClass('hover');
			$(this).addClass('hover');
		}, function() {
			$(this).parent().find('LABEL').removeClass('hover');
		});
		
		// Keyboard
		multiselect.keydown( function(e) {
		
			var multiselectOptions = $(this).next('.multiSelectOptions');

			// Is dropdown visible?
			if( multiselectOptions.css('visibility') != 'hidden' ) {
				// Dropdown is visible
				// Tab
				if( e.keyCode == 9 ) {
					$(this).addClass('focus').trigger('click'); // esc, left, right - hide
					$(this).focus().next(':input').focus();
					return true;
				}
				
				// ESC, Left, Right
				if( e.keyCode == 27 || e.keyCode == 37 || e.keyCode == 39 ) {
					// Hide dropdown
					$(this).addClass('focus').trigger('click');
				}
				// Down || Up
				if( e.keyCode == 40 || e.keyCode == 38) {
					var allOptions = multiselectOptions.find('LABEL');
					var oldHoverIndex = allOptions.index(allOptions.filter('.hover'));
					var newHoverIndex = -1;
					
					// if there is no current highlighted item then highlight the first item
					if(oldHoverIndex < 0) {
						// Default to first item
						multiselectOptions.find('LABEL:first').addClass('hover');
					}
					// else if we are moving down and there is a next item then move
					else if(e.keyCode == 40 && oldHoverIndex < allOptions.length - 1)
					{
						newHoverIndex = oldHoverIndex + 1;
					}
					// else if we are moving up and there is a prev item then move
					else if(e.keyCode == 38 && oldHoverIndex > 0)
					{
						newHoverIndex = oldHoverIndex - 1;
					}

					if(newHoverIndex >= 0) {
						$(allOptions.get(oldHoverIndex)).removeClass('hover'); // remove the current highlight
						$(allOptions.get(newHoverIndex)).addClass('hover'); // add the new highlight
						
						// Adjust the viewport if necessary
						adjustViewPort(multiselectOptions);
					}
					
					return false;
				}

				// Enter, Space
				if( e.keyCode == 13 || e.keyCode == 32 ) {
					var selectedCheckbox = multiselectOptions.find('LABEL.hover INPUT:checkbox');
					
					// Set the checkbox (and label class)
					selectedCheckbox.attr('checked', !selectedCheckbox.attr('checked')).parent("LABEL").toggleClass('checked', selectedCheckbox.attr('checked'));
					
					// if the checkbox was the select all then set all the checkboxes
					if(selectedCheckbox.hasClass("selectAll")) {
						multiselectOptions.find('INPUT:checkbox').prop('checked', selectedCheckbox.prop('checked'));
						multiselectOptions.find("LABEL").toggleClass('checked', selectedCheckbox.prop('checked'));
					}

					updateSelected.call(multiselect);
					
					if( callback ) callback($(this));
					return false;
				}

				// Any other standard keyboard character (try and match the first character of an option)
				if( e.keyCode >= 33 && e.keyCode <= 126 ) {
					// find the next matching item after the current hovered item
					var match = multiselectOptions.find('LABEL:startsWith(' + String.fromCharCode(e.keyCode) + ')');
					
					var currentHoverIndex = match.index(match.filter('LABEL.hover'));
					
					// filter the set to any items after the current hovered item
					var afterHoverMatch = match.filter(function (index) {
						return index > currentHoverIndex;
					});

					// if there were no item after the current hovered item then try using the full search results (filtered to the first one)
					match = (afterHoverMatch.length >= 1 ? afterHoverMatch : match).filter("LABEL:first");

					if(match.length == 1) {
						// if we found a match then move the hover
						multiselectOptions.find('LABEL.hover').removeClass('hover');
						match.addClass('hover');
						
						adjustViewPort(multiselectOptions);
					}
				}
			} else {
				// Dropdown is not visible
				if( e.keyCode == 38 || e.keyCode == 40 || e.keyCode == 13 || e.keyCode == 32 ) { //up, down, enter, space - show
					// Show dropdown
					$(this).removeClass('focus').trigger('click');
					multiselectOptions.find('LABEL:first').addClass('hover');
					return false;
				}
				//  Tab key
				if( e.keyCode == 9 ) {
					// Shift focus to next INPUT element on page
					multiselectOptions.next(':input').focus();
					return true;
				}
			}
			// Prevent enter key from submitting form
			if( e.keyCode == 13 ) return false;
		});
	}
	
	// Adjust the viewport if necessary
	function adjustViewPort(multiselectOptions) {
		// check for and move down
		var selectionBottom = multiselectOptions.find('LABEL.hover').position().top + multiselectOptions.find('LABEL.hover').outerHeight();
		
		if(selectionBottom > multiselectOptions.innerHeight()){
			multiselectOptions.scrollTop(multiselectOptions.scrollTop() + selectionBottom - multiselectOptions.innerHeight());
		}
		
		// check for and move up
		if(multiselectOptions.find('LABEL.hover').position().top < 0){
			multiselectOptions.scrollTop(multiselectOptions.scrollTop() + multiselectOptions.find('LABEL.hover').position().top);
		}
	}
	
	// Update the optgroup checked status
	function updateOptGroup(optGroup) {
		var multiselect = $(this);
		var o = multiselect.data("config");
		
		// Determine if the optgroup should be checked
		if(o.optGroupSelectable) {
			var optGroupSelected = true;
			$(optGroup).next().find('INPUT:checkbox').each( function() {
				if( !$(this).prop('checked') ) {
					optGroupSelected = false;
					return false;
				}
			});
			
			$(optGroup).find('INPUT.optGroup').prop('checked', optGroupSelected);
			$(optGroup).find("LABEL").toggleClass('checked', optGroupSelected);
		}
	}
	
	// Update the textbox with the total number of selected items, and determine select all
	function updateSelected() {
		var multiselect = $(this);
		var multiselectOptions = multiselect.next('.multiSelectOptions');
		var o = multiselect.data("config");
		
		var i = 0;
		var selectAll = true;
		var display = '';
		multiselectOptions.find('INPUT:checkbox').not('.selectAll, .optGroup').each( function() {
			if( $(this).prop('checked') ) {
				i++;
				display = display + $(this).parent().text() + ', ';
			}
			else selectAll = false;
		});
		
		// trim any end comma and surounding whitespace
		display = display.replace(/\s*\,\s*$/,'');
		
		if( i === 0 ) {
			multiselect.find("span").html( o.noneSelected );
		} else {
			if( o.oneOrMoreSelected == '*' ) {
				multiselect.find("span").html( display );
				multiselect.attr( "title", display );
			} else {
				multiselect.find("span").html( o.oneOrMoreSelected.replace('%', i) );
			}
		}

		// Determine if Select All should be checked
		if(o.selectAll) {
			multiselectOptions.find('INPUT.selectAll').attr('checked', selectAll).parent("LABEL").toggleClass('checked', selectAll);
		}
	}
	
	$.extend($.fn, {
		multiSelect: function(o, callback) {
			// Default options
			if( !o ) o = {};
			if( o.selectAll === undefined ) o.selectAll = true;
			if( o.selectAllText === undefined ) o.selectAllText = "Select All";
			if( o.noneSelected === undefined ) o.noneSelected = 'Select options';
			if( o.oneOrMoreSelected === undefined ) o.oneOrMoreSelected = '% selected';
			if( o.optGroupSelectable === undefined ) o.optGroupSelectable = false;
			if( o.listHeight === undefined ) o.listHeight = 150;

			// Initialize each multiSelect
			$(this).each( function() {
				var select = $(this);
				var html = '<a href="javascript:;" class="multiSelect"><span></span></a>';
				html += '<div class="multiSelectOptions" style="position: absolute; z-index: 99999; visibility: hidden;"></div>';
				$(select).after(html);
				
				var multiselect = $(select).next('.multiSelect');
				var multiselectOptions = multiselect.next('.multiSelectOptions');
				
				// give the multiSelect the width of the parent element
				multiselect.css("width", ($(select).parent().width() - multiselect.outerWidth()) + 'px');
				
				// Attach the config options to the multiselect
				multiselect.data("config", o);
				
				// Attach the callback to the multiselect
				multiselect.data("callback", callback);
				
				// Serialize the select options into json options
				var options = [];
				$(select).children().each( function() {
					if(this.tagName.toUpperCase() == 'OPTGROUP')
					{
						var suboptions = [];
						options.push({optgroup: $(this).attr('label'), options: suboptions});
						
						$(this).children('OPTION').each( function() {
							if( $(this).val() !== '' ) {
								suboptions.push({text: $(this).html(), value: $(this).val(), selected: $(this).prop('selected')});
							}
						});
					}
					else if(this.tagName.toUpperCase() == 'OPTION')
					{
						if( $(this).val() !== '' ) {
							options.push({text: $(this).html(), value: $(this).val(), selected: $(this).prop('selected')});
						}
					}
				});
				
				// Eliminate the original form element
				$(select).remove();
				
				// Add the id that was on the original select element to the new input
				multiselect.attr("id", $(select).attr("id"));
				
				// Build the dropdown options
				buildOptions.call(multiselect, options);

				// Events
				multiselect.hover( function() {
					$(this).addClass('hover');
				}, function() {
					$(this).removeClass('hover');
				}).click( function() {
					// Show/hide on click
					if( $(this).hasClass('active') ) {
						$(this).multiSelectOptionsHide();
					} else {
						$(this).multiSelectOptionsShow();
					}
					return false;
				}).focus( function() {
					// So it can be styled with CSS
					$(this).addClass('focus');
				}).blur( function() {
					// So it can be styled with CSS
					$(this).removeClass('focus');
				});
				
				// Hide the Options when mouse moves away and stays away
				multiselectOptions.hover(
					// Mouseover-Event deletes timer
					function() {
						try{
							clearTimeout(hover_intent);
						} catch(e) { }
						// Give class for hover
						multiselectOptions.addClass('hover');
					},
					// Mouseout-Event sets Timer and removes hover-class
					function() {
						hover_intent = setTimeout( function() {
							if ($(this).not('.hover')) multiselect.multiSelectOptionsHide();
						} , 1000 );
						multiselectOptions.removeClass('hover');
					}
				);
					
				// Add an event listener to the window to close the multiselect if the user clicks off
				$(document).click( function(event) {
					// If somewhere outside of the multiselect was clicked then hide the multiselect
					if(!($(event.target).parents().andSelf().is('.multiSelectOptions'))){
						multiselect.multiSelectOptionsHide();
					}
				});
			});
		},
		
		// Update the dropdown options
		multiSelectOptionsUpdate: function(options) {
			buildOptions.call($(this), options);
		},
		
		// Hide the dropdown
		multiSelectOptionsHide: function() {
			$(this).removeClass('active').removeClass('hover').next('.multiSelectOptions').css('visibility', 'hidden');
		},
		
		// Show the dropdown
		multiSelectOptionsShow: function() {
			var multiselect = $(this);
			var multiselectOptions = multiselect.next('.multiSelectOptions');
			var o = multiselect.data("config");
		
			// Hide any open option boxes
			$('.multiSelect').multiSelectOptionsHide();
			multiselectOptions.find('LABEL').removeClass('hover');
			multiselect.addClass('active').next('.multiSelectOptions').css('visibility', 'visible');
			multiselect.focus();
			
			// reset the scroll to the top
			multiselect.next('.multiSelectOptions').scrollTop(0);

			// Position it
			var offset = multiselect.position();
			multiselect.next('.multiSelectOptions').css({top:  offset.top + $(this).outerHeight() + 'px'});
			multiselect.next('.multiSelectOptions').css({left: offset.left + 'px'});
		},
		
		// Reset the MultiSelect and then set the values
		multiSelectOptionsSetValues: function(values) {
			var multiselect = $(this);
			var multiselectOptions = multiselect.next('.multiSelectOptions');
			
			multiselectOptions.find('label.checked').removeClass('checked').children('input:checked').prop('checked', false);
			
			$.each(values, function(i, value) {
				$('label:not(selectAll) input', multiselectOptions).each(function () {
					if ($(this).val() == value) {
						$(this).prop('checked', true);
						$(this).parent('label').addClass('checked');
					}
				});
			});
			
			updateSelected.call(multiselect);
		},
		
		// get a coma-delimited list of selected values
		selectedValuesString: function() {
			var selectedValues = "";
			$(this).next('.multiSelectOptions').find('INPUT:checkbox:checked').not('.optGroup, .selectAll').each(function() {
				selectedValues += $(this).attr('value') + ",";
			});
			// trim any end comma and surounding whitespace
			return selectedValues.replace(/\s*\,\s*$/,'');
		}
	});
	
	// add a new ":startsWith" search filter
	$.expr[":"].startsWith = function(el, i, m) {
		var search = m[3];
		if (!search) return false;
		return eval("/^[/s]*" + search + "/i").test($(el).text());
	};
	
})(jQuery);
