MediaWiki

Jquery.autocomplete.js: Difference between revisions

No edit summary
No edit summary
 
Line 2: Line 2:
  * Autocomplete - jQuery plugin 1.0 Beta
  * Autocomplete - jQuery plugin 1.0 Beta
  *
  *
  * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, J+›rn Zaefferer
  * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
  *
  *
  * Dual licensed under the MIT and GPL licenses:
  * Dual licensed under the MIT and GPL licenses:
Line 8: Line 8:
  *  http://www.gnu.org/licenses/gpl.html
  *  http://www.gnu.org/licenses/gpl.html
  *
  *
  * Revision: $Id: jquery.autocomplete.js 3909 2007-11-23 15:11:17Z joern.zaefferer $
  * Revision: $Id: jquery.autocomplete.js 4485 2008-01-20 13:52:47Z joern.zaefferer $
  *
  *
  */
  */
Line 61: Line 61:
  * @option Number cacheLength The number of backend query results to store in cache. If set to 1 (the current result), no caching will happen. Do not set below 1. Default: 10
  * @option Number cacheLength The number of backend query results to store in cache. If set to 1 (the current result), no caching will happen. Do not set below 1. Default: 10
  * @option Boolean matchSubset Whether or not the autocompleter can use a cache for more specific queries. This means that all matches of "foot" are a subset of all matches for "foo". Usually this is true, and using this options decreases server load and increases performance. Only useful with cacheLength settings bigger than one, like 10. Default: true
  * @option Boolean matchSubset Whether or not the autocompleter can use a cache for more specific queries. This means that all matches of "foot" are a subset of all matches for "foo". Usually this is true, and using this options decreases server load and increases performance. Only useful with cacheLength settings bigger than one, like 10. Default: true
  * @option Boolean matchCase Whether or not the comparison is case sensitive. Only important only if you use caching. Default: false
  * @option Boolean matchCase Whether or not the comparison is case sensitive. Important only if you use caching. Default: false
  * @option Boolean matchContains Whether or not the comparison looks inside (i.e. does "ba" match "foo bar") the search results. Only important if you use caching. Don't mix with autofill. Default: false
  * @option Boolean matchContains Whether or not the comparison looks inside (i.e. does "ba" match "foo bar") the search results. Important only if you use caching. Don't mix with autofill. Default: false
  * @option Booolean mustMatch If set to true, the autocompleter will only allow results that are presented by the backend. Note that illegal values result in an empty input box. Default: false
  * @option Booolean mustMatch If set to true, the autocompleter will only allow results that are presented by the backend. Note that illegal values result in an empty input box. Default: false
  * @option Object extraParams Extra parameters for the backend. If you were to specify { bar:4 }, the autocompleter would call my_autocomplete_backend.php?q=foo&bar=4 (assuming the input box contains "foo"). The param can be a function that is called to calculate the param before each request. Default: none
  * @option Object extraParams Extra parameters for the backend. If you were to specify { bar:4 }, the autocompleter would call my_autocomplete_backend.php?q=foo&bar=4 (assuming the input box contains "foo"). The param can be a function that is called to calculate the param before each request. Default: none
  * @option Boolean selectFirst If this is set to true, the first autocomplete value will be automatically selected on tab/return, even if it has not been handpicked by keyboard or mouse action. If there is a handpicked (highlighted) result, that result will take precedence. Default: true
  * @option Boolean selectFirst If this is set to true, the first autocomplete value will be automatically selected on tab/return, even if it has not been handpicked by keyboard or mouse action. If there is a handpicked (highlighted) result, that result will take precedence. Default: true
  * @option Function formatItem Provides advanced markup for an item. For each row of results, this function will be called. The returned value will be displayed inside an LI element in the results list. Autocompleter will provide 4 parameters: the results row, the position of the row in the list of results (starting at 1), the number of items in the list of results and the search term. Default: none, assumes that a single row contains a single value.
  * @option Function formatItem Provides advanced markup for an item. For each row of results, this function will be called. The returned value will be displayed inside an LI element in the results list. Autocompleter will provide 4 parameters: the results row, the position of the row in the list of results (starting at 1), the number of items in the list of results and the search term. Default: none, assumes that a single row contains a single value.
  * @option Function formatResult Similar to formatResult, but provides the formatting for the value to be put into the input field. Again three arguments: Data, position (starting with one) and total number of data. Default: none, assumes either plain data to use as result or uses the same value as provided by formatItem.
  * @option Function formatResult Similar to formatItem, but provides the formatting for the value to be put into the input field. Again three arguments: Data, position (starting with one) and total number of data. Default: none, assumes either plain data to use as result or uses the same value as provided by formatItem.
  * @option Boolean multiple Whether to allow more than one autocomplted-value to enter. Default: false
  * @option Boolean multiple Whether to allow more than one autocomplted-value to enter. Default: false
  * @option String multipleSeparator Seperator to put between values when using multiple option. Default: ", "
  * @option String multipleSeparator Seperator to put between values when using multiple option. Default: ", "
  * @option Number width Specify a custom width for the select box. Default: width of the input element
  * @option Number width Specify a custom width for the select box. Default: width of the input element
  * @option Boolean autoFill Fill the textinput while still selecting a value, replacing the value if more is type or something else is selected. Default: false
  * @option Boolean autoFill Fill the textinput while still selecting a value, replacing the value if more is typed or something else is selected. Default: false
  * @option Number max Limit the number of items in the select box. Is also send as a "limit" parameter with a remote request. Default: 10
  * @option Number max Limit the number of items in the select box. Is also sent as a "limit" parameter with a remote request. Default: 10
  * @option Boolean|Function highlight Whether and how to highlight matches in the select box. Set to false to disable. Set to a function to customize. The function gets the value as the first argument and the search term as the second and must return the formatted value. Default: Wraps the search term in a <strong> element
  * @option Boolean|Function highlight Whether and how to highlight matches in the select box. Set to false to disable. Set to a function to customize. The function gets the value as the first argument and the search term as the second and must return the formatted value. Default: Wraps the search term in a <strong> element  
  * @option Boolean|String moreItems Whether or not to show the "more items" text if there are more items than are currently be displayed. Set to false to disable. Set to a string to customize the html. Default: Displays "more", surrounded with three arrows.
  * @option Boolean scroll Whether to scroll when more results than configured via scrollHeight are available. Default: true
* @option Boolean scroll Whether or not use experimental scroll feature
  * @option Number scrollHeight height of scrolled autocomplete control in pixels
  * @option Number scrollHeight height of scrolled autocomplete control in pixels
  * @option String attachTo The element to attach the autocomplete list to. Useful if used inside a modal window like Thickbox. Default: body -MM
  * @option String attachTo The element to attach the autocomplete list to. Useful if used inside a modal window like Thickbox. Default: body -MM
Line 95: Line 94:
  *
  *
  * @param Function handler The event handler, gets a default event object as first and
  * @param Function handler The event handler, gets a default event object as first and
  *       the selected list item as second argument.
  * the selected list item as second argument.
  * @name result
  * @name result
  * @cat Plugins/Autocomplete
  * @cat Plugins/Autocomplete
Line 141: Line 140:
  */
  */


(function($) {
;(function($) {
 
$.fn.extend({
$.fn.extend({
  autocomplete: function(urlOrData, options) {
autocomplete: function(urlOrData, options) {
      var isUrl = typeof urlOrData == "string";
var isUrl = typeof urlOrData == "string";
      options = $.extend({}, $.Autocompleter.defaults, {
options = $.extend({}, $.Autocompleter.defaults, {
        url: isUrl ? urlOrData : null,
url: isUrl ? urlOrData : null,
        data: isUrl ? null : urlOrData,
data: isUrl ? null : urlOrData,
        delay: isUrl ? $.Autocompleter.defaults.delay : 10,
delay: isUrl ? $.Autocompleter.defaults.delay : 10,
        max: options && !options.scroll ? 10 : 100
max: options && !options.scroll ? 10 : 150
      }, options);
}, options);
 
      // if highlight is set to false, replace it with a do-nothing function
// if highlight is set to false, replace it with a do-nothing function
      options.highlight = options.highlight || function(value) { return value; };
options.highlight = options.highlight || function(value) { return value; };
      // if moreItems is false, replace it w/empty string
      options.moreItems = options.moreItems || "";
return this.each(function() {
 
new $.Autocompleter(this, options);
      return this.each(function() {
});
        new $.Autocompleter(this, options);
},
      });
result: function(handler) {
  },
return this.bind("result", handler);
  result: function(handler) {
},
      return this.bind("result", handler);
search: function(handler) {
  },
return this.trigger("search", [handler]);
  search: function(handler) {
},
      return this.trigger("search", [handler]);
flushCache: function() {
  },
return this.trigger("flushCache");
  flushCache: function() {
},
      return this.trigger("flushCache");
setOptions: function(options){
  },
return this.trigger("setOptions", [options]);
  setOptions: function(options){
},
      return this.trigger("setOptions", [options]);
unautocomplete: function() {
  },
return this.trigger("unautocomplete");
  unautocomplete: function() {
}
      return this.trigger("unautocomplete");
  }
});
});


$.Autocompleter = function(input, options) {
$.Autocompleter = function(input, options) {


  var KEY = {
var KEY = {
      UP: 38,
UP: 38,
      DOWN: 40,
DOWN: 40,
      DEL: 46,
DEL: 46,
      TAB: 9,
TAB: 9,
      RETURN: 13,
RETURN: 13,
      ESC: 27,
ESC: 27,
      COMMA: 188,
COMMA: 188,
      PAGEUP: 33,
PAGEUP: 33,
      PAGEDOWN: 34
PAGEDOWN: 34
  };
};
 
  // 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 lastAjaxCall    = {query:""};  // HLS - for options.listIsSorted
  var config = {
      mouseDownOnSelect: false
  };
  var select = $.Autocompleter.Select(options, input, selectCurrent, config);
 
  $input.keydown(function(event) {
      // track last key pressed
      lastKeyPressCode = event.keyCode;
      switch(event.keyCode) {
 
        case KEY.UP:
            event.preventDefault();
            if ( select.visible() ) {
              select.prev();
            } else {
              onChange(0, true);
            }
            break;
 
        case KEY.DOWN:
            event.preventDefault();
            if ( select.visible() ) {
              select.next();
            } else {
              onChange(0, true);
            }
            break;
 
        case KEY.PAGEUP:
            event.preventDefault();
            if ( select.visible() ) {
              select.pageUp();
            } else {
              onChange(0, true);
            }
            break;
 
        case KEY.PAGEDOWN:
            event.preventDefault();
            if ( select.visible() ) {
              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() ){
              // make sure to blur off the current field
              if( !options.multiple )
                  $input.blur();
              event.preventDefault();
            }
            break;
 
        case KEY.ESC:
            // HLS - added ESC input when suggest box is hidden
            select.visible()?select.hide():(this.value = "");
            break;
 
        default:
            clearTimeout(timeout);
            timeout = setTimeout(onChange, options.delay);
            break;
      }
  }).keypress(function() {
      // having fun with opera - remove this binding and Opera submits the form when we select an entry via return
  }).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
      if ( hasFocus++ > 1 && !select.visible() ) {
        onChange(0, true);
      }
  }).bind("search", function() {
      // TODO why not just specifying both arguments?
      var fn = (arguments.length > 1) ? arguments[1] : null;
      function findValueCallback(q, data) {
        var result;
        if( data && data.length ) {
            for (var i=0; i < data.length; i++) {
              if( data[i].result.toLowerCase() == q.toLowerCase() ) {
                  result = data[i];
                  break;
              }
            }
        }
        if( typeof fn == "function" ) fn(result);
        else $input.trigger("result", result && [result.data, result.value]);
      }
      $.each(trimWords($input.val()), function(i, value) {
        request(value, findValueCallback, findValueCallback);
      });
  }).bind("flushCache", function() {
      cache.flush();
  }).bind("setOptions", function() {
      $.extend(options, arguments[1]);
      // if we've updated the data, repopulate
      if ( "data" in arguments[1] )
        cache.populate();
  }).bind("unautocomplete", function() {
      select.unbind();
      $input.unbind();
  });
 
 
  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 ) {
            v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
        }
        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 [""];
      }
      //var words = value.split( $.trim( options.multipleSeparator ) );  This is the original
      var words = value.split( options.multipleSeparator ); // Mod by Acyuta to accept ' ' (empty char) as the separator
      var result = [];
      $.each(words, function(i, value) {
        if ( $.trim(value) )
            result[i] = $.trim(value);
      });
      return result;
  }
 
  function lastWord(value) {
      if ( !options.multiple )
        return value;
      var words = trimWords(value);
      return words[words.length - 1];
  }
 
  // fills in the input box w/the first match (assumed to be the best match)
  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 != 8 ) {
        // 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)
        $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
      }
  };
 
  function hideResults() {
      clearTimeout(timeout);
      timeout = setTimeout(hideResultsNow, 200);
  };
 
  function hideResultsNow() {
      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 ) $input.val("");
            }
        );
      }
  };
 
  function receiveData(q, data) {
      if ( data && data.length && hasFocus ) {
        stopLoading();
        select.display(data, q);
        autoFill(q, data[0].value);
        select.show();
      } else {
        hideResultsNow();
      }
  };


  //-------------------------------------------
// Create $ object for input element
  // HLS - duplicate of autocompleter "class"
var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
  function matchSubset(s, sub) {
      if (!options.matchCase)
        s = s.toLowerCase();
      var i = s.indexOf(sub);
      if (i == -1) return false;
      return i == 0 || options.matchContains;
  };
  //-------------------------------------------


  // HLS - reorganized the code for better exit reading
var timeout;
  function request(term, success, failure) {
var previousValue = "";
var cache = $.Autocompleter.Cache(options);
var hasFocus = 0;
var lastKeyPressCode;
var config = {
mouseDownOnSelect: false
};
var select = $.Autocompleter.Select(options, input, selectCurrent, config);
$input.keydown(function(event) {
// track last key pressed
lastKeyPressCode = event.keyCode;
switch(event.keyCode) {
case KEY.UP:
event.preventDefault();
if ( select.visible() ) {
select.prev();
} else {
onChange(0, true);
}
break;
case KEY.DOWN:
event.preventDefault();
if ( select.visible() ) {
select.next();
} else {
onChange(0, true);
}
break;
case KEY.PAGEUP:
event.preventDefault();
if ( select.visible() ) {
select.pageUp();
} else {
onChange(0, true);
}
break;
case KEY.PAGEDOWN:
event.preventDefault();
if ( select.visible() ) {
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() ){
// make sure to blur off the current field
if( !options.multiple )
$input.blur();
event.preventDefault();
}
break;
case KEY.ESC:
select.hide();
break;
default:
clearTimeout(timeout);
timeout = setTimeout(onChange, options.delay);
break;
}
}).keypress(function() {
// having fun with opera - remove this binding and Opera submits the form when we select an entry via return
}).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
if ( hasFocus++ > 1 && !select.visible() ) {
onChange(0, true);
}
}).bind("search", function() {
// TODO why not just specifying both arguments?
var fn = (arguments.length > 1) ? arguments[1] : null;
function findValueCallback(q, data) {
var result;
if( data && data.length ) {
for (var i=0; i < data.length; i++) {
if( data[i].result.toLowerCase() == q.toLowerCase() ) {
result = data[i];
break;
}
}
}
if( typeof fn == "function" ) fn(result);
else $input.trigger("result", result && [result.data, result.value]);
}
$.each(trimWords($input.val()), function(i, value) {
request(value, findValueCallback, findValueCallback);
});
}).bind("flushCache", function() {
cache.flush();
}).bind("setOptions", function() {
$.extend(options, arguments[1]);
// if we've updated the data, repopulate
if ( "data" in arguments[1] )
cache.populate();
}).bind("unautocomplete", function() {
select.unbind();
$input.unbind();
});
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 ) {
v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
}
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 [""];
}
var words = value.split( options.multipleSeparator );
var result = [];
$.each(words, function(i, value) {
if ( $.trim(value) )
result[i] = $.trim(value);
});
return result;
}
function lastWord(value) {
if ( !options.multiple )
return value;
var words = trimWords(value);
return words[words.length - 1];
}
// fills in the input box w/the first match (assumed to be the best match)
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 != 8 ) {
// 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)
$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
}
};


      if (!options.matchCase) term = term.toLowerCase();
function hideResults() {
clearTimeout(timeout);
timeout = setTimeout(hideResultsNow, 200);
};


      // recieve the cached data
function hideResultsNow() {
      var data = cache.load(term);
select.hide();
      if (data && data.length) {
clearTimeout(timeout);
        success(term, data);
stopLoading();
        lastAjaxCall.query  = lastWord(term); // HLS
if (options.mustMatch) {
        return true;                           // HLS
// call search and run callback
      }
$input.search(
function (result){
// if no value found, clear the input box
if( !result ) $input.val("");
}
);
}
};


      // if an AJAX url has been supplied, try loading the data now
function receiveData(q, data) {
      if( (typeof options.url == "string") && (options.url.length > 0) ){
if ( data && data.length && hasFocus ) {
stopLoading();
select.display(data, q);
autoFill(q, data[0].value);
select.show();
} else {
hideResultsNow();
}
};


        //-------------------------------------
function request(term, success, failure) {
        // HLS - skip ajax if failure is a quaranteed with
if (!options.matchCase)
        //       a sorted RPC resultant data set.
term = term.toLowerCase();
        //-------------------------------------
var data = cache.load(term);
        if (options.listIsSorted &&
// recieve the cached data
                lastAjaxCall.query && (lastAjaxCall.query != "")) {
if (data && data.length) {
            if (matchSubset(lastWord(term),lastAjaxCall.query)) {
success(term, data);
              //console.log("- LAC: Skip AJAX!");
// if an AJAX url has been supplied, try loading the data now
              stopLoading();
} else if( (typeof options.url == "string") && (options.url.length > 0) ){
              return true;
            }
var extraParams = {};
        }
$.each(options.extraParams, function(key, param) {
        //-------------------------------------
extraParams[key] = typeof param == "function" ? param() : param;
        //console.log("- ajax. term=",lastWord(term));
});
$.ajax({
// try to leverage ajaxQueue plugin to abort previous requests
mode: "abort",
// limit abortion to this input
port: "autocomplete" + input.name,
dataType: options.dataType,
url: options.url,
data: $.extend({
q: lastWord(term),
limit: options.max
}, extraParams),
success: function(data) {
var parsed = options.parse && options.parse(data) || parse(data);
cache.add(term, parsed);
success(term, parsed);
}
});
} else {
failure(term);
}
};
function parse(data) {
var parsed = [];
var rows = data.split("\n");
for (var i=0; i < rows.length; i++) {
var row = $.trim(rows[i]);
if (row) {
row = row.split("|");
parsed[parsed.length] = {
data: row,
value: row[0],
result: options.formatResult && options.formatResult(row, row[0]) || row[0]
};
}
}
return parsed;
};


        var extraParams = {};
function stopLoading() {
        $.each(options.extraParams, function(key, param) {
$input.removeClass(options.loadingClass);
            extraParams[key] = typeof param == "function" ? param() : param;
};
        });
 
        //-------------------------------------
        // HLS
        //-------------------------------------
        var furl  = options.url;
        var fdata = $.extend({q: lastWord(term), limit: options.max},
                              extraParams);
        if (options.formatUrl) {
            furl = options.formatUrl(furl, fdata);
            if (furl != options.url) fdata = {};
        }
        if (options.formatUrlData) {
            fdata = options.formatUrlData(fdata);
        }
 
        lastAjaxCall.query  = lastWord(term);
 
        var xhrOpts = {
            url: furl,
            data: fdata,
            error: function(){ stopLoading(); },
            success: function(data) {
                var parsed = options.parse &&
                              options.parse(data) || parse(data);
                cache.add(term, parsed);
                success(term, parsed);
              },
            mode: "abort",                    // leverage ajaxQueue plugin
            port: "autocomplete" + input.name // limit abortion
 
        };
        if (options.dataType) {
            xhrOpts.dataType = options.dataType;
        }
        //-------------------------------------
 
        // call ajax!
        $.ajax(xhrOpts);
        return true; // HLS
      }
 
      failure(term);
      return false; // HLS
  };
 
  function parse(data) {
      var parsed = [];
      var rows = data.split("\n");
      for (var i=0; i < rows.length; i++) {
        var row = $.trim(rows[i]);
        if (row) {
            row = row.split("|");
            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 = {
$.Autocompleter.defaults = {
  inputClass: "ac_input",
inputClass: "ac_input",
  resultsClass: "ac_results",
resultsClass: "ac_results",
  loadingClass: "ac_loading",
loadingClass: "ac_loading",
  overClass: "ac_over",        // HLS
minChars: 1,
  listIsSorted: true,          // HLS  reduces forward lookups
delay: 400,
  minChars: 1,
matchCase: false,
  delay: 400,
matchSubset: true,
  matchCase: false,
matchContains: false,
  matchSubset: true,
cacheLength: 10,
  matchContains: false,
max: 100,
  cacheLength: 10,
mustMatch: false,
  max: 100,
extraParams: {},
  mustMatch: false,
selectFirst: true,
  extraParams: {},
formatItem: function(row) { return row[0]; },
  selectFirst: true,
autoFill: false,
  formatItem: function(value) { return value[0]; }, // HLS - return value[0]
width: 0,
  //moreItems: "&#x25be;&#x25be;&#x25be; more &#x25be;&#x25be;&#x25be;",
multiple: false,
  moreItems: "",
multipleSeparator: ", ",
  autoFill: false,
highlight: function(value, term) {
  width: 0,
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
  multiple: false,
},
  multipleSeparator: ", ",
    scroll: true,
  highlight: function(value, term) {
    scrollHeight: 180,
      return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
attachTo: 'body'
  },
  scroll: true,
  scrollHeight: 180,
  scrollLimit: 0,        // HLS overrides scrollHeight
attachTo: 'body'
};
};


$.Autocompleter.Cache = function(options) {
$.Autocompleter.Cache = function(options) {


  var data = {};
var data = {};
  var length = 0;
var length = 0;
 
  function matchSubset(s, sub) {
function matchSubset(s, sub) {
      if (!options.matchCase)
if (!options.matchCase)  
        s = s.toLowerCase();
s = s.toLowerCase();
      var i = s.indexOf(sub);
var i = s.indexOf(sub);
      if (i == -1) return false;
if (i == -1) return false;
      return i == 0 || options.matchContains;
return i == 0 || options.matchContains;
  };
};
 
  function add(q, value) {
function add(q, value) {
      if (length > options.cacheLength){
if (length > options.cacheLength){
        flush();
flush();
      }
}
      if (!data[q]){
if (!data[q]){  
        length++;
length++;
      }
}
      data[q] = value;
data[q] = value;
  }
}
 
  function populate(){
function populate(){
      if( !options.data ) return false;
if( !options.data ) return false;
      // track the matches
// track the matches
      var stMatchSets = {},
var stMatchSets = {},
        nullData = 0;
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 row is a string, make an array otherwise just reference the array
       
        var value = options.formatItem(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
// no url was specified, we need to adjust the cache length to make sure it fits the local data store
        if ( nullData++ < options.max ) {
if( !options.url ) options.cacheLength = 1;
            stMatchSets[""].push(row);
        }
// 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.formatItem(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] = [];


      // add the data items to the cache
// if the match is a string
      $.each(stMatchSets, function(i, value) {
var row = {
        // increase the cache size
value: value,
        options.cacheLength++;
data: rawValue,
        // add to the cache
result: options.formatResult && options.formatResult(rawValue) || value
        add(i, value);
};
      });
  }
// push the current match into the set list
 
stMatchSets[firstChar].push(row);
  // populate any existing data
  setTimeout(populate, 25);


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


  return {
// add the data items to the cache
      flush: flush,
$.each(stMatchSets, function(i, value) {
      add: add,
// increase the cache size
      populate: populate,
options.cacheLength++;
      load: function(q) {
// add to the cache
        if (!options.cacheLength || !length)
add(i, value);
            return null;
});
        /*
}
          * if dealing w/local data and matchContains than we must make sure
          * to loop through all the data collections looking for matches
// populate any existing data
          */
setTimeout(populate, 25);
        if( !options.url && options.matchContains ){
            // track all matches
function flush(){
            var csub = [];
data = {};
            // loop through all the data grids for matches
length = 0;
            for( var k in data ){
}
              // don't search through the stMatchSets[""] (minChars: 0) cache
              // this prevents duplicates
return {
              if( k.length > 0 ){
flush: flush,
                  var c = data[k];
add: add,
                  $.each(c, function(i, x) {
populate: populate,
                    // if we've got a match, add it to the array
load: function(q) {
                    if (matchSubset(x.value, q)) {
if (!options.cacheLength || !length)
                        csub.push(x);
return null;
                    }
/*  
                  });
* if dealing w/local data and matchContains than we must make sure
              }
* to loop through all the data collections looking for matches
            }
*/
            return csub;
if( !options.url && options.matchContains ){
        } else
// track all matches
        // if the exact item exists, use it
var csub = [];
        if (data[q]){
// loop through all the data grids for matches
            return data[q];
for( var k in data ){
        } else
// don't search through the stMatchSets[""] (minChars: 0) cache
        if (options.matchSubset) {
// this prevents duplicates
            for (var i = q.length - 1; i >= options.minChars; i--) {
if( k.length > 0 ){
              var c = data[q.substr(0, i)];
var c = data[k];
              if (c) {
$.each(c, function(i, x) {
                  var csub = [];
// if we've got a match, add it to the array
                  $.each(c, function(i, x) {
if (matchSubset(x.value, q)) {
                    if (matchSubset(x.value, q)) {
csub.push(x);
                        csub[csub.length] = x;
}
                    }
});
                  });
}
                  return csub;
}
              }
return csub;
            }
} else  
        }
// if the exact item exists, use it
        return null;
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) {
$.Autocompleter.Select = function (options, input, select, config) {
  var CLASSES = {
var CLASSES = {
      ACTIVE: options.overClass  //HLS - was "ac_over"
ACTIVE: "ac_over"
  };
};
 
  var listItems,
var listItems,
      active = -1,
active = -1,
      data,
data,
      term = "",
term = "",
      needsInit = true,
needsInit = true,
      element,
element,
      list,
list;
      moreItems;
 
// Create results
  // Create results
function init() {
  function init() {
if (!needsInit)
      if (!needsInit)
return;
        return;
element = $("<div/>")
      element = $("<div>")
.hide()
      .hide()
.addClass(options.resultsClass)
      .addClass(options.resultsClass)
.css("position", "absolute")
      .css("position", "absolute")
.appendTo(options.attachTo);
      .appendTo(options.attachTo);
 
list = $("<ul>").appendTo(element).mouseover( function(event) {
      list = $("<ul>").appendTo(element).mouseover( function(event) {
if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
        if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
              active = $("li", list).removeClass().index(target(event));
    $(target(event)).addClass(CLASSES.ACTIVE);          
            $(target(event)).addClass(CLASSES.ACTIVE);
        }
          }
}).click(function(event) {
      }).click(function(event) {
$(target(event)).addClass(CLASSES.ACTIVE);
        $(target(event)).addClass(CLASSES.ACTIVE);
select();
        select();
input.focus();
        input.focus();
return false;
        return false;
}).mousedown(function() {
      }).mousedown(function() {
config.mouseDownOnSelect = true;
        config.mouseDownOnSelect = true;
}).mouseup(function() {
      }).mouseup(function() {
config.mouseDownOnSelect = false;
        config.mouseDownOnSelect = false;
});
      });
if( options.width > 0 )
element.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;
}


      if( options.moreItems.length > 0 )
function moveSelect(step) {
      moreItems = $("<div>")
listItems.slice(active, active + 1).removeClass();
        .addClass("ac_moreItems")
movePosition(step);
        .css("display", "none")
        var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
        .html(options.moreItems)
        if(options.scroll) {
        .appendTo(element);
 
      if( options.width > 0 )
        element.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_OLD(step) {
      listItems.slice(active, active + 1).removeClass();
      movePosition(step);
      var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
      if(options.scroll) {
             var offset = 0;
             var offset = 0;
             listItems.slice(0, active).each(function() {
             listItems.slice(0, active).each(function() {
                offset += this.offsetHeight;
offset += this.offsetHeight;
                });
});
             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
                 var ofs = offset + activeItem[0].offsetHeight - list.innerHeight();
                 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
                list.scrollTop(ofs);
             } else if(offset < list.scrollTop()) {
             } else if(offset < list.scrollTop()) {
                 list.scrollTop(offset);
                 list.scrollTop(offset);
             }
             }
      }
  };
  //------------------------------------
  // HLS
  //------------------------------------
  function boxScrollTop(v)
  // - move to the top of scroll
  {
      // set it
      if (typeof v == "number") {
          element[0].scrollTop = v;
          return v;
      }
      // read it
      return element[0].scrollTop;
  }
  function boxScrollBy(pixels)
  // - scroll by +/- pixel amount
  {
      element[0].scrollTop += pixels;
      return element[0].scrollTop;
  }
  function boxCurrentItem(i, onoff)
  // - unselect current or select new position
  {
      if (onoff) {
        return listItems.slice(i,i+1).addClass(CLASSES.ACTIVE);
      }
      return listItems.slice(i,i+1).removeClass(CLASSES.ACTIVE);
  }
  function boxCalcPixelJump(a1, a2)
  {
      var top1 = $(listItems[a1]).offset().top;
      var top2 = $(listItems[a2]).offset().top;
      return top2-top1;
  }
  function boxCalcScrollLimit(hLimit)
  // - return number of items within hLimit
  {
      if (hLimit > 0) {
        var h = 0;
        for (var i=0; i < listItems.length; i++) {
          h += listItems[i].clientHeight;
          if (h >= hLimit) return i+1;
         }
         }
      }
};
      return 0;
  }
function movePosition(step) {
 
active += step;
  function moveSelect(step)
if (active < 0) {
  // - move scroll cursor +/- steps
active = listItems.size() - 1;
  {
} else if (active >= listItems.size()) {
      var lastActive = active;
active = 0;
      boxCurrentItem(active, false);
}
      if (step > 0) {
}
          active += step;
          if (active >= listItems.size()) {
function limitNumberOfItems(available) {
            active = listItems.size() - 1;
return options.max && options.max < available
          } else {
? options.max
            var h = (active+1)*listItems[active].offsetHeight;
: available;
            if (h >= element[0].offsetHeight) {
}
                var pixels = boxCalcPixelJump(lastActive, active);
                boxScrollBy(pixels);
function fillList() {
            }
list.empty();
          }
var max = limitNumberOfItems(data.length);
      } else {
for (var i=0; i < max; i++) {
          active += step;
if (!data[i])
          if (active < 0) {
continue;
              active = 0;
var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
          } else {
if ( formatted === false )
            var h = active*listItems[active].offsetHeight;
continue;
            if ( h < element[0].scrollTop) {
var li = $("<li>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_event" : "ac_odd").appendTo(list)[0];
                var pixels = boxCalcPixelJump(active,lastActive)
$.data(li, "ac_data", data[i]);
                boxScrollBy(-1*pixels);
}
            }
listItems = list.find("li");
          }
if ( options.selectFirst ) {
      }
listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
      boxCurrentItem(active, true);
active = 0;
  };
}
 
list.bgiframe();
  function movePosition(step)
}
  // HLS - decrepated
  {
return {
      active += step;
display: function(d, q) {
      if (active < 0) {
init();
        active = listItems.size() - 1;
data = d;
      } else if (active >= listItems.size()) {
term = q;
        active = 0;
fillList();
      }
},
  }
next: function() {
 
moveSelect(1);
  //------------------------------------
},
 
prev: function() {
  function limitNumberOfItems(available) {
moveSelect(-1);
      return options.max && options.max < available
},
        ? options.max
pageUp: function() {
        : available;
if (active != 0 && active - 8 < 0) {
  }
moveSelect( -active );
 
} else {
  function fillList() {
moveSelect(-8);
      list.empty();
}
      var max = limitNumberOfItems(data.length);
},
      for (var i=0; i < max; i++) {
pageDown: function() {
        if (!data[i])
if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
            continue;
moveSelect( listItems.size() - 1 - active );
        var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
} else {
        if ( formatted === false )
moveSelect(8);
            continue;
}
        var li = $("<li>").html( options.highlight(formatted, term) ).appendTo(list)[0];
},
        $.data(li, "ac_data", data[i]);
hide: function() {
      }
element && element.hide();
      listItems = list.find("li");
active = -1;
      if ( options.selectFirst ) {
},
        listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
visible : function() {
        active = 0;
return element && element.is(":visible");
      }
},
      if( options.moreItems.length > 0 && !options.scroll)
current: function() {
        moreItems.css("display", (data.length > max)? "block" : "none");
return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
      // HLS
},
      //list.bgiframe();
show: function() {
      try { list.bgiframe(); } catch(e) { }
var offset = $(input).offset();
      //
element.css({
  }
width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
 
top: offset.top + input.offsetHeight,
  return {
left: offset.left
      display: function(d, q) {
}).show();
        init();
            if(options.scroll) {
        data = d;
                list.scrollTop(0);
        term = q;
                list.css({
        fillList();
maxHeight: options.scrollHeight,
      },
overflow: 'auto'
      next: function() {
});
        moveSelect(1);
      },
                if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
      prev: function() {
var listHeight = 0;
        moveSelect(-1);
listItems.each(function() {
      },
listHeight += this.offsetHeight;
      pageUp: function() {
});
        if (active != 0 && active - 8 < 0) {
var scrollbarsVisible = listHeight > options.scrollHeight;
            moveSelect( -active );
                    list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
        } else {
if (!scrollbarsVisible) {
            moveSelect(-8);
// IE doesn't recalculate width when scrollbar disappears
        }
listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
      },
}
      pageDown: function() {
                 }
        if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
               
            moveSelect( listItems.size() - 1 - active );
            }
        } else {
},
            moveSelect(8);
selected: function() {
        }
var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
      },
return selected && selected.length && $.data(selected[0], "ac_data");
      hide: function() {
},
        element && element.hide();
unbind: function() {
        active = -1;
element && element.remove();
      },
}
      visible : function() {
};
        return element && element.is(":visible");
      },
      current: function() {
        return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
      },
      show: function() {
        var offset = $(input).offset();
        element.css({
            width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
            top: offset.top + input.offsetHeight,
            left: offset.left
          }).show();
 
        if (options.scroll) {
 
            //------------------------------------------
            // HLS - Calc the height of the box. This is
            // assuming fixed row sizes. The logic
            // here is to only use the scrollLimit
            // if defined, otherwise use the
            // scrollHeight (backward compatibility)
            //------------------------------------------
 
            var bs    = {"overflow-y": "auto"};
            var hs    = "auto";
            var rh    = listItems[0].clientHeight;
            if ($.browser.msie) rh = listItems[0].offsetHeight;
 
            if (options.scrollLimit > 0) {
                if (listItems.size() > options.scrollLimit)
                    hs = options.scrollLimit*rh;
            } else {
                var n = boxCalcScrollLimit(options.scrollHeight);
                if (n>0) hs = n*rh;
            }
 
            if ($.browser.msie) {
                bs.height = hs;
            } else {
                 bs.maxHeight = hs;
            }
 
            element.css(bs);
            boxScrollTop(0);
            //------------------------------------------
        }
      },
      selected: function() {
        return listItems && $.data(listItems.filter("." + CLASSES.ACTIVE)[0], "ac_data");
      },
      unbind: function() {
        element && element.remove();
      }
  };
};
};


$.Autocompleter.Selection = function(field, start, end) {
$.Autocompleter.Selection = function(field, start, end) {
  if( field.createTextRange ){
if( field.createTextRange ){
      var selRange = field.createTextRange();
var selRange = field.createTextRange();
      selRange.collapse(true);
selRange.collapse(true);
      selRange.moveStart("character", start);
selRange.moveStart("character", start);
      selRange.moveEnd("character", end);
selRange.moveEnd("character", end);
      selRange.select();
selRange.select();
  } else if( field.setSelectionRange ){
} else if( field.setSelectionRange ){
      field.setSelectionRange(start, end);
field.setSelectionRange(start, end);
  } else {
} else {
      if( field.selectionStart ){
if( field.selectionStart ){
        field.selectionStart = start;
field.selectionStart = start;
        field.selectionEnd = end;
field.selectionEnd = end;
      }
}
  }
}
  field.focus();
field.focus();
};
};


})(jQuery);
})(jQuery);

Latest revision as of 14:53, 24 April 2008

/*
 * Autocomplete - jQuery plugin 1.0 Beta
 *
 * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
 *
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 *
 * Revision: $Id: jquery.autocomplete.js 4485 2008-01-20 13:52:47Z joern.zaefferer $
 *
 */

/**
 * Provide autocomplete for text-inputs or textareas.
 *
 * Depends on dimensions plugin's offset method for correct positioning of the select box and bgiframe plugin
 * to fix IE's problem with selects.
 *
 * @example $("#input_box").autocomplete("my_autocomplete_backend.php");
 * @before <input id="input_box" />
 * @desc Autocomplete a text-input with remote data. For small to giant datasets.
 *
 * When the user starts typing, a request is send to the specified backend ("my_autocomplete_backend.php"),
 * with a GET parameter named q that contains the current value of the input box and a paremeter "limit" with
 * the value specified for the max option.
 *
 * A value of "foo" would result in this request url: my_autocomplete_backend.php?q=foo&limit=10
 *
 * The result must return with one value on each line. The result is presented in the order
 * the backend sends it.
 *
 * @example $("#input_box").autocomplete(["Cologne", "Berlin", "Munich"]);
 * @before <input id="input_box" />
 * @desc Autcomplete a text-input with local data. For small datasets.
 *
 * @example $.getJSON("my_backend.php", function(data) {
 *   $("#input_box").autocomplete(data);
 * });
 * @before <input id="input_box" />
 * @desc Autcomplete a text-input with data received via AJAX. For small to medium sized datasets.
 *
 * @example $("#mytextarea").autocomplete(["Cologne", "Berlin", "Munich"], {
 *  multiple: true
 * });
 * @before <textarea id="mytextarea" />
 * @desc Autcomplete a textarea with local data (for small datasets). Once the user chooses one
 * value, a separator is appended (by default a comma, see multipleSeparator option) and more values
 * are autocompleted.
 *
 * @name autocomplete
 * @cat Plugins/Autocomplete
 * @type $
 * @param String|Array urlOrData Pass either an URL for remote-autocompletion or an array of data for local auto-completion
 * @param Map options Optional settings
 * @option String inputClass This class will be added to the input box. Default: "ac_input"
 * @option String resultsClass The class for the UL that will contain the result items (result items are LI elements). Default: "ac_results"
 * @option String loadingClass The class for the input box while results are being fetched from the server. Default: "ac_loading"
 * @option Number minChars The minimum number of characters a user has to type before the autocompleter activates. Default: 1
 * @option Number delay The delay in milliseconds the autocompleter waits after a keystroke to activate itself. Default: 400 for remote, 10 for local
 * @option Number cacheLength The number of backend query results to store in cache. If set to 1 (the current result), no caching will happen. Do not set below 1. Default: 10
 * @option Boolean matchSubset Whether or not the autocompleter can use a cache for more specific queries. This means that all matches of "foot" are a subset of all matches for "foo". Usually this is true, and using this options decreases server load and increases performance. Only useful with cacheLength settings bigger than one, like 10. Default: true
 * @option Boolean matchCase Whether or not the comparison is case sensitive. Important only if you use caching. Default: false
 * @option Boolean matchContains Whether or not the comparison looks inside (i.e. does "ba" match "foo bar") the search results. Important only if you use caching. Don't mix with autofill. Default: false
 * @option Booolean mustMatch If set to true, the autocompleter will only allow results that are presented by the backend. Note that illegal values result in an empty input box. Default: false
 * @option Object extraParams Extra parameters for the backend. If you were to specify { bar:4 }, the autocompleter would call my_autocomplete_backend.php?q=foo&bar=4 (assuming the input box contains "foo"). The param can be a function that is called to calculate the param before each request. Default: none
 * @option Boolean selectFirst If this is set to true, the first autocomplete value will be automatically selected on tab/return, even if it has not been handpicked by keyboard or mouse action. If there is a handpicked (highlighted) result, that result will take precedence. Default: true
 * @option Function formatItem Provides advanced markup for an item. For each row of results, this function will be called. The returned value will be displayed inside an LI element in the results list. Autocompleter will provide 4 parameters: the results row, the position of the row in the list of results (starting at 1), the number of items in the list of results and the search term. Default: none, assumes that a single row contains a single value.
 * @option Function formatResult Similar to formatItem, but provides the formatting for the value to be put into the input field. Again three arguments: Data, position (starting with one) and total number of data. Default: none, assumes either plain data to use as result or uses the same value as provided by formatItem.
 * @option Boolean multiple Whether to allow more than one autocomplted-value to enter. Default: false
 * @option String multipleSeparator Seperator to put between values when using multiple option. Default: ", "
 * @option Number width Specify a custom width for the select box. Default: width of the input element
 * @option Boolean autoFill Fill the textinput while still selecting a value, replacing the value if more is typed or something else is selected. Default: false
 * @option Number max Limit the number of items in the select box. Is also sent as a "limit" parameter with a remote request. Default: 10
 * @option Boolean|Function highlight Whether and how to highlight matches in the select box. Set to false to disable. Set to a function to customize. The function gets the value as the first argument and the search term as the second and must return the formatted value. Default: Wraps the search term in a <strong> element 
 * @option Boolean scroll Whether to scroll when more results than configured via scrollHeight are available. Default: true 
 * @option Number scrollHeight height of scrolled autocomplete control in pixels
 * @option String attachTo The element to attach the autocomplete list to. Useful if used inside a modal window like Thickbox. Default: body -MM
 */

/**
 * Handle the result of a search event. Is executed when the user selects a value or a
 * programmatic search event is triggered (see search()).
 *
 * You can add and remove (using unbind("result")) this event at any time.
 *
 * @example $('input#suggest').result(function(event, data, formatted) {
 *   $("#result").html( !data ? "No match!" : "Selected: " + formatted);
 * });
 * @desc Bind a handler to the result event to display the selected value in a #result element.
 *    The first argument is a generic event object, in this case with type "result".
 *    The second argument refers to the selected data, which can be a plain string value or an array or object.
 *    The third argument is the formatted value that is inserted into the input field.
 *
 * @param Function handler The event handler, gets a default event object as first and
 * 		the selected list item as second argument.
 * @name result
 * @cat Plugins/Autocomplete
 * @type $
 */

/**
 * Trigger a search event. See result(Function) for binding to that event.
 *
 * A search event mimics the same behaviour as when the user selects a value from
 * the list of autocomplete items. You can use it to execute anything that does something
 * with the selected value, beyond simply putting the value into the input and submitting it.
 *
 * @example $('input#suggest').search();
 * @desc Triggers a search event.
 *
 * @name search
 * @cat Plugins/Autocomplete
 * @type $
 */
 
/**
 * Flush (empty) the cache of matched input's autocompleters.
 *
 * @example $('input#suggest').flushCache();
 *
 * @name flushCache
 * @cat Plugins/Autocomplete
 * @type $
 */

/**
 * Updates the options for the current autocomplete field. This allows 
 * you to change things like the URL, max items to display, etc. If you're
 * changing the URL, be sure to remember to call the flushCache() method.
 *
 * @example $('input#suggest').setOptions({
 *  max: 15
 * });
 * @desc Changes the maximum number of items to display to 15.
 *
 * @name setOptions
 * @cat Plugins/Autocomplete
 * @type $
 */

;(function($) {
	
$.fn.extend({
	autocomplete: function(urlOrData, options) {
		var isUrl = typeof urlOrData == "string";
		options = $.extend({}, $.Autocompleter.defaults, {
			url: isUrl ? urlOrData : null,
			data: isUrl ? null : urlOrData,
			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
			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; };
		
		return this.each(function() {
			new $.Autocompleter(this, options);
		});
	},
	result: function(handler) {
		return this.bind("result", handler);
	},
	search: function(handler) {
		return this.trigger("search", [handler]);
	},
	flushCache: function() {
		return this.trigger("flushCache");
	},
	setOptions: function(options){
		return this.trigger("setOptions", [options]);
	},
	unautocomplete: function() {
		return this.trigger("unautocomplete");
	}
});

$.Autocompleter = function(input, options) {

	var KEY = {
		UP: 38,
		DOWN: 40,
		DEL: 46,
		TAB: 9,
		RETURN: 13,
		ESC: 27,
		COMMA: 188,
		PAGEUP: 33,
		PAGEDOWN: 34
	};

	// 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);
	
	$input.keydown(function(event) {
		// track last key pressed
		lastKeyPressCode = event.keyCode;
		switch(event.keyCode) {
		
			case KEY.UP:
				event.preventDefault();
				if ( select.visible() ) {
					select.prev();
				} else {
					onChange(0, true);
				}
				break;
				
			case KEY.DOWN:
				event.preventDefault();
				if ( select.visible() ) {
					select.next();
				} else {
					onChange(0, true);
				}
				break;
				
			case KEY.PAGEUP:
				event.preventDefault();
				if ( select.visible() ) {
					select.pageUp();
				} else {
					onChange(0, true);
				}
				break;
				
			case KEY.PAGEDOWN:
				event.preventDefault();
				if ( select.visible() ) {
					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() ){
					// make sure to blur off the current field
					if( !options.multiple )
						$input.blur();
					event.preventDefault();
				}
				break;
				
			case KEY.ESC:
				select.hide();
				break;
				
			default:
				clearTimeout(timeout);
				timeout = setTimeout(onChange, options.delay);
				break;
		}
	}).keypress(function() {
		// having fun with opera - remove this binding and Opera submits the form when we select an entry via return
	}).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
		if ( hasFocus++ > 1 && !select.visible() ) {
			onChange(0, true);
		}
	}).bind("search", function() {
		// TODO why not just specifying both arguments?
		var fn = (arguments.length > 1) ? arguments[1] : null;
		function findValueCallback(q, data) {
			var result;
			if( data && data.length ) {
				for (var i=0; i < data.length; i++) {
					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
						result = data[i];
						break;
					}
				}
			}
			if( typeof fn == "function" ) fn(result);
			else $input.trigger("result", result && [result.data, result.value]);
		}
		$.each(trimWords($input.val()), function(i, value) {
			request(value, findValueCallback, findValueCallback);
		});
	}).bind("flushCache", function() {
		cache.flush();
	}).bind("setOptions", function() {
		$.extend(options, arguments[1]);
		// if we've updated the data, repopulate
		if ( "data" in arguments[1] )
			cache.populate();
	}).bind("unautocomplete", function() {
		select.unbind();
		$input.unbind();
	});
	
	
	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 ) {
				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
			}
			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 [""];
		}
		var words = value.split( options.multipleSeparator );
		var result = [];
		$.each(words, function(i, value) {
			if ( $.trim(value) )
				result[i] = $.trim(value);
		});
		return result;
	}
	
	function lastWord(value) {
		if ( !options.multiple )
			return value;
		var words = trimWords(value);
		return words[words.length - 1];
	}
	
	// fills in the input box w/the first match (assumed to be the best match)
	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 != 8 ) {
			// 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)
			$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
		}
	};

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

	function hideResultsNow() {
		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 ) $input.val("");
				}
			);
		}
	};

	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);
		// if an AJAX url has been supplied, try loading the data now
		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
			
			var extraParams = {};
			$.each(options.extraParams, function(key, param) {
				extraParams[key] = typeof param == "function" ? param() : param;
			});
			
			$.ajax({
				// try to leverage ajaxQueue plugin to abort previous requests
				mode: "abort",
				// limit abortion to this input
				port: "autocomplete" + input.name,
				dataType: options.dataType,
				url: options.url,
				data: $.extend({
					q: lastWord(term),
					limit: options.max
				}, extraParams),
				success: function(data) {
					var parsed = options.parse && options.parse(data) || parse(data);
					cache.add(term, parsed);
					success(term, parsed);
				}
			});
		} else {
			failure(term);
		}
	};
	
	function parse(data) {
		var parsed = [];
		var rows = data.split("\n");
		for (var i=0; i < rows.length; i++) {
			var row = $.trim(rows[i]);
			if (row) {
				row = row.split("|");
				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: "ac_input",
	resultsClass: "ac_results",
	loadingClass: "ac_loading",
	minChars: 1,
	delay: 400,
	matchCase: false,
	matchSubset: true,
	matchContains: false,
	cacheLength: 10,
	max: 100,
	mustMatch: false,
	extraParams: {},
	selectFirst: true,
	formatItem: function(row) { return row[0]; },
	autoFill: false,
	width: 0,
	multiple: false,
	multipleSeparator: ", ",
	highlight: function(value, term) {
		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
	},
    scroll: true,
    scrollHeight: 180,
	attachTo: 'body'
};

$.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 (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.formatItem(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: "ac_over"
	};
	
	var listItems,
		active = -1,
		data,
		term = "",
		needsInit = true,
		element,
		list;
	
	// Create results
	function init() {
		if (!needsInit)
			return;
		element = $("<div/>")
		.hide()
		.addClass(options.resultsClass)
		.css("position", "absolute")
		.appendTo(options.attachTo);
	
		list = $("<ul>").appendTo(element).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();
			input.focus();
			return false;
		}).mousedown(function() {
			config.mouseDownOnSelect = true;
		}).mouseup(function() {
			config.mouseDownOnSelect = false;
		});
		
		if( options.width > 0 )
			element.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();
		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) {
		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;
			var li = $("<li>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_event" : "ac_odd").appendTo(list)[0];
			$.data(li, "ac_data", data[i]);
		}
		listItems = list.find("li");
		if ( options.selectFirst ) {
			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
			active = 0;
		}
		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() {
			element && element.hide();
			active = -1;
		},
		visible : function() {
			return element && element.is(":visible");
		},
		current: function() {
			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
		},
		show: function() {
			var offset = $(input).offset();
			element.css({
				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
				top: offset.top + input.offsetHeight,
				left: offset.left
			}).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);
			return selected && selected.length && $.data(selected[0], "ac_data");
		},
		unbind: function() {
			element && element.remove();
		}
	};
};

$.Autocompleter.Selection = function(field, start, end) {
	if( field.createTextRange ){
		var selRange = field.createTextRange();
		selRange.collapse(true);
		selRange.moveStart("character", start);
		selRange.moveEnd("character", end);
		selRange.select();
	} else if( field.setSelectionRange ){
		field.setSelectionRange(start, end);
	} else {
		if( field.selectionStart ){
			field.selectionStart = start;
			field.selectionEnd = end;
		}
	}
	field.focus();
};

})(jQuery);