bgneal@45: /* bgneal@45: * Autocomplete - jQuery plugin 1.0.2 bgneal@45: * bgneal@45: * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer bgneal@45: * bgneal@45: * Dual licensed under the MIT and GPL licenses: bgneal@45: * http://www.opensource.org/licenses/mit-license.php bgneal@45: * http://www.gnu.org/licenses/gpl.html bgneal@45: * bgneal@45: * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $ bgneal@45: * bgneal@45: */ bgneal@45: bgneal@45: ;(function($) { bgneal@45: bgneal@45: $.fn.extend({ bgneal@45: autocomplete: function(urlOrData, options) { bgneal@45: var isUrl = typeof urlOrData == "string"; bgneal@45: options = $.extend({}, $.Autocompleter.defaults, { bgneal@45: url: isUrl ? urlOrData : null, bgneal@45: data: isUrl ? null : urlOrData, bgneal@45: delay: isUrl ? $.Autocompleter.defaults.delay : 10, bgneal@45: max: options && !options.scroll ? 10 : 150 bgneal@45: }, options); bgneal@45: bgneal@45: // if highlight is set to false, replace it with a do-nothing function bgneal@45: options.highlight = options.highlight || function(value) { return value; }; bgneal@45: bgneal@45: // if the formatMatch option is not specified, then use formatItem for backwards compatibility bgneal@45: options.formatMatch = options.formatMatch || options.formatItem; bgneal@45: bgneal@45: return this.each(function() { bgneal@45: new $.Autocompleter(this, options); bgneal@45: }); bgneal@45: }, bgneal@45: result: function(handler) { bgneal@45: return this.bind("result", handler); bgneal@45: }, bgneal@45: search: function(handler) { bgneal@45: return this.trigger("search", [handler]); bgneal@45: }, bgneal@45: flushCache: function() { bgneal@45: return this.trigger("flushCache"); bgneal@45: }, bgneal@45: setOptions: function(options){ bgneal@45: return this.trigger("setOptions", [options]); bgneal@45: }, bgneal@45: unautocomplete: function() { bgneal@45: return this.trigger("unautocomplete"); bgneal@45: } bgneal@45: }); bgneal@45: bgneal@45: $.Autocompleter = function(input, options) { bgneal@45: bgneal@45: var KEY = { bgneal@45: UP: 38, bgneal@45: DOWN: 40, bgneal@45: DEL: 46, bgneal@45: TAB: 9, bgneal@45: RETURN: 13, bgneal@45: ESC: 27, bgneal@45: COMMA: 188, bgneal@45: PAGEUP: 33, bgneal@45: PAGEDOWN: 34, bgneal@45: BACKSPACE: 8 bgneal@45: }; bgneal@45: bgneal@45: // Create $ object for input element bgneal@45: var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass); bgneal@45: bgneal@45: var timeout; bgneal@45: var previousValue = ""; bgneal@45: var cache = $.Autocompleter.Cache(options); bgneal@45: var hasFocus = 0; bgneal@45: var lastKeyPressCode; bgneal@45: var config = { bgneal@45: mouseDownOnSelect: false bgneal@45: }; bgneal@45: var select = $.Autocompleter.Select(options, input, selectCurrent, config); bgneal@45: bgneal@45: var blockSubmit; bgneal@45: bgneal@45: // prevent form submit in opera when selecting with return key bgneal@45: $.browser.opera && $(input.form).bind("submit.autocomplete", function() { bgneal@45: if (blockSubmit) { bgneal@45: blockSubmit = false; bgneal@45: return false; bgneal@45: } bgneal@45: }); bgneal@45: bgneal@45: // only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all bgneal@45: $input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) { bgneal@45: // track last key pressed bgneal@45: lastKeyPressCode = event.keyCode; bgneal@45: switch(event.keyCode) { bgneal@45: bgneal@45: case KEY.UP: bgneal@45: event.preventDefault(); bgneal@45: if ( select.visible() ) { bgneal@45: select.prev(); bgneal@45: } else { bgneal@45: onChange(0, true); bgneal@45: } bgneal@45: break; bgneal@45: bgneal@45: case KEY.DOWN: bgneal@45: event.preventDefault(); bgneal@45: if ( select.visible() ) { bgneal@45: select.next(); bgneal@45: } else { bgneal@45: onChange(0, true); bgneal@45: } bgneal@45: break; bgneal@45: bgneal@45: case KEY.PAGEUP: bgneal@45: event.preventDefault(); bgneal@45: if ( select.visible() ) { bgneal@45: select.pageUp(); bgneal@45: } else { bgneal@45: onChange(0, true); bgneal@45: } bgneal@45: break; bgneal@45: bgneal@45: case KEY.PAGEDOWN: bgneal@45: event.preventDefault(); bgneal@45: if ( select.visible() ) { bgneal@45: select.pageDown(); bgneal@45: } else { bgneal@45: onChange(0, true); bgneal@45: } bgneal@45: break; bgneal@45: bgneal@45: // matches also semicolon bgneal@45: case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA: bgneal@45: case KEY.TAB: bgneal@45: case KEY.RETURN: bgneal@45: if( selectCurrent() ) { bgneal@45: // stop default to prevent a form submit, Opera needs special handling bgneal@45: event.preventDefault(); bgneal@45: blockSubmit = true; bgneal@45: return false; bgneal@45: } bgneal@45: break; bgneal@45: bgneal@45: case KEY.ESC: bgneal@45: select.hide(); bgneal@45: break; bgneal@45: bgneal@45: default: bgneal@45: clearTimeout(timeout); bgneal@45: timeout = setTimeout(onChange, options.delay); bgneal@45: break; bgneal@45: } bgneal@45: }).focus(function(){ bgneal@45: // track whether the field has focus, we shouldn't process any bgneal@45: // results if the field no longer has focus bgneal@45: hasFocus++; bgneal@45: }).blur(function() { bgneal@45: hasFocus = 0; bgneal@45: if (!config.mouseDownOnSelect) { bgneal@45: hideResults(); bgneal@45: } bgneal@45: }).click(function() { bgneal@45: // show select when clicking in a focused field bgneal@45: if ( hasFocus++ > 1 && !select.visible() ) { bgneal@45: onChange(0, true); bgneal@45: } bgneal@45: }).bind("search", function() { bgneal@45: // TODO why not just specifying both arguments? bgneal@45: var fn = (arguments.length > 1) ? arguments[1] : null; bgneal@45: function findValueCallback(q, data) { bgneal@45: var result; bgneal@45: if( data && data.length ) { bgneal@45: for (var i=0; i < data.length; i++) { bgneal@45: if( data[i].result.toLowerCase() == q.toLowerCase() ) { bgneal@45: result = data[i]; bgneal@45: break; bgneal@45: } bgneal@45: } bgneal@45: } bgneal@45: if( typeof fn == "function" ) fn(result); bgneal@45: else $input.trigger("result", result && [result.data, result.value]); bgneal@45: } bgneal@45: $.each(trimWords($input.val()), function(i, value) { bgneal@45: request(value, findValueCallback, findValueCallback); bgneal@45: }); bgneal@45: }).bind("flushCache", function() { bgneal@45: cache.flush(); bgneal@45: }).bind("setOptions", function() { bgneal@45: $.extend(options, arguments[1]); bgneal@45: // if we've updated the data, repopulate bgneal@45: if ( "data" in arguments[1] ) bgneal@45: cache.populate(); bgneal@45: }).bind("unautocomplete", function() { bgneal@45: select.unbind(); bgneal@45: $input.unbind(); bgneal@45: $(input.form).unbind(".autocomplete"); bgneal@45: }); bgneal@45: bgneal@45: bgneal@45: function selectCurrent() { bgneal@45: var selected = select.selected(); bgneal@45: if( !selected ) bgneal@45: return false; bgneal@45: bgneal@45: var v = selected.result; bgneal@45: previousValue = v; bgneal@45: bgneal@45: if ( options.multiple ) { bgneal@45: var words = trimWords($input.val()); bgneal@45: if ( words.length > 1 ) { bgneal@45: v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v; bgneal@45: } bgneal@45: v += options.multipleSeparator; bgneal@45: } bgneal@45: bgneal@45: $input.val(v); bgneal@45: hideResultsNow(); bgneal@45: $input.trigger("result", [selected.data, selected.value]); bgneal@45: return true; bgneal@45: } bgneal@45: bgneal@45: function onChange(crap, skipPrevCheck) { bgneal@45: if( lastKeyPressCode == KEY.DEL ) { bgneal@45: select.hide(); bgneal@45: return; bgneal@45: } bgneal@45: bgneal@45: var currentValue = $input.val(); bgneal@45: bgneal@45: if ( !skipPrevCheck && currentValue == previousValue ) bgneal@45: return; bgneal@45: bgneal@45: previousValue = currentValue; bgneal@45: bgneal@45: currentValue = lastWord(currentValue); bgneal@45: if ( currentValue.length >= options.minChars) { bgneal@45: $input.addClass(options.loadingClass); bgneal@45: if (!options.matchCase) bgneal@45: currentValue = currentValue.toLowerCase(); bgneal@45: request(currentValue, receiveData, hideResultsNow); bgneal@45: } else { bgneal@45: stopLoading(); bgneal@45: select.hide(); bgneal@45: } bgneal@45: }; bgneal@45: bgneal@45: function trimWords(value) { bgneal@45: if ( !value ) { bgneal@45: return [""]; bgneal@45: } bgneal@45: var words = value.split( options.multipleSeparator ); bgneal@45: var result = []; bgneal@45: $.each(words, function(i, value) { bgneal@45: if ( $.trim(value) ) bgneal@45: result[i] = $.trim(value); bgneal@45: }); bgneal@45: return result; bgneal@45: } bgneal@45: bgneal@45: function lastWord(value) { bgneal@45: if ( !options.multiple ) bgneal@45: return value; bgneal@45: var words = trimWords(value); bgneal@45: return words[words.length - 1]; bgneal@45: } bgneal@45: bgneal@45: // fills in the input box w/the first match (assumed to be the best match) bgneal@45: // q: the term entered bgneal@45: // sValue: the first matching result bgneal@45: function autoFill(q, sValue){ bgneal@45: // autofill in the complete box w/the first match as long as the user hasn't entered in more data bgneal@45: // if the last user key pressed was backspace, don't autofill bgneal@45: if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) { bgneal@45: // fill in the value (keep the case the user has typed) bgneal@45: $input.val($input.val() + sValue.substring(lastWord(previousValue).length)); bgneal@45: // select the portion of the value not typed by the user (so the next character will erase) bgneal@45: $.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length); bgneal@45: } bgneal@45: }; bgneal@45: bgneal@45: function hideResults() { bgneal@45: clearTimeout(timeout); bgneal@45: timeout = setTimeout(hideResultsNow, 200); bgneal@45: }; bgneal@45: bgneal@45: function hideResultsNow() { bgneal@45: var wasVisible = select.visible(); bgneal@45: select.hide(); bgneal@45: clearTimeout(timeout); bgneal@45: stopLoading(); bgneal@45: if (options.mustMatch) { bgneal@45: // call search and run callback bgneal@45: $input.search( bgneal@45: function (result){ bgneal@45: // if no value found, clear the input box bgneal@45: if( !result ) { bgneal@45: if (options.multiple) { bgneal@45: var words = trimWords($input.val()).slice(0, -1); bgneal@45: $input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") ); bgneal@45: } bgneal@45: else bgneal@45: $input.val( "" ); bgneal@45: } bgneal@45: } bgneal@45: ); bgneal@45: } bgneal@45: if (wasVisible) bgneal@45: // position cursor at end of input field bgneal@45: $.Autocompleter.Selection(input, input.value.length, input.value.length); bgneal@45: }; bgneal@45: bgneal@45: function receiveData(q, data) { bgneal@45: if ( data && data.length && hasFocus ) { bgneal@45: stopLoading(); bgneal@45: select.display(data, q); bgneal@45: autoFill(q, data[0].value); bgneal@45: select.show(); bgneal@45: } else { bgneal@45: hideResultsNow(); bgneal@45: } bgneal@45: }; bgneal@45: bgneal@45: function request(term, success, failure) { bgneal@45: if (!options.matchCase) bgneal@45: term = term.toLowerCase(); bgneal@45: var data = cache.load(term); bgneal@45: // recieve the cached data bgneal@45: if (data && data.length) { bgneal@45: success(term, data); bgneal@45: // if an AJAX url has been supplied, try loading the data now bgneal@45: } else if( (typeof options.url == "string") && (options.url.length > 0) ){ bgneal@45: bgneal@45: var extraParams = { bgneal@45: timestamp: +new Date() bgneal@45: }; bgneal@45: $.each(options.extraParams, function(key, param) { bgneal@45: extraParams[key] = typeof param == "function" ? param() : param; bgneal@45: }); bgneal@45: bgneal@45: $.ajax({ bgneal@45: // try to leverage ajaxQueue plugin to abort previous requests bgneal@45: mode: "abort", bgneal@45: // limit abortion to this input bgneal@45: port: "autocomplete" + input.name, bgneal@45: dataType: options.dataType, bgneal@45: url: options.url, bgneal@45: data: $.extend({ bgneal@45: q: lastWord(term), bgneal@45: limit: options.max bgneal@45: }, extraParams), bgneal@45: success: function(data) { bgneal@45: var parsed = options.parse && options.parse(data) || parse(data); bgneal@45: cache.add(term, parsed); bgneal@45: success(term, parsed); bgneal@45: } bgneal@45: }); bgneal@45: } else { bgneal@45: // if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match bgneal@45: select.emptyList(); bgneal@45: failure(term); bgneal@45: } bgneal@45: }; bgneal@45: bgneal@45: function parse(data) { bgneal@45: var parsed = []; bgneal@45: var rows = data.split("\n"); bgneal@45: for (var i=0; i < rows.length; i++) { bgneal@45: var row = $.trim(rows[i]); bgneal@45: if (row) { bgneal@45: row = row.split("|"); bgneal@45: parsed[parsed.length] = { bgneal@45: data: row, bgneal@45: value: row[0], bgneal@45: result: options.formatResult && options.formatResult(row, row[0]) || row[0] bgneal@45: }; bgneal@45: } bgneal@45: } bgneal@45: return parsed; bgneal@45: }; bgneal@45: bgneal@45: function stopLoading() { bgneal@45: $input.removeClass(options.loadingClass); bgneal@45: }; bgneal@45: bgneal@45: }; bgneal@45: bgneal@45: $.Autocompleter.defaults = { bgneal@45: inputClass: "ac_input", bgneal@45: resultsClass: "ac_results", bgneal@45: loadingClass: "ac_loading", bgneal@45: minChars: 1, bgneal@45: delay: 400, bgneal@45: matchCase: false, bgneal@45: matchSubset: true, bgneal@45: matchContains: false, bgneal@45: cacheLength: 10, bgneal@45: max: 100, bgneal@45: mustMatch: false, bgneal@45: extraParams: {}, bgneal@45: selectFirst: true, bgneal@45: formatItem: function(row) { return row[0]; }, bgneal@45: formatMatch: null, bgneal@45: autoFill: false, bgneal@45: width: 0, bgneal@45: multiple: false, bgneal@45: multipleSeparator: ", ", bgneal@45: highlight: function(value, term) { bgneal@45: return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); bgneal@45: }, bgneal@45: scroll: true, bgneal@45: scrollHeight: 180 bgneal@45: }; bgneal@45: bgneal@45: $.Autocompleter.Cache = function(options) { bgneal@45: bgneal@45: var data = {}; bgneal@45: var length = 0; bgneal@45: bgneal@45: function matchSubset(s, sub) { bgneal@45: if (!options.matchCase) bgneal@45: s = s.toLowerCase(); bgneal@45: var i = s.indexOf(sub); bgneal@45: if (i == -1) return false; bgneal@45: return i == 0 || options.matchContains; bgneal@45: }; bgneal@45: bgneal@45: function add(q, value) { bgneal@45: if (length > options.cacheLength){ bgneal@45: flush(); bgneal@45: } bgneal@45: if (!data[q]){ bgneal@45: length++; bgneal@45: } bgneal@45: data[q] = value; bgneal@45: } bgneal@45: bgneal@45: function populate(){ bgneal@45: if( !options.data ) return false; bgneal@45: // track the matches bgneal@45: var stMatchSets = {}, bgneal@45: nullData = 0; bgneal@45: bgneal@45: // no url was specified, we need to adjust the cache length to make sure it fits the local data store bgneal@45: if( !options.url ) options.cacheLength = 1; bgneal@45: bgneal@45: // track all options for minChars = 0 bgneal@45: stMatchSets[""] = []; bgneal@45: bgneal@45: // loop through the array and create a lookup structure bgneal@45: for ( var i = 0, ol = options.data.length; i < ol; i++ ) { bgneal@45: var rawValue = options.data[i]; bgneal@45: // if rawValue is a string, make an array otherwise just reference the array bgneal@45: rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue; bgneal@45: bgneal@45: var value = options.formatMatch(rawValue, i+1, options.data.length); bgneal@45: if ( value === false ) bgneal@45: continue; bgneal@45: bgneal@45: var firstChar = value.charAt(0).toLowerCase(); bgneal@45: // if no lookup array for this character exists, look it up now bgneal@45: if( !stMatchSets[firstChar] ) bgneal@45: stMatchSets[firstChar] = []; bgneal@45: bgneal@45: // if the match is a string bgneal@45: var row = { bgneal@45: value: value, bgneal@45: data: rawValue, bgneal@45: result: options.formatResult && options.formatResult(rawValue) || value bgneal@45: }; bgneal@45: bgneal@45: // push the current match into the set list bgneal@45: stMatchSets[firstChar].push(row); bgneal@45: bgneal@45: // keep track of minChars zero items bgneal@45: if ( nullData++ < options.max ) { bgneal@45: stMatchSets[""].push(row); bgneal@45: } bgneal@45: }; bgneal@45: bgneal@45: // add the data items to the cache bgneal@45: $.each(stMatchSets, function(i, value) { bgneal@45: // increase the cache size bgneal@45: options.cacheLength++; bgneal@45: // add to the cache bgneal@45: add(i, value); bgneal@45: }); bgneal@45: } bgneal@45: bgneal@45: // populate any existing data bgneal@45: setTimeout(populate, 25); bgneal@45: bgneal@45: function flush(){ bgneal@45: data = {}; bgneal@45: length = 0; bgneal@45: } bgneal@45: bgneal@45: return { bgneal@45: flush: flush, bgneal@45: add: add, bgneal@45: populate: populate, bgneal@45: load: function(q) { bgneal@45: if (!options.cacheLength || !length) bgneal@45: return null; bgneal@45: /* bgneal@45: * if dealing w/local data and matchContains than we must make sure bgneal@45: * to loop through all the data collections looking for matches bgneal@45: */ bgneal@45: if( !options.url && options.matchContains ){ bgneal@45: // track all matches bgneal@45: var csub = []; bgneal@45: // loop through all the data grids for matches bgneal@45: for( var k in data ){ bgneal@45: // don't search through the stMatchSets[""] (minChars: 0) cache bgneal@45: // this prevents duplicates bgneal@45: if( k.length > 0 ){ bgneal@45: var c = data[k]; bgneal@45: $.each(c, function(i, x) { bgneal@45: // if we've got a match, add it to the array bgneal@45: if (matchSubset(x.value, q)) { bgneal@45: csub.push(x); bgneal@45: } bgneal@45: }); bgneal@45: } bgneal@45: } bgneal@45: return csub; bgneal@45: } else bgneal@45: // if the exact item exists, use it bgneal@45: if (data[q]){ bgneal@45: return data[q]; bgneal@45: } else bgneal@45: if (options.matchSubset) { bgneal@45: for (var i = q.length - 1; i >= options.minChars; i--) { bgneal@45: var c = data[q.substr(0, i)]; bgneal@45: if (c) { bgneal@45: var csub = []; bgneal@45: $.each(c, function(i, x) { bgneal@45: if (matchSubset(x.value, q)) { bgneal@45: csub[csub.length] = x; bgneal@45: } bgneal@45: }); bgneal@45: return csub; bgneal@45: } bgneal@45: } bgneal@45: } bgneal@45: return null; bgneal@45: } bgneal@45: }; bgneal@45: }; bgneal@45: bgneal@45: $.Autocompleter.Select = function (options, input, select, config) { bgneal@45: var CLASSES = { bgneal@45: ACTIVE: "ac_over" bgneal@45: }; bgneal@45: bgneal@45: var listItems, bgneal@45: active = -1, bgneal@45: data, bgneal@45: term = "", bgneal@45: needsInit = true, bgneal@45: element, bgneal@45: list; bgneal@45: bgneal@45: // Create results bgneal@45: function init() { bgneal@45: if (!needsInit) bgneal@45: return; bgneal@45: element = $("
") bgneal@45: .hide() bgneal@45: .addClass(options.resultsClass) bgneal@45: .css("position", "absolute") bgneal@45: .appendTo(document.body); bgneal@45: bgneal@45: list = $("