Created
June 15, 2009 15:38
-
-
Save jherdman/130174 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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