/*******************************************************

TagSuggest - a keyboard-driven interface widget for
enhancing tag input.

Version 0.13, released December 25, 2006. The latest version 
can be found here: http://code.appnel.com/tagsuggest

This library is modification of the AutoSuggest library,
Copyright (C) 2005 Joe Kepley, The Sling & Rock Design
Group, Inc. The original library can be found at
http://www.gadgetopia.com/autosuggest/.

TagSuggest modifications are by Timothy Appnel, Appnel
Solutions. Copyright (C) 2005-2006.

This library is free software; you can redistribute it
and/or modify it under the terms of the GNU Lesser General
Public License as published by the Free Software Foundation;
either version 2.1 of the License, or (at your option) any
later version.

This library 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 Lesser General Public License for more
details.

You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA  02111-1307  USA

*******************************************************

Please send any useful modifications or improvements via 
email to info at appnel (dot) com

*******************************************************/

/********************************************************
 The TagSuggest class binds to a text input field
 and creates an automatic suggestion dropdown in the style
 of the "IntelliSense" and "AutoComplete" features of some
 desktop apps coupled with the unique attributes of single word 
 tags.
 
 Parameters: 
 elem: A DOM element for an INPUT TYPE="text" form field
 suggestions: an array of strings to be used as suggestions
              when someone's typing.
 limit: the maximum number of suggestions to display.

 Example usage: 
 
 Please enter some tags.
 <input type="text" id="tags" name="tags" />
 <script language="Javascript">
   var tags=new Array("appnel","ajax","atom","folksonomy","javascript","tags");
   new TagSuggest(document.getElementById("tags",tags));
 </script>

 Requirements: 

 Unfortunately the TagSuggest class doesn't seem to work 
 well with dynamically-created DIVs. So, somewhere in your 
 HTML, you'll need to add this: 
 
 <div id="tagsuggest"><ul></ul></div>

 This library also does not function properly with the Safari browser.
 Any suggestions as to how to fix this are highly appreciated.

 Here's a default set of style rules that you'll also want to 
 add to your CSS: 

     .suggestion_list
    {
        background: white;
        border: 1px solid silver;
        padding: 2px;
        margin-top: 2px;
    }
    
    .suggestion_list ul, li
    {
        margin: 0;
        padding: 1px 5px 1px 1px;
        list-style-type: none;
        line-height: 1em;
        display: inline;
        float: left;
    }
    
    .suggestion_list a
    {
        text-decoration: none;
        color: navy;
    }
    
    .suggestion_list .selected
    {
        background: navy;
        color: white;
    }
    
    .suggestion_list .selected a { color: white; }
    #tagsuggest { display: none; }

*********************************************************/
function TagSuggest(elem, suggestions, limit)
{

	//The 'me' variable allow you to access the AutoSuggest object
	//from the elem's event handlers defined below.
	var me = this;

	//A reference to the element we're binding the list to.
	this.elem = elem;

	this.suggestions = suggestions;

    // cap on number of eligible suggestions
    this.limit = limit; 

	//Arrow to store a subset of eligible suggestions that match the user's input
	this.eligible = new Array();

	//The text input by the user.
	this.inputText = null;

	//A pointer to the index of the highlighted eligible item. -1 means nothing highlighted.
	this.highlighted = -1;

	//A div to use to create the dropdown.
	this.div = document.getElementById("tagsuggest");
    this.div.style.width = elem.offsetWidth + 'px';

    var lastEdit = ''; //addition

	//Do you want to remember what keycode means what? Me neither.
	var TAB = 9;
	var ENTER = 13;
	var ESC = 27;
	var SPACE = 32;
	var END = 35;
	var HOME = 36;
	var KEYLF = 37;
	var KEYUP = 38;
	var KEYRT = 39;
	var KEYDN = 40;
	
	//The browsers' own autocomplete feature can be problematic, since it will 
	//be making suggestions from the users' past input.
	//Setting this attribute should turn it off.
	elem.setAttribute("autocomplete","off");

	//We need to be able to reference the elem by id. If it doesn't have an id, set one.
	if(!elem.id)
	{
		var id = "tagsuggest" + idCounter;
		idCounter++;

		elem.id = id;
	}


	/********************************************************
	onkeydown event handler for the input elem.
	Tab key = use the highlighted suggestion, if there is one.
	Esc key = get rid of the autosuggest dropdown
	Up/down arrows = Move the highlight up and down in the suggestions.
	********************************************************/
	elem.onkeydown = function(ev)
	{
		var key = me.getKeyCode(ev);

		switch(key)
		{
			case TAB:
			me.useSuggestion();
            me.cancelEvent(ev);
			break;

            case KEYUP:
			if (me.highlighted > 0)
			{
				me.highlighted--;
			}
			me.changeHighlight(key);
            me.cancelEvent(ev);
            break;

            case KEYDN:
			if (me.highlighted < (me.eligible.length - 1))
			{
				me.highlighted++;
			}
			me.changeHighlight(key);
    		me.cancelEvent(ev);
			break;
		}
	};

	/********************************************************
	onkeypress handler for the elem
	Don't move the text box cursor when the up and down key
	is used to select a suggestion.
	********************************************************/

    elem.onkeypress = function (ev)
    {
		var key = me.getKeyCode(ev);
        switch(key)
        {
		  case KEYUP:
		  case KEYDN:
            if (ev.cancelable) ev.preventDefault();
            break;
        }
    }


	/********************************************************
	onkeyup handler for the elem
	If the text is of sufficient length, and has been changed, 
	then display a list of eligible suggestions.
	********************************************************/

	elem.onkeyup = function(ev) 
	{
		var key = me.getKeyCode(ev);
		switch(key)
		{
		
        case ESC:
        case END:
        case HOME:
        case KEYLF:
		case KEYRT:
            me.hideDiv();
            break;
			
		case KEYUP:
		case KEYDN:
		case TAB:
			break;
        
		default:
			if (me.getCurrentTag() && currentTag.text)
			{
				me.inputText = currentTag.text;
				me.getEligible();
				if (me.eligible.length) {
    				me.createDiv();
	       			me.positionDiv();
			     	me.showDiv();
			    } else {
			         me.hideDiv();
			    }
			}
			else
			{
				if( this.value.substr(-1,1) != ' ' || split_delim.join().indexOf( ' ' ) >= 0 )
					me.hideDiv();
			}
			lastEdit = this.value;
		}

	};


	/********************************************************
	Insert the highlighted suggestion into the input box, and 
	remove the suggestion dropdown.
	********************************************************/
	this.useSuggestion = function() 
	{
		if (this.highlighted > -1)
		{
            var tag = this.eligible[this.highlighted];
            // this.elem.value = this.eligible[this.highlighted]; 
			var tagArray = split_tags( split_delim, this.elem.value );
            if(typeof tag == 'undefined') tag = suggestions[suggestions.picked].innerHTML.unescHtml() // tab complete rather than click complete
            tagArray[currentTag.index] = tag
			var spacer = ' ';
            var text = join_tags( join_delim, spacer, tagArray );
			if( join_delim == spacer )
			{
				this.elem.value = (text.substr(-1,1) == spacer ? text : text + spacer );
			}
			else
			{
				this.elem.value = (text.substr(-2,2) == join_delim + spacer ? text : text + join_delim + spacer );
			}
	        lastEdit = this.elem.value;
			this.hideDiv();
			//It's impossible to cancel the Tab key's default behavior. 
			//So this undoes it by moving the focus back to our field right after
			//the event completes.
			setTimeout("document.getElementById('" + this.elem.id + "').focus()",0);
		}
	};

	/********************************************************
	Display the dropdown. Pretty straightforward.
	********************************************************/
	this.showDiv = function()
	{
		this.div.style.display = 'block';
	};

	/********************************************************
	Hide the dropdown and clear any highlight.
	********************************************************/
	this.hideDiv = function()
	{
		this.div.style.display = 'none';
		this.highlighted = -1;
	};

	/********************************************************
	Modify the HTML in the dropdown to move the highlight.
	********************************************************/
	this.changeHighlight = function()
	{
		var lis = this.div.getElementsByTagName('LI');
		for (i = 0 ; i < lis.length ; i++)
		{
			var li = lis[i];

			if (this.highlighted == i)
			{
				li.className = "selected";
			}
			else
			{
				li.className = "";
			}
		}
	};

	/********************************************************
	Position the dropdown div below the input text field.
	********************************************************/
	this.positionDiv = function()
	{
		var el = this.elem;
		var x = 0;
		var y = el.offsetHeight;
	
		//Walk up the DOM and add up all of the offset positions.
		while (el.offsetParent && el.tagName.toUpperCase() != 'BODY')
		{
			x += el.offsetLeft;
			y += el.offsetTop;
			el = el.offsetParent;
		}

		x += el.offsetLeft;
		y += el.offsetTop;

		this.div.style.left = x + 'px';
		this.div.style.top = y + 'px';

	};

	/********************************************************
	Build the HTML for the dropdown div
	********************************************************/
	this.createDiv = function()
	{
		var ul = document.createElement('ul');
	
		//Create an array of LI's for the words.
		for (i = 0; i < this.eligible.length; i++)
		{
			var word = this.eligible[i];
	
			var li = document.createElement('li');
			var a = document.createElement('a');
			a.href="javascript:false";
			a.innerHTML = word;
			li.appendChild(a);
	
			if (me.highlighted == i)
			{
				li.className = "selected";
			}
	
			ul.appendChild(li);
		}
	
		this.div.replaceChild(ul,this.div.childNodes[0]);

		/********************************************************
		mouseover handler for the dropdown ul
		move the highlighted suggestion with the mouse
		********************************************************/
		ul.onmouseover = function(ev)
		{
			//Walk up from target until you find the LI.
			var target = me.getEventSource(ev);
			// not all implementations have a global 'undefined' variable
 			var undefValue;
 			while (target.tagName == undefValue || target.parentNode && target.tagName.toUpperCase() != 'LI')
			{
				target = target.parentNode;
			}
		
			var lis = me.div.getElementsByTagName('LI');
			
	
			for(i = 0 ; i < lis.length ; i++)
			{
				var li = lis[i];
				if(li == target)
				{
					me.highlighted = i;
					break;
				}
			}
			me.changeHighlight();
		};

		/********************************************************
		click handler for the dropdown ul
		insert the clicked suggestion into the input
		********************************************************/
		ul.onclick = function(ev)
		{
			me.useSuggestion();
			me.hideDiv();
			me.cancelEvent(ev);
			return false;
		};
	
		this.div.className="suggestion_list";
		this.div.style.position = 'absolute';

	};

	/********************************************************
	determine which of the suggestions matches the input
	********************************************************/
	this.getEligible = function()
	{
	    var count = 0;
		this.eligible = new Array();
		for (i = 0 ; i < this.suggestions.length ; i++) 
		{
			var suggestion = this.suggestions[i];
			if(suggestion.toLowerCase().indexOf(this.inputText.toLowerCase()) == "0")
			{
               if (this.elem.value.search(new RegExp(suggestion,'i')) == -1 ) { // no false positives?
    				this.eligible[this.eligible.length]=suggestion;
	       	        count++;
			        if (count == this.limit) break;
               }
			}
		}
	};

	/********************************************************
	Helper function to determine the keycode pressed in a 
	browser-independent manner.
	********************************************************/
	this.getKeyCode = function(ev)
	{
		if(ev)			//Moz
		{
			return ev.keyCode;
		}
		if(window.event)	//IE
		{
			return window.event.keyCode;
		}
	};

	/********************************************************
	Helper function to determine the event source element in a 
	browser-independent manner.
	********************************************************/
	this.getEventSource = function(ev)
	{
		if(ev)			//Moz
		{
			return ev.target;
		}
	
		if(window.event)	//IE
		{
			return window.event.srcElement;
		}
	};

	/********************************************************
	Helper function to cancel an event in a 
	browser-independent manner.
	(Returning false helps too).
	********************************************************/
	this.cancelEvent = function(ev)
	{
		if(ev)			//Moz
		{
			ev.preventDefault();
			ev.stopPropagation();
		}
		if(window.event)	//IE
		{
			window.event.returnValue = false;
		}
	}

    this.getCurrentTag = function()
    {
        if(this.elem.value == lastEdit) return true // no edit
        if(this.elem == '') return false
        currentTag = {}
		var tagArray = split_tags( split_delim, this.elem.value.toLowerCase() );
		var oldArray = split_tags( split_delim, lastEdit.toLowerCase() );
        var currentTags = [];
        var matched=false;
        var t;
        var o;
        for (t in tagArray) {
            for (o in oldArray) {
                if(typeof oldArray[o] == 'undefined') { 
                    oldArray.splice(o,1); 
                    break
                }
                if(tagArray[t] == oldArray[o]) { 
                    matched = true; 
                    oldArray.splice(o,1); 
                    break; 
                }
            }
            if(!matched) currentTags[currentTags.length] = t
            matched=false
        }
        // more than one word changed... abort
        if(currentTags.length > 1) { 
            this.hideDiv();
            return false; 
        } 
        currentTag = { text:tagArray[currentTags[0]], index:currentTags[0] }
        return true    
    }

}

//counter to help create unique ID's
var idCounter = 0;
