/**
 * JQuery Searchable DropDown Plugin
 * 
 * @required jQuery 1.3.x
 * @author Sascha Wolski <hagman@gmx.de>
 * $Id: jquery.searchabledropdown.js 34 2009-12-08 21:37:57Z xhaggi $
 * 
 * Based up on the AddIncSearch plugin published by Tobias Oetiker
 * http://plugins.jquery.com/project/AddIncSearch
 * 
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
(function($) {
	
	// register plugin
	var plugin = register("searchable");
	
    // defaults
	plugin.defaults = {
        maxListSize: 100,
        maxMultiMatch: 50,
        exactMatch: false,
        wildcards: true,
        ignoreCase: true,
        warnMultiMatch: "top {0} matches ...",
        warnNoMatch: "no matches ...",
        latency: 200,
        observing: true,
        observingInterval: 200,
        zIndex: "auto"
    };
    
	/**
	 * Execute function
	 * element-specific code here
	 * @param {Options} settings Settings
	 */
	plugin.execute = function(settings){
		
		// do not attach on IE6 or lower
		if ($.browser.msie && parseInt(jQuery.browser.version) < 7)
			return this;
        	
    	// only active select elements with drop down capability
        if (this.nodeName != "SELECT" || this.size > 1) 
            return this;
                    
        var $this = $(this);
       
        var storage = {index: -1, options: null}; // holds data for restoring
        
        // detecting chrome
        $.browser.chrome = /chrome/.test(navigator.userAgent.toLowerCase());
        if($.browser.chrome) $.browser.safari = false; 
                    
        // lets you override the options
        // inside the dom objects class property
        // requires the jQuery metadata plugin
        // <div class="hello {color: 'red'}">ddd</div>
        if ($.meta){
            settings = $.extend({}, options, $this.data());
        }
                                
        // fix some styles
        $this.css("text-decoration", "none");
        $this.width($this.outerWidth());
        $this.height($this.outerHeight());
                
        // store the current selectedIndex of the source select element
        $this.data("index", new Number($this.get(0).selectedIndex));
                    
        // matching option items
        var topMatchItem = $("<option>"+settings.warnMultiMatch.replace(/\{0\}/g, settings.maxMultiMatch)+"</option>").attr("disabled", "true");
        var noMatchItem = $("<option>"+settings.warnNoMatch+"</option>").attr("disabled", "true");
       
        // overlay div to block events of source select element
        var $blocker = $("<div/>");
        $blocker.attr( "id", "searchable-blocker" );
        $blocker.css({
            "position": "absolute",
            "width":  $this.outerWidth(),
            "height": $this.outerHeight(),
            "background-color": "#FFFFFF",
            "opacity": "0.01"
        });
        
        // overlay text field for searching capability
        var $input = $("<input/>");
        $input.attr("type", "text");
        $input.hide();
        
        // default styles for text field
        $input.css({
            "position": "absolute",
            "margin": 0,
            "padding": 0,
            "outline-style": "none",
            "border-style": "solid",
            "border-bottom-style": "none",
            "border-color": "transparent",
            "background-color": "transparent"
        });
        
        // adjust width + height of input
        $input.width($this.innerWidth() - 30);
        $input.height($this.outerHeight());
        
        // copy selected styles to text field
		var sty = new Array();
		sty.push("border-left-width");
		sty.push("border-top-width");
		//sty.push("font-family");
		sty.push("font-size");
		sty.push("font-stretch");
		sty.push("font-variant");
		sty.push("font-weight");
		sty.push("color");
		sty.push("text-align");
		sty.push("text-indent");
		sty.push("text-shadow");
		sty.push("text-transform");
		sty.push("padding-left");
		sty.push("padding-top");
		for(var i=0; i < sty.length;i++)
			$input.css(sty[i], $this.css(sty[i]));
	
		// adjust search text field 
		// IE7
		if($.browser.msie && parseInt(jQuery.browser.version) < 8) {
			$input.css("padding", "0px");
			$input.css("padding-left", "3px");
			$input.css("border-left-width", "2px");
			$input.css("border-top-width", "3px");			
		}
		// chrome
		else if($.browser.chrome) {
			$input.height($this.innerHeight());
			$input.css("text-transform", "none");
			$input.css("padding-left", parseInt(/([0-9]+)px/.exec($input.css("padding-left"))[1])+3);
			$input.css("padding-top", 2);
		}
		// safari
		else if($.browser.safari) {
			$input.height($this.innerHeight());
			$input.css("padding-top", 2);
			$input.css("padding-left", 3);
			$input.css("text-transform", "none");
		}
		// opera
		else if($.browser.opera) {
			$input.height($this.innerHeight());
			var pl = parseInt(/([0-9]+)px/.exec($this.css("padding-left"))[1]);
			$input.css("padding-left", pl == 1 ? pl+1 : pl);
			$input.css("padding-top", 0);
		}
		// all other browsers
		else {
			$input.css("padding-left", parseInt(/([0-9]+)px/.exec($this.css("padding-left"))[1])+3);
			$input.css("padding-top", parseInt(/([0-9]+)px/.exec($this.css("padding-top"))[1])+1);
		}     
		
		// store css width of source select object then set width 
		// to auto to obtain the maximum width depends on the longest entry.
		// this is nessesary to set the width of the chooser, because min-width
		// do not work in all browser.
        var w = $this.css("width");
        var ow = $this.outerWidth();
        $this.css("width", "auto");
        var ow = ow > $this.outerWidth() ? ow : $this.outerWidth();
        $this.css("width", w);
        
        // entries chooser replacement
        var $chooser = $("<select />");
        $chooser.attr("size", Math.min($this.get(0).length, 20));
        $chooser.hide();
        $chooser.css({
            "position": "absolute",
            "width": ow,
            "border": "1px solid #333",
            "font-weight": "normal",
            "padding": 0,
            "background-color": $this.css("background-color"),
            "text-transform": $this.css("text-transform")
        });
        
        
                    
        // z-index handling
        var zIndex = /^\d+$/.test($this.css("z-index")) ? $this.css("z-index") : 1;
        // if z-index option is defined, use it instead of select element z-index
        if (settings.zIndex && /^\d+$/.test(settings.zIndex))
        	zIndex = settings.zIndex;
        $blocker.css("z-index", (zIndex).toString(10));
        $input.css("z-index", (zIndex+1).toString(10));
        $chooser.css("z-index", (zIndex+2).toString(10));
        
        /**
         * Positioning
         */
        function position() {
            var offset = $this.offset();
            offset.top = 0;
            offset.left = 0;
            $blocker.css({
                top: offset.top,
                left: offset.left
            });
            $input.css({
                top: offset.top,
                left: offset.left
            });
            $chooser.css({
                top: offset.top + $this.outerHeight(),
                left: offset.left
            });
        };
        
        // fix positioning on window resize
        $this.resize(position);
        $(window).resize(position);

        // set initial position
        position();
        
        // load default
        loadDefaults();
        
        // append after source select element
        $this.after($blocker);
        $this.after($input);
        $this.after($chooser);
        
        /*
         * EVENT HANDLING
         */            
        var suspendBlur = false;
        $($blocker).mouseover(function() {
        	suspendBlur = true;
        });
        $($blocker).mouseout(function() {
        	suspendBlur = false;
        });
        $($chooser).mouseover(function() {
        	suspendBlur = true;
        });
        $($chooser).mouseout(function() {
        	suspendBlur = false;
        });
        
        // click event of search field
        $input.click(function(e) {
        	if(!enabled) {
    			enable(e, true);
        	}
    		else {
    			disable(e, true);
    		}
        });
        
        // keydown on select element
        $this.keydown(function(e) {
        	if(e.keyCode != 9)
        		enable(e, false, true);
        });
        
        // click event on select element
        $this.click(function(e) {
        	$chooser.focus();
        });
        
        // chooser click event
        $chooser.click(function(e) {
            if ($chooser.get(0).selectedIndex < 0) return;
            synchronize();
            disable(e);
        });
        
        // chooser focus event
        $chooser.focus(function(e) {
        	$input.focus();
        });
        
        // chooser blur event
        $chooser.blur(function(e) {
        	if(!suspendBlur)
        		disable(e, true);
        });
        
        // chooser mousemove event 
        $chooser.mousemove(function(e) {
        	// Disabled on opera because of <select> elements always return scrollTop of 0
        	// Affects up to Opera 10 beta 1, can be removed if bug is fixed
            // http://www.greywyvern.com/code/opera/bugs/selectScrollTop
        	if($.browser.opera && parseFloat(jQuery.browser.version) >= 9.8)
        		return true;
        	
        	// get font-size of option
        	var fs = Math.floor(parseFloat(/([0-9\.]+)px/.exec($($chooser.get(0).options[0]).css("font-size"))));
        	// calc line height depends on browser
        	var fsdiff = 4;
        	if($.browser.opera)
        		fsdiff = 2.5; 
        	if($.browser.safari || $.browser.chrome)
        		fsdiff = 3;
        	fs += Math.round(fs / fsdiff);
        	// set selectedIndex depends on mouse position and line height
        	$chooser.get(0).selectedIndex = Math.floor((e.pageY - this.offsetTop + this.scrollTop) / fs);
        });
        
        // toggle click event on $blocker div
        var enabled = false;
        $blocker.click(function(e) {
    		if(!enabled) {
    			enable(e, true);
    		}
    		else {
    			disable(e, true);
    		}
        });
        
        // trigger event keyup
        $input.keyup(function(e) {
        	            	
        	// break searching while using navigation keys
        	if(jQuery.inArray(e.keyCode, new Array(9, 13, 16, 33, 34, 35, 36, 38, 40)) > -1)
        		return true;
        	
        	// set search text
        	search = $.trim($input.val().toLowerCase());
        	
        	// if a previous timer is running, stop it
            if (timer != null)
            	clearTimeout(timer);
            
            // start new timer      
            timer = setTimeout(searching, settings.latency);
        });
                    
        // trigger keydown event for keyboard usage
        $input.keydown(function(e) {
        	
        	// return on shift, ctrl, alt key mode
        	if(e.shiftKey || e.ctrlKey || e.altKey) 
        		return;
        	                        	
        	// which key is pressed
            switch(e.keyCode) {
                case 9:	// tab
                	disable(e);
                	moveTab($this, e.shiftKey ? -1 : 1);
                    break;
                case 13:  // enter
                	disable(e);
                	$this.focus();
                    break;
                case 27: // escape
					disable(e, true);
					$this.focus();
                	break;
                case 33: // pgup
                    if ($chooser.get(0).selectedIndex - $chooser.get(0).size > 0){
                        $chooser.get(0).selectedIndex -= $chooser.get(0).size;
                    } 
                    else {
					    $chooser.get(0).selectedIndex = 0;
				    }
                    synchronize();
                    break;
                case 34: // pgdown
                    if ($chooser.get(0).selectedIndex + $chooser.get(0).size < $chooser.get(0).options.length - 1){
                        $chooser.get(0).selectedIndex += $chooser.get(0).size;
                    } 
                    else {
					    $chooser.get(0).selectedIndex = $chooser.get(0).options.length-1;
				    }
                    synchronize();
                    break;
//	                case 35: // end
//	                	$chooser.get(0).selectedIndex = $chooser.get(0).length - 1;
//	                	synchronize();
//	                	break;
//	                case 36: // pos1
//	                	$chooser.get(0).selectedIndex = 0;
//	                	synchronize();
//	                	break;
                case 38: // up
                    if ($chooser.get(0).selectedIndex > 0){
                        $chooser.get(0).selectedIndex--;
                        synchronize();
                    }
                    break;
                case 40: // down
                    if ($chooser.get(0).selectedIndex < $chooser.get(0).options.length - 1){
                        $chooser.get(0).selectedIndex++;
                        synchronize();
                    }
                    break;
                default:
                    return true;
            }
            
            // we handled the key.stop
            // doing anything with it!
            return false;
        });
        
        
        /**
         * Enable the search facilities
         * 
         * @param {Object} e Event
		 * @param {boolean} sc Show chooser
         * @param {boolean} v Verbose enabling
         */
        function enable(e, sc, v) {			
    		// exit event on disabled select element
    		if($this.attr("disabled")) 
    			return false;
    		
    		// set state to enabled
    		if(typeof v == "undefined")
    			enabled = !enabled;
    		
    		// synchronize select and dropdown replacement
    		synchronize();
    		
    		// store search result
        	store();
        	
        	// store current text of selected entry then clear it
        	var cst = $this.find(":selected");
        	$this.data("text", cst.text());
        	cst.text("");
        	    		
    		// show search field
    		$input.show();
            $input.focus();
            $input.select();
            
        	// show chooser
        	if(sc)
        		$chooser.show();
        	
        	if(typeof e != "undefined")
        		e.stopPropagation();
        };
        
        /**
         * Disable the search facilities
         * 
         * @param {Object} e Event
		 * @param {boolean} rs Restore last results
         */
        function disable(e, rs) {
        	
        	// set state to enabled
        	enabled = false;
			
			// clear running timer
            if(timer != null)
            	clearTimeout(timer);
            
            // restore text of selected entry
            $this.find(":selected").text($this.data("text"));
            
			// hide search field and chooser
			$input.hide();
        	$chooser.hide();
        				
			// restore last results
			if(typeof rs != "undefined")
				restore();
			
			// populate changes
			if(typeof rs == "undefined")
				populate();
            
            if(typeof e != "undefined")
            	e.stopPropagation();                
        };
        
        /**
         * Populate changes to select element
         */
        function populate() {
        	// disbale observing before change the selectedIndex
        	observing = false;
							            	
        	// invalid selectedIndex or disabled elements do not be populate
        	if($chooser.get(0).selectedIndex < 0 || $chooser.get(0).options[$chooser.get(0).selectedIndex].disabled) {
        		// enable observing
        		observing = true;
        		return;
        	}
        		
    		$this.get(0).selectedIndex = parseInt($($chooser.get(0).options[$chooser.get(0).selectedIndex]).attr("lang"));
        	
        	// trigger change event
        	$this.change();
        	
        	// store selectedIndex
        	$this.data("index", new Number($this.get(0).selectedIndex));
        	
        	// enable observing
        	observing = true;
        };
        
        
		// observing interval logic
		var observing = true;
		if(settings.observing) {
			var obsInterval = window.setInterval(observe, settings.observingInterval);
		}
		
		/**
         * Observing changes on the selectedIndex of soruce select element
         */
        function observe() {
        	if(observing && $this.get(0).selectedIndex != $this.data("index")) {
        		$this.data("index", new Number($this.get(0).selectedIndex));
            	loadDefaults();
        	}
        };
        
        /**
         * Synchronize selected item on dropdown replacement with source select element
         */
        function synchronize() {
        	if($chooser.get(0).selectedIndex > -1 && !$chooser.get(0).options[$chooser.get(0).selectedIndex].disabled)
        		$input.val($this.get(0).options[parseInt($($chooser.get(0).options[$chooser.get(0).selectedIndex]).attr("lang"))].text);
        	else
        		$input.val($this.get(0).options[$this.get(0).selectedIndex].text);
        };
        
        /**
         * Stores last results of chooser
         */
        function store() {
			storage.index = $chooser.get(0).selectedIndex;
			storage.options = new Array();
        	for(var i=0;i<$chooser.get(0).options.length;i++)
        		storage.options.push($chooser.get(0).options[i]);
        };
        
        /**
         * Restores last results of chooser previously stored by store function
         */
        function restore() {
        	$chooser.empty();
        	$chooser.attr("size", Math.max(2, Math.min(storage.options.length, 20)));
        	for(var i=0;i<storage.options.length;i++)
				$chooser.append(storage.options[i]);
        	$chooser.get(0).selectedIndex = storage.index;
        };
        
        /**
         * Load default entries depends on selectedIndex of the source select element and maxMultiMatch count
         */
        function loadDefaults() {
        	
        	// calc start and length of iteration
            var mc = Math.floor(settings.maxMultiMatch / 2);
            var st = Math.max(0, ($this.get(0).selectedIndex - mc));
            var len = Math.min($this.get(0).length, Math.max(settings.maxMultiMatch, ($this.get(0).selectedIndex + mc)));
            var si = $this.get(0).selectedIndex - st;
            
            // clear chooser select element
            $chooser.empty();
            
            // append options
            for (var i=st; i < len; i++)
            	$chooser.append($($this.get(0).options[i]).clone().attr("lang", i));
            
            // append top match item if length exceeds
            if($this.get(0).length > settings.maxMultiMatch)
            	$chooser.append(topMatchItem);
            
            // set selectedIndex of chooser
            $chooser.get(0).selectedIndex = si;
        };
        
        /**
         * Move tab x steps forward or backward if you set negative values to step
         * @param {jQueryElement} jqe Element
         * @param {int} steps Steps to move tab forward or background if negative value
         * @return {Boolean} true if tab is moved, otherwise false
         * @private
         */
        function moveTab(jqe, steps) {
            var fields = jqe.parents("form,body").eq(0).find("button,input[type!=hidden],textarea,select");
            var index = fields.index(jqe);
            if(index > -1 && index + steps < fields.length && index + steps >= 0) {
            	fields.eq(index + steps).focus();
                return true;
            }
            return false;
        };
        
        /**
         * Escape regular expression string
         * 
         * @param str String
         * @return escaped regexp string
         */
        function escapeRegExp(str) {
        	var specials = ["/", ".", "*", "+", "?", "|", "(", ")", "[", "]", "{", "}", "\\", "^", "$"];
        	var regexp = new RegExp("(\\" + specials.join("|\\") + ")", "g");
        	return str.replace(regexp, "\\$1");
    	};


        var timer = null;
        var searchCache;
        var search;

        /**
         * The actual searching gets done here
         */
        function searching() {
            if (searchCache == search) { // no change ...
                timer = null;
                return;
            }
            
            var matches = 0;
            searchCache = search;
            $chooser.hide();
            $chooser.empty();
            
            // escape regexp characters
            var regexp = escapeRegExp(search);
            // exact match
            if(settings.exactMatch)
            	regexp = "^" + regexp;
            // wildcard support
            if(settings.wildcards) {
            	regexp = regexp.replace(/\\\*/g, ".*");
            	regexp = regexp.replace(/\\\?/g, ".");
            }
            // ignore case sensitive
            var flags;
            if(settings.ignoreCase)
            	flags = "i";
            	
            // RegExp object
            search = new RegExp(regexp, flags);
							
			// for each item in list
            for(var i=0;i<$this.get(0).length && matches < settings.maxMultiMatch;i++){
            	// search
                if(search.length == 0 || search.test($this.get(0).options[i].text)){
                	var opt = $($this.get(0).options[i]).clone().attr("lang", i);
                	if($this.data("index") == i)
                		opt.text($this.data("text"));
                    $chooser.append(opt);
                    matches++;
                }
            }
            
            // result actions
            if(matches >= 1){
                $chooser.get(0).selectedIndex = 0;
            }
            else if(matches == 0){
                $chooser.append(noMatchItem);
            }
            
            // append top match item if matches exceeds maxMultiMatch
            if(matches >= settings.maxMultiMatch){
                $chooser.append(topMatchItem);
            }
            
            // modify size depends on entries
            $chooser.attr("size", Math.max(2, Math.min(matches, 20)));
            
            $chooser.show();
            timer = null;
        };
        
        return;
    };
    
    /**
     * Register plugin under given namespace
     * 
     * Plugin Pattern informations
     * The function creates the namespace under jQuery
     * and bind the function to execute the plugin code.
     * The plugin code goes to the plugin.execute function.
     * The defaults can setup under plugin.defaults.
     * 
     * @param {String} nsp Namespace for the plugin
     * @return {Object} Plugin object
     */
    function register(nsp) {
    	
    	// init plugin namespace
    	var plugin = $[nsp] = {};
    	
    	// bind function to jQuery fn object
    	$.fn[nsp] = function(settings) {    		
    		// extend default settings
    		settings = $.extend(plugin.defaults, settings);
    		
            return this.each(function(){
            	plugin.execute.call(this, settings)
            });
        };
        
        return plugin;
	};
    
})(jQuery);


