/* jQuery Autocomplete plugin 1.2.1 (c) 2009 Joern Zaefferer with small modifications by Alfonso Gomez-Arzola.
 * jQuery textbox list plugin 2.0   (c) 2009-2011 Michael Daum http://michaeldaumconsulting.com
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Modified by Andrii Zadorozhnii.
 */
(function($) {

$.extend($.fn, {
  nextAll: function(s) {
    var $els = $(), $el = this.next()
    while( $el.length ) {
      if(typeof s === 'undefined' || $el.is(s)) $els = $els.add($el)
      $el = $el.next()
    }
    return $els
  },
  prevAll: function(s) {
    var $els = $(), $el = this.prev()
    while( $el.length ) {
      if(typeof s === 'undefined' || $el.is(s)) $els = $els.add($el)
      $el = $el.prev()
    }
    return $els
  },

  textboxlister: function(options) {
    var opts = $.extend(true, $.TextboxLister.defaults, options);
    return this.each(function() {
      new $.TextboxLister(this, opts);
    });
  },

  autocomplete: function(urlOrData, options) {
    var isUrl = typeof urlOrData == "string";
    options = $.extend({}, $.Autocompleter.defaults, {
      url: isUrl ? urlOrData : null,
      data: isUrl ? null : urlOrData,
      max: options && !options.scroll ? 10 : 150
    }, options);

    // if highlight is set to false, replace it with a do-nothing function
    options.highlight = options.highlight || function(value) { return value; };
    // if the formatMatch option is not specified, then use formatItem for backwards compatibility
    options.formatMatch = options.formatMatch || options.formatItem;

    return this.each(function() {
      new $.Autocompleter(this, options);
    });
  },
  result: function(handler) { return this.on("result", handler);}
});

$.Autocompleter = function(input, options) {
  var KEY = {
    UP: 38,
    DOWN: 40,
    DEL: 46,
    TAB: 9,
    RETURN: 13,
    ESC: 27,
    COMMA: 188,
    PAGEUP: 33,
    PAGEDOWN: 34,
    BACKSPACE: 8
  };

  var globalFailure = null;
  if(options.failure != null && typeof options.failure == "function") {
    globalFailure = options.failure;
  }
  // Create $ object for input element
  var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);

  var timeout;
  var previousValue = "";
  var cache = $.Autocompleter.Cache(options);
  var hasFocus = 0;
  var lastKeyPressCode;
  var config = {
    mouseDownOnSelect: false
  };
  var select = $.Autocompleter.Select(options, input, selectCurrent, config);

  var blockSubmit;

  // add spinner

  // prevent form submit in opera when selecting with return key
  $.browser.opera && $(input.form).bind("submit.autocomplete", function() {
    if (blockSubmit) {
      blockSubmit = false;
      return false;
    }
  });

  $input.bind('keyup change', function(event) {
    // a keypress means the input has focus
    // avoids issue where input had focus before the autocomplete was applied
    hasFocus = 1;
    // track last key pressed
    lastKeyPressCode = event.keyCode;

    switch(event.keyCode) {
    case KEY.UP:
      if ( select.visible() ) {
        event.preventDefault();
        select.prev();
      } else {
        onChange(0, true);
      }
      break;

    case KEY.DOWN:
      if ( select.visible() ) {
        event.preventDefault();
        select.next();
      } else {
        onChange(0, true);
      }
      break;

    case KEY.PAGEUP:
      if ( select.visible() ) {
        event.preventDefault();
        select.pageUp();
      } else {
        onChange(0, true);
      }
      break;

    case KEY.PAGEDOWN:
      if ( select.visible() ) {
        event.preventDefault();
        select.pageDown();
      } else {
        onChange(0, true);
      }
      break;

    // matches also semicolon
    case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
    case KEY.TAB:
    case KEY.RETURN:
      if( selectCurrent() ) {
        // stop default to prevent a form submit, Opera needs special handling
        event.preventDefault();
        blockSubmit = true;
        return false;
      }
      break;
    case KEY.ESC:
      select.hide();
      break;
    default:
      clearTimeout(timeout);
      timeout = window.setTimeout(onChange, options.delay);
      break;
    }
  }).focus(function(){
    // track whether the field has focus, we shouldn't process any
    // results if the field no longer has focus
    hasFocus++;
  }).blur(function() {
    hasFocus = 0;
    if (!config.mouseDownOnSelect) {
      hideResults();
    }
  }).click(function() {
    // show select when clicking in a focused field
    // but if clickFire is true, don't require field
    // to be focused to begin with; just show select
    if( options.clickFire ) {
      if ( !select.visible() ) {
        onChange(0, true);
      }
    } else {
      if( hasFocus++ > 1 && !select.visible() ) {
        onChange(0, true);
      }
    }
  }).on("flushCache", function() {
    cache.flush();
  }).on("setOptions", function() {
    $.extend(true, options, arguments[1]);
    // if we've updated the data, repopulate
    if ( "data" in arguments[1] ) cache.populate();
  }).on("unautocomplete", function() {
    select.unbind();
    $input.unbind();
    $(input.form).unbind(".autocomplete");
  }).on('autocompleteData', function(e, res, term){
      var data = options.parse && options.parse(res) || parse(res);
      receiveData(term, data);
  });

  function selectCurrent() {
    var selected = select.selected();
    if( !selected ) return false;
    var v = selected.result;
    previousValue = v;

    if ( options.multiple ) {
      var words = trimWords($input.val());
      if ( words.length > 1 ) {
        var seperator = options.multipleSeparator.length;
        var cursorAt = $(input).selection().start;
        var wordAt, progress = 0;
        $.each(words, function(i, word) {
          progress += word.length;
          if (cursorAt <= progress) {
            wordAt = i;
            return false;
          }
          progress += seperator;
        });
        words[wordAt] = v;
        // TODO this should set the cursor to the right position, but it gets overriden somewhere
        //$.Autocompleter.Selection(input, progress + seperator, progress + seperator);
        v = words.join( options.multipleSeparator );
      }
      v += options.multipleSeparator;
    }

    $input.val(v);
    hideResultsNow();
    $input.trigger("result", [selected.data, selected.value]);
    return true;
  }

  function onChange(crap, skipPrevCheck) {
    if( lastKeyPressCode == KEY.DEL ) {
      select.hide();
      return;
    }

    var currentValue = $input.val();
    if ( !skipPrevCheck && currentValue == previousValue ){return};

    previousValue = currentValue;
    currentValue = lastWord(currentValue);

    if ( currentValue.length >= options.minChars) {
      $input.addClass(options.loadingClass);
      if (!options.matchCase) currentValue = currentValue.toLowerCase();
      request(currentValue, receiveData, hideResultsNow);
    } else {
      stopLoading();
      select.hide(); 
    }
  };

  function trimWords(value) {
    if (!value) return [""];
    if (!options.multiple) return [$.trim(value)];
    return $.map(value.split(options.multipleSeparator), function(word) {
      return $.trim(value).length ? $.trim(word) : null;
    });
  }

  function lastWord(value) {
    if ( !options.multiple ) return value;
    var words = trimWords(value);
    if (words.length == 1) return words[0];
    var cursorAt = $(input).selection().start;
    if (cursorAt == value.length) {
      words = trimWords(value)
    } else {
      words = trimWords(value.replace(value.substring(cursorAt), ""));
    }
    return words[words.length - 1];
  }

  // fills in the input box w/the first match (assumed to be the best match)
  // q: the term entered
  // sValue: the first matching result
  function autoFill(q, sValue){
    // autofill in the complete box w/the first match as long as the user hasn't entered in more data
    // if the last user key pressed was backspace, don't autofill
    if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
      // fill in the value (keep the case the user has typed)
      $input.val($input.val() + sValue.substring(lastWord(previousValue).length));
      // select the portion of the value not typed by the user (so the next character will erase)
      $(input).selection(previousValue.length, previousValue.length + sValue.length);
    }
  };

  function hideResults() {
    clearTimeout(timeout);
    timeout = setTimeout(hideResultsNow, 200);
  };

  function hideResultsNow() {
    var wasVisible = select.visible();
    select.hide();
    clearTimeout(timeout);
    stopLoading();
    if (options.mustMatch) {
      // call search and run callback
      $input.search(
      function (result){
        // if no value found, clear the input box
        if( !result ) {
          if (options.multiple) {
            var words = trimWords($input.val()).slice(0, -1);
            $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
          } else {
            $input.val( "" );
            $input.trigger("result", null);
          }
        }
      });
    }
  };

  function receiveData(q, data) {
    if ( data && data.length && hasFocus ) {
      stopLoading();
      select.display(data, q);
      autoFill(q, data[0].value);
      select.show();
    } else {
      hideResultsNow();
    }
  };

  function request(term, success, failure) {
    if (!options.matchCase) term = term.toLowerCase();
    var data = cache.load(term);
    // recieve the cached data
    if (data && data.length) {
      success(term, data);
    } else if(options.autocomplete){
      var extraParams = {timestamp: +new Date()};

      $.each(options.extraParams, function(key, param) {
        extraParams[key] = typeof param == "function" ? param($input) : param;
      });

      if(options.autocomplete.postback) eval(options.autocomplete.postback);
    } else {
      // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting 
      // the last successful match
      select.emptyList();
      if(globalFailure != null) {
        globalFailure();
      } else {
        failure(term);
      }
    }
  };

  function parse(data) {
    var parsed = [];
    var i, row;
    for(i in data){
      row=data[i];
      parsed[parsed.length] = {
        data: row,
        value: row[0],
        result: options.formatResult && options.formatResult(row, row[0]) || row[0]
      }
    }
    return parsed;
  };

  function stopLoading() {
    $input.removeClass(options.loadingClass);
  };
};

$.Autocompleter.defaults = {
  inputClass: "textboxlister-autocomplete-input",
  resultsClass: "tbl-autocomplete",
  loadingClass: "textboxlister-autocomplete-loading",
  minChars: 1,
  delay: 10,
  matchCase: false,
  matchSubset: true,
  matchContains: false,
  cacheLength: 100,
  max: 1000,
  mustMatch: false,
  extraParams: {},
  selectFirst: true,
  formatItem: function(row){return row[0];},
  formatMatch: null,
  autoFill: false,
  width: 0,
  multiple: false,
  multipleSeparator: " ",
  inputFocus: true,
  clickFire: false,
  inputFocus: true,
  clickFire: false,
  highlight: function(value, term) {
    return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  },
  scroll: true,
  scrollHeight: 180,
  scrollJumpPosition: true
};

$.Autocompleter.Cache = function(options) {
  var data = {};
  var length = 0;

  function matchSubset(s, sub) {
    if (!options.matchCase) s = s.toLowerCase();
    var i = s.indexOf(sub);
    if (options.matchContains == "word"){
      i = s.toLowerCase().search("\\b" + sub.toLowerCase());
    }
    if (i == -1) return false;
    return i == 0 || options.matchContains;
  };

  function add(q, value) {
    if (length > options.cacheLength){
      flush();
    }
    if (!data[q]){
      length++;
    }
    data[q] = value;
  }

  function populate(){
    if( !options.data ) return false;
    // track the matches
    var stMatchSets = {},
    nullData = 0;

    // no url was specified, we need to adjust the cache length to make sure it fits the local data store
    if( !options.url ) options.cacheLength = 1;

    // track all options for minChars = 0
    stMatchSets[""] = [];

    // loop through the array and create a lookup structure
    for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
      var rawValue = options.data[i];
      // if rawValue is a string, make an array otherwise just reference the array
      rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;

      var value = options.formatMatch(rawValue, i+1, options.data.length);
      if ( value === false ) continue;

      var firstChar = value.charAt(0).toLowerCase();
      // if no lookup array for this character exists, look it up now
      if( !stMatchSets[firstChar] ) stMatchSets[firstChar] = [];

      // if the match is a string
      var row = {
        value: value,
        data: rawValue,
        result: options.formatResult && options.formatResult(rawValue) || value
      };

      // push the current match into the set list
      stMatchSets[firstChar].push(row);

      // keep track of minChars zero items
      if ( nullData++ < options.max ) {
        stMatchSets[""].push(row);
      }
    };

    // add the data items to the cache
    $.each(stMatchSets, function(i, value) {
      // increase the cache size
      options.cacheLength++;
      // add to the cache
      add(i, value);
    });
  }

  // populate any existing data
  setTimeout(populate, 25);

  function flush(){
    data = {};
    length = 0;
  }

  return {
    flush: flush,
    add: add,
    populate: populate,
    load: function(q) {
      if (!options.cacheLength || !length) return null;
      /* 
       * if dealing w/local data and matchContains than we must make sure
       * to loop through all the data collections looking for matches
       */
      if( !options.url && options.matchContains ){
        // track all matches
        var csub = [];
        // loop through all the data grids for matches
        for( var k in data ){
          // don't search through the stMatchSets[""] (minChars: 0) cache
          // this prevents duplicates
          if( k.length > 0 ){
            var c = data[k];
            $.each(c, function(i, x) {
              // if we've got a match, add it to the array
              if (matchSubset(x.value, q)) {
                csub.push(x);
              }
            });
          }
        }
        return csub;
      } else 
      // if the exact item exists, use it
      if (data[q]){
        return data[q];
      } else
      if (options.matchSubset) {
        for (var i = q.length - 1; i >= options.minChars; i--) {
          var c = data[q.substr(0, i)];
          if (c) {
            var csub = [];
            $.each(c, function(i, x) {
              if (matchSubset(x.value, q)) {
                csub[csub.length] = x;
              }
            });
            return csub;
          }
        }
      }
      return null;
    }
  };
};

$.Autocompleter.Select = function (options, input, select, config) {
  var CLASSES = {ACTIVE: "active"};

  var b = $(input).parent().parent();
  var listItems,
  active = -1,
  data,
  term = "",
  needsInit = true,
  list;

  // Create results
  function init() {
    if (!needsInit) return;
    list = $("<ul/>")
      .addClass('dropdown-menu')
      .addClass(options.resultsClass)
      .hide()
      .appendTo(b)
      .mouseover(function(event) {
        if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
          active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
          $(target(event)).addClass(CLASSES.ACTIVE);
        }
      }).click(function(event) {
        $(target(event)).addClass(CLASSES.ACTIVE);
        select();
        if( options.inputFocus ) input.focus();
        return false;
      }).mousedown(function() {
        config.mouseDownOnSelect = true;
      }).mouseup(function() {
        config.mouseDownOnSelect = false;
      });

    if( options.width > 0 ) list.css("width", options.width);
    needsInit = false;
  }

  function target(event) {
    var element = event.target;
    while(element && element.tagName != "LI")
      element = element.parentNode;
      // more fun with IE, sometimes event.target is empty, just ignore it then
      if(!element) return [];
      return element;
    }

  function moveSelect(step) {
    listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
    movePosition(step);
    var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
    if(options.scroll) {
      var offset = 0;
      listItems.slice(0, active).each(function() {
        offset += this.offsetHeight;
      });
      if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
        list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
      } else if(offset < list.scrollTop()) {
        list.scrollTop(offset);
      }
    }
  };

  function movePosition(step) {
    if (options.scrollJumpPosition || (!options.scrollJumpPosition && !((step < 0 && active == 0) || (step > 0 && active == listItems.size() - 1)) )) {
      active += step;
      if (active < 0) {
        active = listItems.size() - 1;
      } else if (active >= listItems.size()) {
        active = 0;
      }
    }
  }

  function limitNumberOfItems(available) {
    return options.max && options.max < available ? options.max : available;
  }

  function fillList() {
    list.empty();
    var max = limitNumberOfItems(data.length);
    for (var i=0; i < max; i++) {
      if (!data[i]) continue;
      var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
      if ( formatted === false ) continue;

      $("<li/>")
        .attr("data-tbl-autocomplete", JSON.stringify(data[i]))
        .append($("<a href='#'/>").html(options.highlight(formatted, term)))
        .appendTo(list);
    }
    listItems = list.find("li");
    if ( options.selectFirst ) {
      listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
      active = 0;
    }
    // apply bgiframe if available
    if ( $.fn.bgiframe ) list.bgiframe();
  }

  return {
    display: function(d, q) {
      init();
      data = d;
      term = q;
      fillList();
    },
    next: function() { moveSelect(1);},
    prev: function() { moveSelect(-1);},
    pageUp: function() {
      if (active != 0 && active - 8 < 0) {
        moveSelect( -active );
      } else {
        moveSelect(-8);
      }
    },
    pageDown: function() {
      if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
        moveSelect( listItems.size() - 1 - active );
      } else {
        moveSelect(8);
      }
    },
    hide: function() {
      list && list.hide();
      listItems && listItems.removeClass(CLASSES.ACTIVE);
      active = -1;
    },
    visible : function() {
      return list && list.css("display")!== 'none';
    },
    current: function() {
      return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
    },
    show: function() {
      list
        .css({
          left: $(input).offset().left,
          top: b.offset().top + b.height()
        })
        .show();

      if(options.scroll) {
        list.scrollTop(0);
        list.css({
          maxHeight: options.scrollHeight,
          overflow: 'auto'
        });

        if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
          var listHeight = 0;
          listItems.each(function() {
            listHeight += this.offsetHeight;
          });
          var scrollbarsVisible = listHeight > options.scrollHeight;
          list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
          if (!scrollbarsVisible) {
            // IE doesn't recalculate width when scrollbar disappears
            listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
          }
        }
      }
    },
    selected: function() {
      var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
      var data = selected;
      if($(selected).attr("data-tbl-autocomplete")){
        data = JSON.parse($(selected).attr("data-tbl-autocomplete"));
      }
      return selected && selected.length && data;
    },
    emptyList: function (){
      list && list.empty();
    },
    unbind: function() {
      list && list.remove();
    }
  };
};

$.fn.selection = function(start, end) {
  if (start !== undefined) {
    return this.each(function() {
      if( this.createTextRange ){
        var selRange = this.createTextRange();
        if (end === undefined || start == end) {
          selRange.move("character", start);
          selRange.select();
        } else {
          selRange.collapse(true);
          selRange.moveStart("character", start);
          selRange.moveEnd("character", end);
          selRange.select();
        }
      } else if( this.setSelectionRange ){
        this.setSelectionRange(start, end);
      } else if( this.selectionStart ){
        this.selectionStart = start;
        this.selectionEnd = end;
      }
    });
  }
  var field = this[0];
  if ( field.createTextRange ) {
    var range = document.selection.createRange(),
      orig = field.value,
      teststring = "<->",
      textLength = range.text.length;
    range.text = teststring;
    var caretAt = field.value.indexOf(teststring);
    field.value = orig;
    this.selection(caretAt, caretAt + textLength);
    return {
      start: caretAt,
      end: caretAt + textLength
    }
  } else if( field.selectionStart !== undefined ){
    return {
      start: field.selectionStart,
      end: field.selectionEnd
    }
  }
};

$.TextboxLister = function(elem, opts) {
  var dir = {self: 0, prev:1, next:2};
  var self = this;
  var cursor, focused = false;
  self.input = $(elem);
  elem.textboxLister = self;
  self.opts = opts;

  $(self.input).attr('data-list', true);

  // wrap
  var valContainer = $("<div class="+self.opts.listValuesContainerClass+"></div>");
  self.container = self.input.wrap(valContainer);
  self.mainContainer = self.input.parent().wrap("<div class="+self.opts.containerClass+"></div>");

  self.mainContainer.focusin(function(){ focused = true;});
  $(document).focusin(function(e){ 
    focused = $(e.target).closest(self.mainContainer).length !=0;
  });


  if (self.opts.clearControl) { //clear button
    $(self.opts.clearControl).click(function() {
      self.clear();
      this.blur();
      return false;
    });
  }

  if (self.opts.resetControl) { //reset button
    $(self.opts.resetControl).click(function() {
      self.reset();
      this.blur();
      return false;
    });
  }

  if (self.opts.autocomplete) {// autocompletion
    self.input.attr('autocomplete', 'off')
      .autocomplete(self.opts.autocomplete, self.opts)
      .result(function(event, data, value) {self.select(data[0]);});
  } else {
    // autocomplete does not fire the result event on typed items
    self.input.keypress(function(event) {
      // track last key pressed
      if(event.keyCode == 13) {
          var val = self.input.val();
          if (val) {
            self.select(val);
            event.preventDefault();
            return false;
          }
      }
    });
  }

  $(document).keydown(function(e){
    if(!focused)return;
    if ( $( '.' + self.opts.containerClass + ' *:focus').length==0 && $('.'+self.opts.listValueSelectedClass).length==0) return;
    if($(self.input).is(':focus') && self.input.val().length > 0) return;

    var eStop = function(){e.stopPropagation(); e.preventDefault();};
    switch(e.keyCode){
      case 8:
        if(self.input.val().length==0){
          eStop();
          if(self.cursor){
            var el = self.cursor;
            if( $(self.cursor).prev('span.'+self.opts.listValueClass).is('span')) {
              self.focusTag(self.cursor, 'prev');
            } else {
              self.input.focus();
            }
            self.deselect(el.find('input').val());
          } else {
            self.focusTag(self.input.prev('span.'+self.opts.listValueClass), 'self');
            self.input.blur();
          }
        }
        return;
      case self.opts.keys.prev:
        eStop();
        if (self.cursor) {
          self.focusTag(self.cursor, 'prev');
        } else {
          self.focusTag(self.input.prev('span.'+self.opts.listValueClass), 'self');
          self.input.blur();
        }
        break;
      case self.opts.keys.next:
        eStop();
        self.focusTag(self.cursor, 'next');
        break;
      case self.opts.keys.del:
        eStop();
        var val = $(self.cursor).find("input").val();
        if( $(self.cursor).next('span.'+self.opts.listValueClass).is('span')) {
          self.focusTag(self.cursor, 'next');
        } else if( $(self.cursor).prev('span.'+self.opts.listValueClass).is('span')) {
          self.focusTag(self.cursor, 'prev');
        } else {
          self.input.focus();
        }
        self.input.trigger("DeleteTag", val);
        break;
    }
  });

  self.input.on('focus', function(){$(this).parent().addClass('focus')});
  self.input.on('blur', function(){$(this).parent().removeClass('focus')});
  self.input.on('TagClick', function(e, val){self.focusTag(val, 'self');});
  self.input.on('UpdateTags', function(e){
    var input = $(self.input);
    var tagsCont  = input.parent();
    var tags = input.prevAll('span.tbl-tag');
    var il = tagsCont.width() - 2;//border
    var span = input.prevAll('span');

    var i, offset=0, width, empty=0, tag;
    for(i=0;i<span.length;i++){
      tag = $(span[i]);
      width = tag.width() + parseInt(tag.css('margin-left'),10) + parseInt(tag.css('margin-right'),10);
      offset = offset + width;
      if(offset > il){
        empty = empty + (offset - il);
        offset = width;
      }
    }
    var iw = il - offset;
    if(il-offset < 40) iw = il;
    input.css({width: iw - (parseInt(input.css('margin-left'),10) + parseInt(input.css('margin-right'),10)) });

  });
  self.input.bind("AddValue", function(e, val) {
    if (val) {
      self.select(val);
    }
  });
  self.input.on("DeleteTag", function(e, val) {
    if (val){
      self.deselect([val]);
      if(!self.cursor) $(self.input).focus();
    }
  });
  self.input.bind("Reset", function(e) {self.reset();});
  self.input.bind("Clear", function(e) {self.clear();});

  // init
  self.currentValues = [];
  self.titleOfValue = [];
  if (self.input.val()) {
    self.select(self.input.val().split(/\s*,\s*/).sort(), true);
  }
  self.initialValues = self.currentValues.slice();
  var wd = self.input.parent().width()
    - (parseInt(self.input.css('margin-left'),10) + parseInt(self.input.css('margin-right'),10))
    - (parseInt(self.input.css('padding-left'),10) + parseInt(self.input.css('padding-right'),10));
  self.input.css({width: wd})
  self.input.show();

  return this;
};

$.TextboxLister.prototype.focusTag = function(span, direction){
  var self = this;

  if (direction == 'self'){
    $('.'+self.opts.listValueSelectedClass).removeClass(self.opts.listValueSelectedClass);
    self.cursor = span;
    $(span).addClass(self.opts.listValueSelectedClass);
  } else if(direction == 'prev'){
    var prev = $(span).prev('span.'+self.opts.listValueClass);
    if(prev.is('span')){
      $(self.cursor).removeClass(self.opts.listValueSelectedClass);
      prev.addClass(self.opts.listValueSelectedClass);
      self.cursor = prev;
    }
  } else if(direction == 'next'){
    var next = $(span).next('span.'+self.opts.listValueClass);
    if(next.is('span')){
      $(self.cursor).removeClass(self.opts.listValueSelectedClass);
      next.addClass(self.opts.listValueSelectedClass);
      self.cursor = next;
    } else {
      $(self.cursor).removeClass(self.opts.listValueSelectedClass);
      self.cursor = null;
      self.input.focus();
    }
  }
}

// clear selection 
$.TextboxLister.prototype.clear = function() {
    var self = this;
    self.container.parent().find("."+self.opts.listValueClass).remove();
    self.currentValues = [];
    self.titleOfValue = [];

    // onClear callback
    if (typeof(self.opts.onClear) == 'function') {
      self.opts.onClear(self);
    }
};

// reset selection
$.TextboxLister.prototype.reset = function() {
    var self = this;
    self.clear();
    self.select(self.initialValues);

    // onReset callback
    if (typeof(self.opts.onReset) == 'function') {
      self.opts.onReset(self);
    }
};

// add values to the selection
$.TextboxLister.prototype.select = function(values, suppressCallback) {
    var self = this, i, j, val, title, close, found, currentVal, input;

    if (typeof(values) === 'object') {
      values = values.join(',');
    } 
    if (typeof(values) !== 'undefined' && typeof(values) !== 'null') {
      values = values.split(/\s*,\s*/).sort();
    } else {
      values = '';
    }

    // parse values
    for (i = 0; i < values.length; i++) {
      val = values[i];
      found = false;
      if (!val) {
        continue;
      }
      title = val;
      if (val.match(/^(.*)=(.*)$/)) {
        values[i] = val = RegExp.$1
        self.titleOfValue["_"+val] = RegExp.$2
      }
    }

    // only set values not already there
    if (self.currentValues.length > 0) {
      for (i = 0; i < values.length; i++) {
        val = values[i];
        found = false;
        if (!val) {
          continue;
        }
        for (j = 0; j < self.currentValues.length; j++) {
          currentVal = self.currentValues[j];
          if (currentVal == val) {
            found = true;
            break;
          }
        }
        if (!found) {
          self.currentValues.push(val);
        }
      }
    } else {
      self.currentValues = [];
      for (i = 0; i < values.length; i++) {
        val = values[i];
        if (val) {
          self.currentValues.push(val);
        }
      }
    }

    if (self.opts.doSort) {
      self.currentValues = self.currentValues.sort();
    }

    var cursorVal = $(self.cursor).find('input').val();
    self.cursor = null;
    self.container.parent().find("."+self.opts.listValueClass).remove();

    for (i = self.currentValues.length-1; i >= 0; i--) {
      val = self.currentValues[i];
      if (!val) continue;

      input = "<input type='hidden' name='"+self.input.attr('id')+"' value='"+val+"'/>";

      close = $("<a class='"+ self.opts.closeClass+"' href='#'>&times;</a>");
      close.on("click", function(e){
        var val = $(this).parent().parent().find("input[type=hidden]").val();
        self.input.trigger('DeleteTag', val);
        return false;
      });

      title = $("<span>"+(self.titleOfValue["_"+val] || val)+"</span>");
      title.addClass(self.opts.titleClass);
      title.append(close);

      var sp = $("<span></span>")
        .addClass(self.opts.listValueClass)
        .append(input)
        .append(title)
        .prependTo(self.container.parent())
        .on('click', function() {
          self.input.trigger('TagClick', $(this));
        });

      if (val==cursorVal){
        sp.addClass("active");
        self.cursor = sp;
      }
    }
    self.input.val('');

    // onSelect callback
    if (!suppressCallback && typeof(self.opts.onSelect) == 'function') {
      self.opts.onSelect(self);
    }
    self.input.trigger("UpdateTags");
};

// remove values from the selection 
$.TextboxLister.prototype.deselect = function(values) {
  var self = this, newValues = [], i, j, currentVal, found, val;

  if (typeof(values) == 'object') {
    values = values.join(',');
  }
  values = values.split(/\s*,\s*/);
  if (!values.length) {
    return;
  }

  for (i = 0; i < self.currentValues.length; i++) {
    currentVal = self.currentValues[i];
    if (!currentVal) continue;
    found = false;
    for (j = 0; j < values.length; j++) {
      val = values[j];
      if (val && currentVal == val) {
        found = true;
        break;
      }
    }
    if (!found && currentVal) {
      newValues.push(currentVal);
    }
  }
  self.currentValues = newValues;

  // onDeselect callback
  if (typeof(self.opts.onDeselect) == 'function') {
    self.opts.onDeselect(self);
  }

  self.select(newValues, true);
};

// default settings
$.TextboxLister.defaults = {
  containerClass: 'tbl',
  listValuesContainerClass: 'tbl-tags',
  listValueClass: 'tbl-tag',
  listValueSelectedClass: 'active',
  titleClass: 'tbl-tag-label',
  closeClass: 'close',
  keys:{prev:37, next:39, del: 46},
  doSort: false,
  resetControl: undefined,
  clearControl: undefined,
  autocomplete: undefined,
  onClear: undefined,
  onReset: undefined,
  onSelect: undefined,
  onDeselect: undefined,
  selectFirst:false,
  autoFill:false,
  matchCase:false,
  matchContains:false,
  matchSubset:true
};

})(window.jQuery || window.Zepto);
