Skip to content

Instantly share code, notes, and snippets.

@jherdman
Created June 15, 2009 15:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jherdman/130174 to your computer and use it in GitHub Desktop.
Save jherdman/130174 to your computer and use it in GitHub Desktop.
/**
* This is a heaviy modified version of Peter Vulgaris' jquery.autosuggest
* plugin. In fact, you're probably going to need to do some heavy squinting
* to recognize the original code.
*
* What's Different?
* =================
*
* Cleaner Code
* ------------
*
* The entire plugin has been adapted to look a little more jQuery-ish.
*
* JSON-centric
* ------------
*
* Vulgaris' code assumed the server was returning a String. So, we would send
* something back like "Apples,Oranges,Bananas". We'd parse this string, and
* then build up a list. This is fine if what we're after is a String.
* We aren't. We want the ID of some database entry, and possibly more. For
* this reason we deal only with JSON.
*
* Be sure to read the documentation below on how to appropriately deal with
* this JSON data.
*
* Original Header
* ===============
*
* jquery.suggest 1.1 - 2007-08-06
*
* Uses code and techniques from following libraries:
* 1. http://www.dyve.net/jquery/?autocomplete
* 2. http://dev.jquery.com/browser/trunk/plugins/interface/iautocompleter.js
*
* All the new stuff written by Peter Vulgaris (www.vulgarisoip.com)
* Feel free to do whatever you want with this file
*/
;(function ($) {
$.fn.extend({
/**
* Positions some element below another.
*
* @param {jQuery} that the element to position the caller below
*/
positionBelow: function (that) {
var $this = $(this);
var offset = that.offset();
$this.css({
top: (offset.top + that.outerHeight()) + "px",
left: offset.left + "px"
});
return $this;
},
/**
* This is the method that establishes a suggestor on one or many text field
* INPUTs. The selected item will have the JSON object it is associated with
* cached using jQuery.data.
*
* @param {Hash} options an collection of key/value pairs to customize this
* plugin's behaviour.
*
* @option {String} :source the URL that will return results of interest.
* This parameter is mandatory.
* @option {String} :delay milliseconds to delay by
* @option {String} :resultsClass CSS class to apply to the results
* @option {String} :selectClass CSS class to apply to the selected result
* @option {String} :matchClass CSS class to apply to the match
* @option {Integer} :minchars minimum number of entered characters before
* a query is made
* @option {Function} :onSelect what to do when a match is selected. This
* function will be executed in the context of the jQuery object that
* 'suggest' is attahed to.
* @option {Integer} :maxCacheSize the maximum number of entries in the cache
* @option {Hash} :jsonOpts data to send on the JSON request for
* suggestions.
* @option {String} :label which JSON attribute to use to display to the end
* user for suggestions
* @option {String} :namespace if your JSON is namespaced, use this option
* to inform the plugin of this namespace so that the data contained may
* be accessed.
* @option {true,false} :bgiframe set to true if you wish to use the
* bgiframe plugin to help out IE6 users
*/
suggest: function (options) {
if (!options.source) { return; }
var defaults = {
delay: 100,
resultsClass: "ac_results",
selectClass: "ac_over",
matchClass: "ac_match",
minchars: 2,
onSelect: false,
maxCacheSize: 65536,
jsonOpts: {},
label: "name",
namespace: null,
bgiframe: false
};
options = $.extend({}, defaults, options);
this.each(function () { $.suggest(this, options); });
return this;
}});
$.extend({
suggest: function (input, options) {
// This is the field that we are watching to trigger suggesting on
var $input = $(input).attr("autocomplete", "off");
// This is where the results of searching are rendered
var $results = $(document.createElement("ul"));
var timeout = false; // hold timeout ID for suggestion results to appear
var prevLength = 0; // last recorded length of $input.val()
var cache = []; // cache MRU list
var cacheSize = 0; // size of cache in chars (bytes?)
$results
.addClass(options.resultsClass)
.appendTo("body");
// When the 'blur' event is triggered, hide the results box 200ms after.
// This gives the user a chance to change their mind.
$input.blur(function () {
setTimeout(function () { $results.hide(); }, 200);
});
// Try to call the bgiframe plugin to aid with IE users and z-index woes.
if (options.bgiframe) { $results.bgiframe(); }
// Immediately reset the position of our target INPUT element. We also
// establish hooks to the window's "load" and "resize" events in case the
// user happens to resize the window.
$results.positionBelow($input);
$(window)
.load(function () { $results.positionBelow($input); })
.resize(function () { $results.positionBelow($input); });
$input.keydown(function (e) {
// handling up/down/escape requires results to be visible
// handling enter/tab requires that AND a result to be selected
if ($.inArray(e.keyCode, [27, 38, 40]) !== -1 && $results.is(":visible") ||
$.inArray(e.keyCode, [13, 9]) !== -1 && getCurrentResult()) {
if (e.preventDefault) { e.preventDefault(); }
if (e.stopPropagation) { e.stopPropagation(); }
e.cancelBubble = true;
e.returnValue = false;
switch(e.keyCode) {
case 38: // up
setCurrentResult("prev");
break;
case 40: // down
setCurrentResult("next");
break;
case 9: // tab
case 13: // return
selectCurrentResult();
break;
case 27: // escape
$results.hide();
break;
}
} else if ($input.val().length != prevLength) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(suggest, options.delay);
prevLength = $input.val().length;
}
});
/**
* This method drives the request for getting suggestions from the
* server. We expect to get back our results as a collection of JSON
* objects. These results, and the query we used to find these results,
* are cached for future use.
*/
function suggest () {
var q = $.trim($input.val());
if (q.length >= options.minchars) {
var cached = checkCache(q);
if (cached) {
displayItems(cached.items);
} else {
var jsonOpts = $.extend({ q: q }, options.jsonOpts);
$.getJSON(options.source, jsonOpts, function (json) {
$results.hide();
displayItems(json);
addToCache(q, json);
});
}
} else {
$results.hide();
}
}
/**
* Checks to see if a given query has been cached or not.
*
* @param {String} q the query that may or may not be cached
*
* @return {Object} cached query results
* @return {false} the query has not been cached
*/
function checkCache (q) {
for (var i = 0; i < cache.length; i++) {
// If we find that the query is cached, move the cached query to the
// front of the queue and return it.
if (cache[i].q === q) {
cache.unshift(cache.splice(i, 1)[0]);
return cache[0];
}
}
return false;
}
/**
* @param {String} q the query that resulted in the items being cached
* @param {Array} items these are the results of the query
*/
function addToCache (q, items) {
// peels cached results off of the end of our cache until we can store
// our new results in the cache.
while (cache.length && (cacheSize + items.length > options.maxCacheSize)) {
cacheSize -= cache.pop().items.length;
}
cache.push({ q: q, items: items });
cacheSize += items.length;
}
/**
* Renders LI tags for each returned result. Data associated with the
* result is bound to the rendered tag via jQuery.data.
*
* @param {Array} json an array of JSON objects
*/
function displayItems (json) {
if (!json) { return; }
if (!json.length) {
$results.hide();
return;
}
// Only draw and register events if there are no children available to show
if ($results.children("li").length === 0) {
for (var i = 0; i < json.length; i++) {
$results
.append(
$(document.createElement("li"))
.html(json[i][options.namespace][options.label])
.data("json", json[i][options.namespace]));
}
$results
.children("li")
.mouseover(function () {
$results.children("li").removeClass(options.selectClass);
$(this).addClass(options.selectClass);
})
.click(function (e) {
e.preventDefault();
e.stopPropagation();
selectCurrentResult();
});
}
$results.show();
}
/**
* Returns the currently selected LI tag from the visible list.
*
* @return {false} the result list is visible
* @return {jQuery} the selected LI tag
*/
function getCurrentResult () {
if (!$results.is(":visible")) { return false; }
var currentResult = $results.children("li." + options.selectClass);
// Checks to see we actually have results
if (!currentResult.length) { currentResult = false; }
return currentResult;
}
/**
* This is called when a user has selected some LI item in a list. If
* the user has defined an onSelect function, it is applied in the
* context of the selected LI item. This means the data bound to this
* item is available to this function. The onSelect() method is also
* given the INPUT element itself as a parameter.
*/
function selectCurrentResult () {
var currentResult = getCurrentResult();
if (currentResult) {
$input.val(currentResult.text());
$results.hide();
if (options.onSelect) {
options.onSelect.apply(currentResult, [$input[0]]);
}
}
}
/**
* Selects either the next or previous LI tag in the results list
* depending on the provided direction.
*
* @param {String} direction either 'next' or 'prev'
*/
function setCurrentResult (direction) {
if (direction !== "next" && direction !== "prev") {
return;
}
var nextOrPrev, firstOrLast;
if (direction === "next") {
nextOrPrev = "next";
firstOrLast = "first";
} else {
nextOrPrev = "prev";
firstOrLast = "last";
}
var currentResult = getCurrentResult();
if (currentResult) {
currentResult
.removeClass(options.selectClass)[nextOrPrev]()
.addClass(options.selectClass);
} else {
$results
.children("li:" + firstOrLast + "-child")
.addClass(options.selectClass);
}
}
}
});
})(jQuery);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment