﻿/*
  @author: remy sharp / http://remysharp.com
  @url: http://remysharp.com/2007/12/28/jquery-tag-suggestion/
  @usage: setGlobalTags(['javascript', 'jquery', 'java', 'json']); // applied tags to be used for all implementations
          $('input.tags').tagSuggest(options);
          
          The selector is the element that the user enters their tag list
  @params:
    matchClass - class applied to the suggestions, defaults to 'tagMatches'
    tagContainer - the type of element uses to contain the suggestions, defaults to 'span'
    tagWrap - the type of element the suggestions a wrapped in, defaults to 'span'
    sort - boolean to force the sorted order of suggestions, defaults to false
    url - optional url to get suggestions if setGlobalTags isn't used.  Must return array of suggested tags
    tags - optional array of tags specific to this instance of element matches
    delay - optional sets the delay between keyup and the request - can help throttle ajax requests, defaults to zero delay
    separator - optional separator string, defaults to ' ' (Brian J. Cardiff)
  @license: Creative Commons License - ShareAlike http://creativecommons.org/licenses/by-sa/3.0/
  @version: 1.4
  @changes: fixed filtering to ajax hits
*/

(function ($) {
    var globalTags = [];
    var count = 0;
 
    // creates a public function within our private code.
    // tags can either be an array of strings OR
    // array of objects containing a 'tag' attribute
    window.setGlobalTags = function(tags /* array */) {
        globalTags = getTags(tags);
    };
    
    function getTags(tags) { 
 
        var tag, i, goodTags = [];
        for (i = 0; i < tags.length; i++) {
            tag = tags[i];
            if (typeof tags[i] == 'object') {
                tag = tags[i].tag;
            } 
            goodTags.push(tag.toLowerCase());
        }
        
        return goodTags;
    }
    
    $.fn.tagSuggest = function (options) {
        var defaults = { 
            'matchClass' : 'tagMatches', 
            'tagContainer' : 'ul', 
            'tagWrap' : 'li', 
            'sort' : true,
            'tags' : null,
            'url' : null,
            'delay' : 0,
            'separator' : ','
        };

        var i, tag, userTags = [], settings = $.extend({}, defaults, options);

        if (settings.tags) {
            userTags = getTags(settings.tags);
        } else {
            userTags = globalTags;
        }

        return this.each(function () {
        
            var tagsElm = $(this);
            var elm = this;
            
            var count = -1;
              
            var matches, fromTab = false;
            var suggestionsShow = false;
            var workingTags = [];
            var currentTag = {"position": 0, tag: ""};
            var tagMatches = document.createElement(settings.tagContainer);
            
            function showSuggestionsDelayed(el, key) {

                if (settings.delay) {
                    if (elm.timer) clearTimeout(elm.timer);
                    elm.timer = setTimeout(function () {
                        showSuggestions(el, key);
                    }, settings.delay);
                } else {
                    showSuggestions(el, key);
                }
            }
   
            function showSuggestions(el, key) {
            
                workingTags = el.value.split(settings.separator);
                
                matches = [];
                var i, html = '', chosenTags = {}, tagSelected = false;

                // we're looking to complete the tag on currentTag.position (to start with)
                currentTag = { position: currentTags.length-1, tag: '' };
                      
                for (i = 0; i < currentTags.length && i < workingTags.length; i++) {
                    if (!tagSelected && 
                        currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
                        currentTag = { position: i, tag: workingTags[i].toLowerCase() };
                        tagSelected = true;
                    }
                    // lookup for filtering out chosen tags
                    chosenTags[currentTags[i].toLowerCase()] = true;
                }
         
                if (currentTag.tag) {
                   // collect potential tags
                    if (settings.url) {
                        $.ajax({
                            'url' : settings.url,
                            'dataType' : 'json',
                            'data' : { 'tag' : currentTag.tag },
                            'async' : false, // wait until this is ajax hit is complete before continue
                            'success' : function (m) {
                                matches = m;
                            }
                        });
                    } else {
                        for (i = 0; i < userTags.length; i++) {
                            if (userTags[i].indexOf(currentTag.tag) === 0) {
                                matches.push(userTags[i]);
                            }
                        }                        
                    }
                    
                    matches = $.grep(matches, function (v, i) {
                        return !chosenTags[v.toLowerCase()];
                    });

                    if (settings.sort) {
                        matches = matches.sort();
                    }                    

                    for (i = 0; i < matches.length; i++) {
                             
                        html += '<' + settings.tagWrap + ' class="_tag_suggestion">' + matches[i] + '</' + settings.tagWrap + '>';
                    }

                    tagMatches.html(html);
                    suggestionsShow = !!(matches.length);
                } else {
                    hideSuggestions();
                }
            }
            
           
            function hideSuggestions() {
                tagMatches.empty();
                matches = [];
                suggestionsShow = false;                
            }

            function setSelection() {
                var v = tagsElm.val();
                
                // tweak for hintted elements
                // http://remysharp.com/2007/01/25/jquery-tutorial-text-box-hints/
                if (v == tagsElm.attr('title') && tagsElm.is('.hint')) v = '';

                currentTags = v.split(settings.separator);
                //hideSuggestions();
            }

            function chooseTag(tag) {
                var i, index;
                for (i = 0; i < currentTags.length; i++) {
                    if (currentTags[i].toLowerCase() != workingTags[i].toLowerCase()) {
                                                            
                        index = i;
                        break;
                    }
                }

                if (index == workingTags.length - 1) tag = tag + settings.separator;

                workingTags[i] = tag.replace(",","");

                //tagsElm.val(workingTags.join(settings.separator));
                tagsElm.val(workingTags[i]);
                tagsElm.blur().focus();
                setSelection();
            }

            function handleKeys(ev) {
              
                fromTab = false;
                var type = ev.type;
                var resetSelection = false;
                
                switch (ev.keyCode) {
                    
                    case 37: // ignore cases (arrow keys)
                    
                      case 40: { // down arrow
                    
                        if (suggestionsShow) {
                        
                           if (count==matches.length-1) {
                                        
                              count = 0;
                                        
                           } else {
                                    
                               count++;
                                    
                           }                         
                           chooseTag(matches[count]);
                           
                           var lastChar=3;
                           var st = tagMatches[0].innerHTML;
                           var c = 0;
                           var found = false;
                           var i=0;
                           
                                                                                         
                           while (i < st.length && !found) {
                           
                                st=st.replace('style="background-color: rgb(153, 153, 153); color: white;"',"");                          
                                /* Fix for IE */
                                st=st.replace('style="BACKGROUND-COLOR: #999999; COLOR: white"',"");   
                                                   
                                if (st.substring(i,lastChar).toLowerCase()=="<li") {
                                     c++;
                                     
                                }
                                
                                if (count+1==c) {
                                    var init = st.substring(0,i);
                                    var st1 = st.substring(i,lastChar);
                                    var st2 = st.substring(lastChar, st.length);
                                     tagMatches.html(init+st1+' style="background-color:#999999;color:white;" '+st2);
                                    found=true;
                                }
                                         
                                i++;
                                lastChar++;
                           }
                                                   
                        }                            
                        return false;             
                    }
                    
                    case 38: { // up arrow
                              
                        if (suggestionsShow) {
                                                               
                            if (count==0) {
                                
                                count = matches.length-1;
                                
                            } else {
                            
                                count--;
                            
                            }
                  
                            chooseTag(matches[count]);     
                            
                             var lastChar=3;
                           var st = tagMatches[0].innerHTML;
                           var c = 0;
                           var found = false;
                           var i=0;
                                                            
                           while (i < st.length && !found) {
                           
                                st=st.replace('style="background-color: rgb(153, 153, 153); color: white;"',"");                          
                                /* Fix for IE */
                                st=st.replace('style="BACKGROUND-COLOR: #999999; COLOR: white"',"");   
                                                   
                                if (st.substring(i,lastChar).toLowerCase()=="<li") {
                                     c++;
                                     
                                }
                                
                                if (count+1==c) {
                                    var init = st.substring(0,i);
                                    var st1 = st.substring(i,lastChar);
                                    var st2 = st.substring(lastChar, st.length);
                                     tagMatches.html(init+st1+' style="background-color:#999999;color:white;" '+st2);
                                    found=true;
                                }
                                         
                                i++;
                                lastChar++;
                           }
                                         
                        }  
                        return false;          
                     }
                          
                    case 39:
                    
                    case 224: //
                    
                    case 17: // ctrl
                    
                    case 16: // shift
                    
                    case 18: { // alt
                        return true;
                    }

                    case 8: { // backspace
                  
                        // delete - hide selections if we're empty
                        if (this.value == '') {
                            hideSuggestions();
                            setSelection();
                            count=-1;
                            return true;
                        } else {
                            type = 'keyup'; // allow drop through
                            //resetSelection = true;                            
                            showSuggestionsDelayed(this);
                        }
                        count=-1;
                        break;
                    }

                    case 9: // return and tab
                    
//                    case 13: { // enter
//               
//                        if (suggestionsShow) {
//                            // complete
//                                                                
//                            chooseTag(matches[0]);
//                            
//                            fromTab = true;
//                            return false;
//                        } else {
//                            return true;
//                        }
//                        
//                    }
                    
                    case 27: { // escape
                    
                        hideSuggestions();
                        setSelection();
                        return true;
                        
                    }
                    case 32: {
                    
                        setSelection();
                        return true;
                        
                    }
                }

                if (type == 'keyup') {
           
                    switch (ev.charCode) {
                        case 9:
                        case 13: {
                            return true;
                        }
                        case 40: {
                            return true;
                        }
                    }

                    if (resetSelection) { 
                        setSelection();
                    }
                    showSuggestionsDelayed(this, ev.charCode);            
                }
                
            }
            
            function handleEnterKey(ev) {
                
                switch (ev.keyCode) {
                    
                    case 13: { 
                        if (suggestionsShow) {
                                                 
                            if (count==-1) {
                            
                                count=0;
                                
                            }    
                                                                                
                            chooseTag(matches[count]);
                            
                            fromTab = true;
                            return false;
                            
                        } else {
                        
                            return true;
                            
                        }
                        
                        count = -1;                         
                    }
                   
                }
            }

            tagsElm.after(tagMatches).keypress(handleEnterKey).keyup(handleKeys).blur(function () {
                if (fromTab == true || suggestionsShow) { // tweak to support tab selection for Opera & IE                            
                    fromTab = false;
                    tagsElm.focus();
                }
            });

            // replace with jQuery version
            tagMatches = $(tagMatches).click(function (ev) { 
                if (ev.target.nodeName == settings.tagWrap.toUpperCase() && $(ev.target).is('._tag_suggestion')) {
                    chooseTag(ev.target.innerHTML);
                    hideSuggestions();
                }                
            }).addClass(settings.matchClass);

            // initialise
            setSelection();            
        });
    };
})(jQuery);
