From Mustard Goat, 5 Years ago, written in Plain Text.
Embed
  1. jQuery.autocomplete = function(input, options) {
  2.         // Create a link to self
  3.         var me = this;
  4.  
  5.         // Create jQuery object for input element
  6.         var $input = $(input).attr("autocomplete", "off");
  7.  
  8.         // Apply inputClass if necessary
  9.         if (options.inputClass) $input.addClass(options.inputClass);
  10.  
  11.         // Create results
  12.         var results = document.createElement("div");
  13.         // Create jQuery object for results
  14.         var $results = $(results);
  15.         $results.hide().addClass(options.resultsClass).css("position", "absolute");
  16.         if( options.width > 0 ) $results.css("width", options.width);
  17.  
  18.         // Add to body element
  19.         $("body").append(results);
  20.  
  21.         input.autocompleter = me;
  22.  
  23.         var timeout = null;
  24.         var prev = "";
  25.         var active = -1;
  26.         var cache = {};
  27.         var keyb = false;
  28.         var hasFocus = false;
  29.         var lastKeyPressCode = null;
  30.  
  31.         // flush cache
  32.         function flushCache(){
  33.                 cache = {};
  34.                 cache.data = {};
  35.                 cache.length = 0;
  36.         };
  37.  
  38.         // flush cache
  39.         flushCache();
  40.  
  41.         // if there is a data array supplied
  42.         if( options.data != null ){
  43.                 var sFirstChar = "", stMatchSets = {}, row = [];
  44.  
  45.                 // no url was specified, we need to adjust the cache length to make sure it fits the local data store
  46.                 if( typeof options.url != "string" ) options.cacheLength = 1;
  47.  
  48.                 // loop through the array and create a lookup structure
  49.                 for( var i=0; i < options.data.length; i++ ){
  50.                         // if row is a string, make an array otherwise just reference the array
  51.                         row = ((typeof options.data[i] == "string") ? [options.data[i]] : options.data[i]);
  52.  
  53.                         // if the length is zero, don't add to list
  54.                         if( row[0].length > 0 ){
  55.                                 // get the first character
  56.                                 sFirstChar = row[0].substring(0, 1).toLowerCase();
  57.                                 // if no lookup array for this character exists, look it up now
  58.                                 if( !stMatchSets[sFirstChar] ) stMatchSets[sFirstChar] = [];
  59.                                 // if the match is a string
  60.                                 stMatchSets[sFirstChar].push(row);
  61.                         }
  62.                 }
  63.  
  64.                 // add the data items to the cache
  65.                 for( var k in stMatchSets ){
  66.                         // increase the cache size
  67.                         options.cacheLength++;
  68.                         // add to the cache
  69.                         addToCache(k, stMatchSets[k]);
  70.                 }
  71.         }
  72.  
  73.         $input
  74.         .keydown(function(e) {
  75.                 // track last key pressed
  76.                 lastKeyPressCode = e.keyCode;
  77.                 switch(e.keyCode) {
  78.                         case 38: // up
  79.                                 e.preventDefault();
  80.                                 moveSelect(-1);
  81.                                 break;
  82.                         case 40: // down
  83.                                 e.preventDefault();
  84.                                 moveSelect(1);
  85.                                 break;
  86.                         case 9:  // tab
  87.                         case 13: // return
  88.                                 if( selectCurrent() ){
  89.                                         // make sure to blur off the current field
  90.                                         $input.get(0).blur();
  91.                                         e.preventDefault();
  92.                                 }
  93.                                 break;
  94.                         default:
  95.                                 active = -1;
  96.                                 if (timeout) clearTimeout(timeout);
  97.                                 timeout = setTimeout(function(){onChange();}, options.delay);
  98.                                 break;
  99.                 }
  100.         })
  101.         .focus(function(){
  102.                 // track whether the field has focus, we shouldn't process any results if the field no longer has focus
  103.                 hasFocus = true;
  104.         })
  105.         .blur(function() {
  106.                 // track whether the field has focus
  107.                 hasFocus = false;
  108.                 hideResults();
  109.         });
  110.  
  111.         hideResultsNow();
  112.  
  113.         function onChange() {
  114.                 // ignore if the following keys are pressed: [del] [shift] [capslock]
  115.                 if( lastKeyPressCode == 46 || (lastKeyPressCode > 8 && lastKeyPressCode < 32) ) return $results.hide();
  116.                 var v = $input.val();
  117.                 if (v == prev) return;
  118.                 prev = v;
  119.                 if (v.length >= options.minChars) {
  120.                         $input.addClass(options.loadingClass);
  121.                         requestData(v);
  122.                 } else {
  123.                         $input.removeClass(options.loadingClass);
  124.                         $results.hide();
  125.                 }
  126.         };
  127.  
  128.         function moveSelect(step) {
  129.  
  130.                 var lis = $("li", results);
  131.                 if (!lis) return;
  132.  
  133.                 active += step;
  134.  
  135.                 if (active < 0) {
  136.                         active = 0;
  137.                 } else if (active >= lis.size()) {
  138.                         active = lis.size() - 1;
  139.                 }
  140.  
  141.                 lis.removeClass("ac_over");
  142.  
  143.                 $(lis[active]).addClass("ac_over");
  144.  
  145.                 // Weird behaviour in IE
  146.                 // if (lis[active] && lis[active].scrollIntoView) {
  147.                 //      lis[active].scrollIntoView(false);
  148.                 // }
  149.  
  150.         };
  151.  
  152.         function selectCurrent() {
  153.                 var li = $("li.ac_over", results)[0];
  154.                 if (!li) {
  155.                         var $li = $("li", results);
  156.                         if (options.selectOnly) {
  157.                                 if ($li.length == 1) li = $li[0];
  158.                         } else if (options.selectFirst) {
  159.                                 li = $li[0];
  160.                         }
  161.                 }
  162.                 if (li) {
  163.                         selectItem(li);
  164.                         return true;
  165.                 } else {
  166.                         return false;
  167.                 }
  168.         };
  169.  
  170.         function selectItem(li) {
  171.                 if (!li) {
  172.                         li = document.createElement("li");
  173.                         li.extra = [];
  174.                         li.selectValue = "";
  175.                 }
  176.                 var v = $.trim(li.selectValue ? li.selectValue : li.innerHTML);
  177.                 input.lastSelected = v;
  178.                 prev = v;
  179.                 $results.html("");
  180.                 $input.val(v);
  181.                 hideResultsNow();
  182.                 if (options.onItemSelect) setTimeout(function() { options.onItemSelect(li) }, 1);
  183.         };
  184.  
  185.         // selects a portion of the input string
  186.         function createSelection(start, end){
  187.                 // get a reference to the input element
  188.                 var field = $input.get(0);
  189.                 if( field.createTextRange ){
  190.                         var selRange = field.createTextRange();
  191.                         selRange.collapse(true);
  192.                         selRange.moveStart("character", start);
  193.                         selRange.moveEnd("character", end);
  194.                         selRange.select();
  195.                 } else if( field.setSelectionRange ){
  196.                         field.setSelectionRange(start, end);
  197.                 } else {
  198.                         if( field.selectionStart ){
  199.                                 field.selectionStart = start;
  200.                                 field.selectionEnd = end;
  201.                         }
  202.                 }
  203.                 field.focus();
  204.         };
  205.  
  206.         // fills in the input box w/the first match (assumed to be the best match)
  207.         function autoFill(sValue){
  208.                 // if the last user key pressed was backspace, don't autofill
  209.                 if( lastKeyPressCode != 8 ){
  210.                         // fill in the value (keep the case the user has typed)
  211.                         $input.val($input.val() + sValue.substring(prev.length));
  212.                         // select the portion of the value not typed by the user (so the next character will erase)
  213.                         createSelection(prev.length, sValue.length);
  214.                 }
  215.         };
  216.  
  217.         function showResults() {
  218.                 // get the position of the input field right now (in case the DOM is shifted)
  219.                 var pos = findPos(input);
  220.                 // either use the specified width, or autocalculate based on form element
  221.                 var iWidth = (options.width > 0) ? options.width : $input.width();
  222.                 // reposition
  223.                 $results.css({
  224.                         width: parseInt(iWidth) + "px",
  225.                         top: (pos.y + input.offsetHeight) + "px",
  226.                         left: pos.x + "px"
  227.                 }).show();
  228.         };
  229.  
  230.         function hideResults() {
  231.                 if (timeout) clearTimeout(timeout);
  232.                 timeout = setTimeout(hideResultsNow, 200);
  233.         };
  234.  
  235.         function hideResultsNow() {
  236.                 if (timeout) clearTimeout(timeout);
  237.                 $input.removeClass(options.loadingClass);
  238.                 if ($results.is(":visible")) {
  239.                         $results.hide();
  240.                 }
  241.                 if (options.mustMatch) {
  242.                         var v = $input.val();
  243.                         if (v != input.lastSelected) {
  244.                                 selectItem(null);
  245.                         }
  246.                 }
  247.         };
  248.  
  249.         function receiveData(q, data) {
  250.                 if (data) {
  251.                         $input.removeClass(options.loadingClass);
  252.                         results.innerHTML = "";
  253.  
  254.                         // if the field no longer has focus or if there are no matches, do not display the drop down
  255.                         if( !hasFocus || data.length == 0 ) return hideResultsNow();
  256.  
  257.                         if ($.browser.msie) {
  258.                                 // we put a styled iframe behind the calendar so HTML SELECT elements don't show through
  259.                                 $results.append(document.createElement('iframe'));
  260.                         }
  261.                         results.appendChild(dataToDom(data));
  262.                         // autofill in the complete box w/the first match as long as the user hasn't entered in more data
  263.                         if( options.autoFill && ($input.val().toLowerCase() == q.toLowerCase()) ) autoFill(data[0][0]);
  264.                         showResults();
  265.                 } else {
  266.                         hideResultsNow();
  267.                 }
  268.         };
  269.  
  270.         function parseData(data) {
  271.                 if (!data) return null;
  272.                 var parsed = [];
  273.                 var rows = data.split(options.lineSeparator);
  274.                 for (var i=0; i < rows.length; i++) {
  275.                         var row = $.trim(rows[i]);
  276.                         if (row) {
  277.                                 parsed[parsed.length] = row.split(options.cellSeparator);
  278.                         }
  279.                 }
  280.                 return parsed;
  281.         };
  282.  
  283.         function dataToDom(data) {
  284.                 var ul = document.createElement("ul");
  285.                 var num = data.length;
  286.  
  287.                 // limited results to a max number
  288.                 if( (options.maxItemsToShow > 0) && (options.maxItemsToShow < num) ) num = options.maxItemsToShow;
  289.  
  290.                 for (var i=0; i < num; i++) {
  291.                         var row = data[i];
  292.                         if (!row) continue;
  293.                         var li = document.createElement("li");
  294.                         if (options.formatItem) {
  295.                                 li.innerHTML = options.formatItem(row, i, num);
  296.                                 li.selectValue = row[0];
  297.                         } else {
  298.                                 li.innerHTML = row[0];
  299.                                 li.selectValue = row[0];
  300.                         }
  301.                         var extra = null;
  302.                         if (row.length > 1) {
  303.                                 extra = [];
  304.                                 for (var j=1; j < row.length; j++) {
  305.                                         extra[extra.length] = row[j];
  306.                                 }
  307.                         }
  308.                         li.extra = extra;
  309.                         ul.appendChild(li);
  310.                         $(li).hover(
  311.                                 function() { $("li", ul).removeClass("ac_over"); $(this).addClass("ac_over"); active = $("li", ul).indexOf($(this).get(0)); },
  312.                                 function() { $(this).removeClass("ac_over"); }
  313.                         ).click(function(e) { e.preventDefault(); e.stopPropagation(); selectItem(this) });
  314.                 }
  315.                 return ul;
  316.         };
  317.  
  318.         function requestData(q) {
  319.                 if (!options.matchCase) q = q.toLowerCase();
  320.                 var data = options.cacheLength ? loadFromCache(q) : null;
  321.                 // recieve the cached data
  322.                 if (data) {
  323.                         receiveData(q, data);
  324.                 // if an AJAX url has been supplied, try loading the data now
  325.                 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  326.                         $.get(makeUrl(q), function(data) {
  327.                                 data = parseData(data);
  328.                                 addToCache(q, data);
  329.                                 receiveData(q, data);
  330.                         });
  331.                 // if there's been no data found, remove the loading class
  332.                 } else {
  333.                         $input.removeClass(options.loadingClass);
  334.                 }
  335.         };
  336.  
  337.         function makeUrl(q) {
  338.                 var url = options.url + "?q=" + encodeURI(q);
  339.                 for (var i in options.extraParams) {
  340.                         url += "&" + i + "=" + encodeURI(options.extraParams[i]);
  341.                 }
  342.                 return url;
  343.         };
  344.  
  345.         function loadFromCache(q) {
  346.                 if (!q) return null;
  347.                 if (cache.data[q]) return cache.data[q];
  348.                 if (options.matchSubset) {
  349.                         for (var i = q.length - 1; i >= options.minChars; i--) {
  350.                                 var qs = q.substr(0, i);
  351.                                 var c = cache.data[qs];
  352.                                 if (c) {
  353.                                         var csub = [];
  354.                                         for (var j = 0; j < c.length; j++) {
  355.                                                 var x = c[j];
  356.                                                 var x0 = x[0];
  357.                                                 if (matchSubset(x0, q)) {
  358.                                                         csub[csub.length] = x;
  359.                                                 }
  360.                                         }
  361.                                         return csub;
  362.                                 }
  363.                         }
  364.                 }
  365.                 return null;
  366.         };
  367.  
  368.         function matchSubset(s, sub) {
  369.                 if (!options.matchCase) s = s.toLowerCase();
  370.                 var i = s.indexOf(sub);
  371.                 if (i == -1) return false;
  372.                 return i == 0 || options.matchContains;
  373.         };
  374.  
  375.         this.flushCache = function() {
  376.                 flushCache();
  377.         };
  378.  
  379.         this.setExtraParams = function(p) {
  380.                 options.extraParams = p;
  381.         };
  382.  
  383.         this.findValue = function(){
  384.                 var q = $input.val();
  385.  
  386.                 if (!options.matchCase) q = q.toLowerCase();
  387.                 var data = options.cacheLength ? loadFromCache(q) : null;
  388.                 if (data) {
  389.                         findValueCallback(q, data);
  390.                 } else if( (typeof options.url == "string") && (options.url.length > 0) ){
  391.                         $.get(makeUrl(q), function(data) {
  392.                                 data = parseData(data)
  393.                                 addToCache(q, data);
  394.                                 findValueCallback(q, data);
  395.                         });
  396.                 } else {
  397.                         // no matches
  398.                         findValueCallback(q, null);
  399.                 }
  400.         }
  401.  
  402.         function findValueCallback(q, data){
  403.                 if (data) $input.removeClass(options.loadingClass);
  404.  
  405.                 var num = (data) ? data.length : 0;
  406.                 var li = null;
  407.  
  408.                 for (var i=0; i < num; i++) {
  409.                         var row = data[i];
  410.  
  411.                         if( row[0].toLowerCase() == q.toLowerCase() ){
  412.                                 li = document.createElement("li");
  413.                                 if (options.formatItem) {
  414.                                         li.innerHTML = options.formatItem(row, i, num);
  415.                                         li.selectValue = row[0];
  416.                                 } else {
  417.                                         li.innerHTML = row[0];
  418.                                         li.selectValue = row[0];
  419.                                 }
  420.                                 var extra = null;
  421.                                 if( row.length > 1 ){
  422.                                         extra = [];
  423.                                         for (var j=1; j < row.length; j++) {
  424.                                                 extra[extra.length] = row[j];
  425.                                         }
  426.                                 }
  427.                                 li.extra = extra;
  428.                         }
  429.                 }
  430.  
  431.                 if( options.onFindValue ) setTimeout(function() { options.onFindValue(li) }, 1);
  432.         }
  433.  
  434.         function addToCache(q, data) {
  435.                 if (!data || !q || !options.cacheLength) return;
  436.                 if (!cache.length || cache.length > options.cacheLength) {
  437.                         flushCache();
  438.                         cache.length++;
  439.                 } else if (!cache[q]) {
  440.                         cache.length++;
  441.                 }
  442.                 cache.data[q] = data;
  443.         };
  444.  
  445.         function findPos(obj) {
  446.                 var curleft = obj.offsetLeft || 0;
  447.                 var curtop = obj.offsetTop || 0;
  448.                 while (obj = obj.offsetParent) {
  449.                         curleft += obj.offsetLeft
  450.                         curtop += obj.offsetTop
  451.                 }
  452.                 return {x:curleft,y:curtop};
  453.         }
  454. }
  455.  
  456. jQuery.fn.autocomplete = function(url, options, data) {
  457.         // Make sure options exists
  458.         options = options || {};
  459.         // Set url as option
  460.         options.url = url;
  461.         // set some bulk local data
  462.         options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null;
  463.  
  464.         // Set default values for required options
  465.         options.inputClass = options.inputClass || "ac_input";
  466.         options.resultsClass = options.resultsClass || "ac_results";
  467.         options.lineSeparator = options.lineSeparator || "\n";
  468.         options.cellSeparator = options.cellSeparator || "|";
  469.         options.minChars = options.minChars || 1;
  470.         options.delay = options.delay || 400;
  471.         options.matchCase = options.matchCase || 0;
  472.         options.matchSubset = options.matchSubset || 1;
  473.         options.matchContains = options.matchContains || 0;
  474.         options.cacheLength = options.cacheLength || 1;
  475.         options.mustMatch = options.mustMatch || 0;
  476.         options.extraParams = options.extraParams || {};
  477.         options.loadingClass = options.loadingClass || "ac_loading";
  478.         options.selectFirst = options.selectFirst || false;
  479.         options.selectOnly = options.selectOnly || false;
  480.         options.maxItemsToShow = options.maxItemsToShow || -1;
  481.         options.autoFill = options.autoFill || false;
  482.         options.width = parseInt(options.width, 10) || 0;
  483.  
  484.         this.each(function() {
  485.                 var input = this;
  486.                 new jQuery.autocomplete(input, options);
  487.         });
  488.  
  489.         // Don't break the chain
  490.         return this;
  491. }
  492.  
  493. jQuery.fn.autocompleteArray = function(data, options) {
  494.         return this.autocomplete(null, options, data);
  495. }
  496.  
  497. jQuery.fn.indexOf = function(e){
  498.         for( var i=0; i<this.length; i++ ){
  499.                 if( this[i] == e ) return i;
  500.         }
  501.         return -1;
  502. };