app/jquery/jquery-autocomplete.js
changeset 1235 41cb87ed9b81
parent 1212 1b64b5922227
equal deleted inserted replaced
1234:a6ccb08911eb 1235:41cb87ed9b81
       
     1 /*
       
     2  * Autocomplete - jQuery plugin 1.0.2
       
     3  *
       
     4  * Copyright (c) 2007 Dylan Verheul, Dan G. Switzer, Anjesh Tuladhar, Jörn Zaefferer
       
     5  *
       
     6  * Dual licensed under the MIT and GPL licenses:
       
     7  *   http://www.opensource.org/licenses/mit-license.php
       
     8  *   http://www.gnu.org/licenses/gpl.html
       
     9  *
       
    10  * Revision: $Id: jquery.autocomplete.js 5747 2008-06-25 18:30:55Z joern.zaefferer $
       
    11  *
       
    12  */
       
    13 
       
    14 ;(function($) {
       
    15 
       
    16 $.fn.extend({
       
    17 	autocomplete: function(urlOrData, options) {
       
    18 		var isUrl = typeof urlOrData == "string";
       
    19 		options = $.extend({}, $.Autocompleter.defaults, {
       
    20 			url: isUrl ? urlOrData : null,
       
    21 			data: isUrl ? null : urlOrData,
       
    22 			delay: isUrl ? $.Autocompleter.defaults.delay : 10,
       
    23 			max: options && !options.scroll ? 10 : 150
       
    24 		}, options);
       
    25 
       
    26 		// if highlight is set to false, replace it with a do-nothing function
       
    27 		options.highlight = options.highlight || function(value) { return value; };
       
    28 
       
    29 		// if the formatMatch option is not specified, then use formatItem for backwards compatibility
       
    30 		options.formatMatch = options.formatMatch || options.formatItem;
       
    31 
       
    32 		return this.each(function() {
       
    33 			new $.Autocompleter(this, options);
       
    34 		});
       
    35 	},
       
    36 	result: function(handler) {
       
    37 		return this.bind("result", handler);
       
    38 	},
       
    39 	search: function(handler) {
       
    40 		return this.trigger("search", [handler]);
       
    41 	},
       
    42 	flushCache: function() {
       
    43 		return this.trigger("flushCache");
       
    44 	},
       
    45 	setOptions: function(options){
       
    46 		return this.trigger("setOptions", [options]);
       
    47 	},
       
    48 	unautocomplete: function() {
       
    49 		return this.trigger("unautocomplete");
       
    50 	}
       
    51 });
       
    52 
       
    53 $.Autocompleter = function(input, options) {
       
    54 
       
    55 	var KEY = {
       
    56 		UP: 38,
       
    57 		DOWN: 40,
       
    58 		DEL: 46,
       
    59 		TAB: 9,
       
    60 		RETURN: 13,
       
    61 		ESC: 27,
       
    62 		COMMA: 188,
       
    63 		PAGEUP: 33,
       
    64 		PAGEDOWN: 34,
       
    65 		BACKSPACE: 8
       
    66 	};
       
    67 
       
    68 	// Create $ object for input element
       
    69 	var $input = $(input).attr("autocomplete", "off").addClass(options.inputClass);
       
    70 
       
    71 	var timeout;
       
    72 	var previousValue = "";
       
    73 	var cache = $.Autocompleter.Cache(options);
       
    74 	var hasFocus = 0;
       
    75 	var lastKeyPressCode;
       
    76 	var config = {
       
    77 		mouseDownOnSelect: false
       
    78 	};
       
    79 	var select = $.Autocompleter.Select(options, input, selectCurrent, config);
       
    80 
       
    81 	var blockSubmit;
       
    82 
       
    83 	// prevent form submit in opera when selecting with return key
       
    84 	$.browser.opera && $(input.form).bind("submit.autocomplete", function() {
       
    85 		if (blockSubmit) {
       
    86 			blockSubmit = false;
       
    87 			return false;
       
    88 		}
       
    89 	});
       
    90 
       
    91 	// only opera doesn't trigger keydown multiple times while pressed, others don't work with keypress at all
       
    92 	$input.bind(($.browser.opera ? "keypress" : "keydown") + ".autocomplete", function(event) {
       
    93 		// track last key pressed
       
    94 		lastKeyPressCode = event.keyCode;
       
    95 		switch(event.keyCode) {
       
    96 
       
    97 			case KEY.UP:
       
    98 				event.preventDefault();
       
    99 				if ( select.visible() ) {
       
   100 					select.prev();
       
   101 				} else {
       
   102 					onChange(0, true);
       
   103 				}
       
   104 				break;
       
   105 
       
   106 			case KEY.DOWN:
       
   107 				event.preventDefault();
       
   108 				if ( select.visible() ) {
       
   109 					select.next();
       
   110 				} else {
       
   111 					onChange(0, true);
       
   112 				}
       
   113 				break;
       
   114 
       
   115 			case KEY.PAGEUP:
       
   116 				event.preventDefault();
       
   117 				if ( select.visible() ) {
       
   118 					select.pageUp();
       
   119 				} else {
       
   120 					onChange(0, true);
       
   121 				}
       
   122 				break;
       
   123 
       
   124 			case KEY.PAGEDOWN:
       
   125 				event.preventDefault();
       
   126 				if ( select.visible() ) {
       
   127 					select.pageDown();
       
   128 				} else {
       
   129 					onChange(0, true);
       
   130 				}
       
   131 				break;
       
   132 
       
   133 			// matches also semicolon
       
   134 			case options.multiple && $.trim(options.multipleSeparator) == "," && KEY.COMMA:
       
   135 			case KEY.TAB:
       
   136 			case KEY.RETURN:
       
   137 				if( selectCurrent() ) {
       
   138 					// stop default to prevent a form submit, Opera needs special handling
       
   139 					event.preventDefault();
       
   140 					blockSubmit = true;
       
   141 					return false;
       
   142 				}
       
   143 				break;
       
   144 
       
   145 			case KEY.ESC:
       
   146 				select.hide();
       
   147 				break;
       
   148 
       
   149 			default:
       
   150 				clearTimeout(timeout);
       
   151 				timeout = setTimeout(onChange, options.delay);
       
   152 				break;
       
   153 		}
       
   154 	}).focus(function(){
       
   155 		// track whether the field has focus, we shouldn't process any
       
   156 		// results if the field no longer has focus
       
   157 		hasFocus++;
       
   158 	}).blur(function() {
       
   159 		hasFocus = 0;
       
   160 		if (!config.mouseDownOnSelect) {
       
   161 			hideResults();
       
   162 		}
       
   163 	}).click(function() {
       
   164 		// show select when clicking in a focused field
       
   165 		if ( hasFocus++ > 1 && !select.visible() ) {
       
   166 			onChange(0, true);
       
   167 		}
       
   168 	}).bind("search", function() {
       
   169 		// TODO why not just specifying both arguments?
       
   170 		var fn = (arguments.length > 1) ? arguments[1] : null;
       
   171 		function findValueCallback(q, data) {
       
   172 			var result;
       
   173 			if( data && data.length ) {
       
   174 				for (var i=0; i < data.length; i++) {
       
   175 					if( data[i].result.toLowerCase() == q.toLowerCase() ) {
       
   176 						result = data[i];
       
   177 						break;
       
   178 					}
       
   179 				}
       
   180 			}
       
   181 			if( typeof fn == "function" ) fn(result);
       
   182 			else $input.trigger("result", result && [result.data, result.value]);
       
   183 		}
       
   184 		$.each(trimWords($input.val()), function(i, value) {
       
   185 			request(value, findValueCallback, findValueCallback);
       
   186 		});
       
   187 	}).bind("flushCache", function() {
       
   188 		cache.flush();
       
   189 	}).bind("setOptions", function() {
       
   190 		$.extend(options, arguments[1]);
       
   191 		// if we've updated the data, repopulate
       
   192 		if ( "data" in arguments[1] )
       
   193 			cache.populate();
       
   194 	}).bind("unautocomplete", function() {
       
   195 		select.unbind();
       
   196 		$input.unbind();
       
   197 		$(input.form).unbind(".autocomplete");
       
   198 	});
       
   199 
       
   200 
       
   201 	function selectCurrent() {
       
   202 		var selected = select.selected();
       
   203 		if( !selected )
       
   204 			return false;
       
   205 
       
   206 		var v = selected.result;
       
   207 		previousValue = v;
       
   208 
       
   209 		if ( options.multiple ) {
       
   210 			var words = trimWords($input.val());
       
   211 			if ( words.length > 1 ) {
       
   212 				v = words.slice(0, words.length - 1).join( options.multipleSeparator ) + options.multipleSeparator + v;
       
   213 			}
       
   214 			v += options.multipleSeparator;
       
   215 		}
       
   216 
       
   217 		$input.val(v);
       
   218 		hideResultsNow();
       
   219 		$input.trigger("result", [selected.data, selected.value]);
       
   220 		return true;
       
   221 	}
       
   222 
       
   223 	function onChange(crap, skipPrevCheck) {
       
   224 		if( lastKeyPressCode == KEY.DEL ) {
       
   225 			select.hide();
       
   226 			return;
       
   227 		}
       
   228 
       
   229 		var currentValue = $input.val();
       
   230 
       
   231 		if ( !skipPrevCheck && currentValue == previousValue )
       
   232 			return;
       
   233 
       
   234 		previousValue = currentValue;
       
   235 
       
   236 		currentValue = lastWord(currentValue);
       
   237 		if ( currentValue.length >= options.minChars) {
       
   238 			$input.addClass(options.loadingClass);
       
   239 			if (!options.matchCase)
       
   240 				currentValue = currentValue.toLowerCase();
       
   241 			request(currentValue, receiveData, hideResultsNow);
       
   242 		} else {
       
   243 			stopLoading();
       
   244 			select.hide();
       
   245 		}
       
   246 	};
       
   247 
       
   248 	function trimWords(value) {
       
   249 		if ( !value ) {
       
   250 			return [""];
       
   251 		}
       
   252 		var words = value.split( options.multipleSeparator );
       
   253 		var result = [];
       
   254 		$.each(words, function(i, value) {
       
   255 			if ( $.trim(value) )
       
   256 				result[i] = $.trim(value);
       
   257 		});
       
   258 		return result;
       
   259 	}
       
   260 
       
   261 	function lastWord(value) {
       
   262 		if ( !options.multiple )
       
   263 			return value;
       
   264 		var words = trimWords(value);
       
   265 		return words[words.length - 1];
       
   266 	}
       
   267 
       
   268 	// fills in the input box w/the first match (assumed to be the best match)
       
   269 	// q: the term entered
       
   270 	// sValue: the first matching result
       
   271 	function autoFill(q, sValue){
       
   272 		// autofill in the complete box w/the first match as long as the user hasn't entered in more data
       
   273 		// if the last user key pressed was backspace, don't autofill
       
   274 		if( options.autoFill && (lastWord($input.val()).toLowerCase() == q.toLowerCase()) && lastKeyPressCode != KEY.BACKSPACE ) {
       
   275 			// fill in the value (keep the case the user has typed)
       
   276 			$input.val($input.val() + sValue.substring(lastWord(previousValue).length));
       
   277 			// select the portion of the value not typed by the user (so the next character will erase)
       
   278 			$.Autocompleter.Selection(input, previousValue.length, previousValue.length + sValue.length);
       
   279 		}
       
   280 	};
       
   281 
       
   282 	function hideResults() {
       
   283 		clearTimeout(timeout);
       
   284 		timeout = setTimeout(hideResultsNow, 200);
       
   285 	};
       
   286 
       
   287 	function hideResultsNow() {
       
   288 		var wasVisible = select.visible();
       
   289 		select.hide();
       
   290 		clearTimeout(timeout);
       
   291 		stopLoading();
       
   292 		if (options.mustMatch) {
       
   293 			// call search and run callback
       
   294 			$input.search(
       
   295 				function (result){
       
   296 					// if no value found, clear the input box
       
   297 					if( !result ) {
       
   298 						if (options.multiple) {
       
   299 							var words = trimWords($input.val()).slice(0, -1);
       
   300 							$input.val( words.join(options.multipleSeparator) + (words.length ? options.multipleSeparator : "") );
       
   301 						}
       
   302 						else
       
   303 							$input.val( "" );
       
   304 					}
       
   305 				}
       
   306 			);
       
   307 		}
       
   308 		if (wasVisible)
       
   309 			// position cursor at end of input field
       
   310 			$.Autocompleter.Selection(input, input.value.length, input.value.length);
       
   311 	};
       
   312 
       
   313 	function receiveData(q, data) {
       
   314 		if ( data && data.length && hasFocus ) {
       
   315 			stopLoading();
       
   316 			select.display(data, q);
       
   317 			autoFill(q, data[0].value);
       
   318 			select.show();
       
   319 		} else {
       
   320 			hideResultsNow();
       
   321 		}
       
   322 	};
       
   323 
       
   324 	function request(term, success, failure) {
       
   325 		if (!options.matchCase)
       
   326 			term = term.toLowerCase();
       
   327 		var data = cache.load(term);
       
   328 		// recieve the cached data
       
   329 		if (data && data.length) {
       
   330 			success(term, data);
       
   331 		// if an AJAX url has been supplied, try loading the data now
       
   332 		} else if( (typeof options.url == "string") && (options.url.length > 0) ){
       
   333 
       
   334 			var extraParams = {
       
   335 				timestamp: +new Date()
       
   336 			};
       
   337 			$.each(options.extraParams, function(key, param) {
       
   338 				extraParams[key] = typeof param == "function" ? param() : param;
       
   339 			});
       
   340 
       
   341 			$.ajax({
       
   342 				// try to leverage ajaxQueue plugin to abort previous requests
       
   343 				mode: "abort",
       
   344 				// limit abortion to this input
       
   345 				port: "autocomplete" + input.name,
       
   346 				dataType: options.dataType,
       
   347 				url: options.url,
       
   348 				data: $.extend({
       
   349 					q: lastWord(term),
       
   350 					limit: options.max
       
   351 				}, extraParams),
       
   352 				success: function(data) {
       
   353 					var parsed = options.parse && options.parse(data) || parse(data);
       
   354 					cache.add(term, parsed);
       
   355 					success(term, parsed);
       
   356 				}
       
   357 			});
       
   358 		} else {
       
   359 			// if we have a failure, we need to empty the list -- this prevents the the [TAB] key from selecting the last successful match
       
   360 			select.emptyList();
       
   361 			failure(term);
       
   362 		}
       
   363 	};
       
   364 
       
   365 	function parse(data) {
       
   366 		var parsed = [];
       
   367 		var rows = data.split("\n");
       
   368 		for (var i=0; i < rows.length; i++) {
       
   369 			var row = $.trim(rows[i]);
       
   370 			if (row) {
       
   371 				row = row.split("|");
       
   372 				parsed[parsed.length] = {
       
   373 					data: row,
       
   374 					value: row[0],
       
   375 					result: options.formatResult && options.formatResult(row, row[0]) || row[0]
       
   376 				};
       
   377 			}
       
   378 		}
       
   379 		return parsed;
       
   380 	};
       
   381 
       
   382 	function stopLoading() {
       
   383 		$input.removeClass(options.loadingClass);
       
   384 	};
       
   385 
       
   386 };
       
   387 
       
   388 $.Autocompleter.defaults = {
       
   389 	inputClass: "ac_input",
       
   390 	resultsClass: "ac_results",
       
   391 	loadingClass: "ac_loading",
       
   392 	minChars: 1,
       
   393 	delay: 400,
       
   394 	matchCase: false,
       
   395 	matchSubset: true,
       
   396 	matchContains: false,
       
   397 	cacheLength: 10,
       
   398 	max: 100,
       
   399 	mustMatch: false,
       
   400 	extraParams: {},
       
   401 	selectFirst: true,
       
   402 	formatItem: function(row) { return row[0]; },
       
   403 	formatMatch: null,
       
   404 	autoFill: false,
       
   405 	width: 0,
       
   406 	multiple: false,
       
   407 	multipleSeparator: ", ",
       
   408 	highlight: function(value, term) {
       
   409 		return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
       
   410 	},
       
   411     scroll: true,
       
   412     scrollHeight: 180
       
   413 };
       
   414 
       
   415 $.Autocompleter.Cache = function(options) {
       
   416 
       
   417 	var data = {};
       
   418 	var length = 0;
       
   419 
       
   420 	function matchSubset(s, sub) {
       
   421 		if (!options.matchCase)
       
   422 			s = s.toLowerCase();
       
   423 		var i = s.indexOf(sub);
       
   424 		if (i == -1) return false;
       
   425 		return i == 0 || options.matchContains;
       
   426 	};
       
   427 
       
   428 	function add(q, value) {
       
   429 		if (length > options.cacheLength){
       
   430 			flush();
       
   431 		}
       
   432 		if (!data[q]){
       
   433 			length++;
       
   434 		}
       
   435 		data[q] = value;
       
   436 	}
       
   437 
       
   438 	function populate(){
       
   439 		if( !options.data ) return false;
       
   440 		// track the matches
       
   441 		var stMatchSets = {},
       
   442 			nullData = 0;
       
   443 
       
   444 		// no url was specified, we need to adjust the cache length to make sure it fits the local data store
       
   445 		if( !options.url ) options.cacheLength = 1;
       
   446 
       
   447 		// track all options for minChars = 0
       
   448 		stMatchSets[""] = [];
       
   449 
       
   450 		// loop through the array and create a lookup structure
       
   451 		for ( var i = 0, ol = options.data.length; i < ol; i++ ) {
       
   452 			var rawValue = options.data[i];
       
   453 			// if rawValue is a string, make an array otherwise just reference the array
       
   454 			rawValue = (typeof rawValue == "string") ? [rawValue] : rawValue;
       
   455 
       
   456 			var value = options.formatMatch(rawValue, i+1, options.data.length);
       
   457 			if ( value === false )
       
   458 				continue;
       
   459 
       
   460 			var firstChar = value.charAt(0).toLowerCase();
       
   461 			// if no lookup array for this character exists, look it up now
       
   462 			if( !stMatchSets[firstChar] )
       
   463 				stMatchSets[firstChar] = [];
       
   464 
       
   465 			// if the match is a string
       
   466 			var row = {
       
   467 				value: value,
       
   468 				data: rawValue,
       
   469 				result: options.formatResult && options.formatResult(rawValue) || value
       
   470 			};
       
   471 
       
   472 			// push the current match into the set list
       
   473 			stMatchSets[firstChar].push(row);
       
   474 
       
   475 			// keep track of minChars zero items
       
   476 			if ( nullData++ < options.max ) {
       
   477 				stMatchSets[""].push(row);
       
   478 			}
       
   479 		};
       
   480 
       
   481 		// add the data items to the cache
       
   482 		$.each(stMatchSets, function(i, value) {
       
   483 			// increase the cache size
       
   484 			options.cacheLength++;
       
   485 			// add to the cache
       
   486 			add(i, value);
       
   487 		});
       
   488 	}
       
   489 
       
   490 	// populate any existing data
       
   491 	setTimeout(populate, 25);
       
   492 
       
   493 	function flush(){
       
   494 		data = {};
       
   495 		length = 0;
       
   496 	}
       
   497 
       
   498 	return {
       
   499 		flush: flush,
       
   500 		add: add,
       
   501 		populate: populate,
       
   502 		load: function(q) {
       
   503 			if (!options.cacheLength || !length)
       
   504 				return null;
       
   505 			/*
       
   506 			 * if dealing w/local data and matchContains than we must make sure
       
   507 			 * to loop through all the data collections looking for matches
       
   508 			 */
       
   509 			if( !options.url && options.matchContains ){
       
   510 				// track all matches
       
   511 				var csub = [];
       
   512 				// loop through all the data grids for matches
       
   513 				for( var k in data ){
       
   514 					// don't search through the stMatchSets[""] (minChars: 0) cache
       
   515 					// this prevents duplicates
       
   516 					if( k.length > 0 ){
       
   517 						var c = data[k];
       
   518 						$.each(c, function(i, x) {
       
   519 							// if we've got a match, add it to the array
       
   520 							if (matchSubset(x.value, q)) {
       
   521 								csub.push(x);
       
   522 							}
       
   523 						});
       
   524 					}
       
   525 				}
       
   526 				return csub;
       
   527 			} else
       
   528 			// if the exact item exists, use it
       
   529 			if (data[q]){
       
   530 				return data[q];
       
   531 			} else
       
   532 			if (options.matchSubset) {
       
   533 				for (var i = q.length - 1; i >= options.minChars; i--) {
       
   534 					var c = data[q.substr(0, i)];
       
   535 					if (c) {
       
   536 						var csub = [];
       
   537 						$.each(c, function(i, x) {
       
   538 							if (matchSubset(x.value, q)) {
       
   539 								csub[csub.length] = x;
       
   540 							}
       
   541 						});
       
   542 						return csub;
       
   543 					}
       
   544 				}
       
   545 			}
       
   546 			return null;
       
   547 		}
       
   548 	};
       
   549 };
       
   550 
       
   551 $.Autocompleter.Select = function (options, input, select, config) {
       
   552 	var CLASSES = {
       
   553 		ACTIVE: "ac_over"
       
   554 	};
       
   555 
       
   556 	var listItems,
       
   557 		active = -1,
       
   558 		data,
       
   559 		term = "",
       
   560 		needsInit = true,
       
   561 		element,
       
   562 		list;
       
   563 
       
   564 	// Create results
       
   565 	function init() {
       
   566 		if (!needsInit)
       
   567 			return;
       
   568 		element = $("<div/>")
       
   569 		.hide()
       
   570 		.addClass(options.resultsClass)
       
   571 		.css("position", "absolute")
       
   572 		.appendTo(document.body);
       
   573 
       
   574 		list = $("<ul/>").appendTo(element).mouseover( function(event) {
       
   575 			if(target(event).nodeName && target(event).nodeName.toUpperCase() == 'LI') {
       
   576 	            active = $("li", list).removeClass(CLASSES.ACTIVE).index(target(event));
       
   577 			    $(target(event)).addClass(CLASSES.ACTIVE);
       
   578 	        }
       
   579 		}).click(function(event) {
       
   580 			$(target(event)).addClass(CLASSES.ACTIVE);
       
   581 			select();
       
   582 			// TODO provide option to avoid setting focus again after selection? useful for cleanup-on-focus
       
   583 			input.focus();
       
   584 			return false;
       
   585 		}).mousedown(function() {
       
   586 			config.mouseDownOnSelect = true;
       
   587 		}).mouseup(function() {
       
   588 			config.mouseDownOnSelect = false;
       
   589 		});
       
   590 
       
   591 		if( options.width > 0 )
       
   592 			element.css("width", options.width);
       
   593 
       
   594 		needsInit = false;
       
   595 	}
       
   596 
       
   597 	function target(event) {
       
   598 		var element = event.target;
       
   599 		while(element && element.tagName != "LI")
       
   600 			element = element.parentNode;
       
   601 		// more fun with IE, sometimes event.target is empty, just ignore it then
       
   602 		if(!element)
       
   603 			return [];
       
   604 		return element;
       
   605 	}
       
   606 
       
   607 	function moveSelect(step) {
       
   608 		listItems.slice(active, active + 1).removeClass(CLASSES.ACTIVE);
       
   609 		movePosition(step);
       
   610         var activeItem = listItems.slice(active, active + 1).addClass(CLASSES.ACTIVE);
       
   611         if(options.scroll) {
       
   612             var offset = 0;
       
   613             listItems.slice(0, active).each(function() {
       
   614 				offset += this.offsetHeight;
       
   615 			});
       
   616             if((offset + activeItem[0].offsetHeight - list.scrollTop()) > list[0].clientHeight) {
       
   617                 list.scrollTop(offset + activeItem[0].offsetHeight - list.innerHeight());
       
   618             } else if(offset < list.scrollTop()) {
       
   619                 list.scrollTop(offset);
       
   620             }
       
   621         }
       
   622 	};
       
   623 
       
   624 	function movePosition(step) {
       
   625 		active += step;
       
   626 		if (active < 0) {
       
   627 			active = listItems.size() - 1;
       
   628 		} else if (active >= listItems.size()) {
       
   629 			active = 0;
       
   630 		}
       
   631 	}
       
   632 
       
   633 	function limitNumberOfItems(available) {
       
   634 		return options.max && options.max < available
       
   635 			? options.max
       
   636 			: available;
       
   637 	}
       
   638 
       
   639 	function fillList() {
       
   640 		list.empty();
       
   641 		var max = limitNumberOfItems(data.length);
       
   642 		for (var i=0; i < max; i++) {
       
   643 			if (!data[i])
       
   644 				continue;
       
   645 			var formatted = options.formatItem(data[i].data, i+1, max, data[i].value, term);
       
   646 			if ( formatted === false )
       
   647 				continue;
       
   648 			var li = $("<li/>").html( options.highlight(formatted, term) ).addClass(i%2 == 0 ? "ac_even" : "ac_odd").appendTo(list)[0];
       
   649 			$.data(li, "ac_data", data[i]);
       
   650 		}
       
   651 		listItems = list.find("li");
       
   652 		if ( options.selectFirst ) {
       
   653 			listItems.slice(0, 1).addClass(CLASSES.ACTIVE);
       
   654 			active = 0;
       
   655 		}
       
   656 		// apply bgiframe if available
       
   657 		if ( $.fn.bgiframe )
       
   658 			list.bgiframe();
       
   659 	}
       
   660 
       
   661 	return {
       
   662 		display: function(d, q) {
       
   663 			init();
       
   664 			data = d;
       
   665 			term = q;
       
   666 			fillList();
       
   667 		},
       
   668 		next: function() {
       
   669 			moveSelect(1);
       
   670 		},
       
   671 		prev: function() {
       
   672 			moveSelect(-1);
       
   673 		},
       
   674 		pageUp: function() {
       
   675 			if (active != 0 && active - 8 < 0) {
       
   676 				moveSelect( -active );
       
   677 			} else {
       
   678 				moveSelect(-8);
       
   679 			}
       
   680 		},
       
   681 		pageDown: function() {
       
   682 			if (active != listItems.size() - 1 && active + 8 > listItems.size()) {
       
   683 				moveSelect( listItems.size() - 1 - active );
       
   684 			} else {
       
   685 				moveSelect(8);
       
   686 			}
       
   687 		},
       
   688 		hide: function() {
       
   689 			element && element.hide();
       
   690 			listItems && listItems.removeClass(CLASSES.ACTIVE);
       
   691 			active = -1;
       
   692 		},
       
   693 		visible : function() {
       
   694 			return element && element.is(":visible");
       
   695 		},
       
   696 		current: function() {
       
   697 			return this.visible() && (listItems.filter("." + CLASSES.ACTIVE)[0] || options.selectFirst && listItems[0]);
       
   698 		},
       
   699 		show: function() {
       
   700 			var offset = $(input).offset();
       
   701 			element.css({
       
   702 				width: typeof options.width == "string" || options.width > 0 ? options.width : $(input).width(),
       
   703 				top: offset.top + input.offsetHeight,
       
   704 				left: offset.left
       
   705 			}).show();
       
   706             if(options.scroll) {
       
   707                 list.scrollTop(0);
       
   708                 list.css({
       
   709 					maxHeight: options.scrollHeight,
       
   710 					overflow: 'auto'
       
   711 				});
       
   712 
       
   713                 if($.browser.msie && typeof document.body.style.maxHeight === "undefined") {
       
   714 					var listHeight = 0;
       
   715 					listItems.each(function() {
       
   716 						listHeight += this.offsetHeight;
       
   717 					});
       
   718 					var scrollbarsVisible = listHeight > options.scrollHeight;
       
   719                     list.css('height', scrollbarsVisible ? options.scrollHeight : listHeight );
       
   720 					if (!scrollbarsVisible) {
       
   721 						// IE doesn't recalculate width when scrollbar disappears
       
   722 						listItems.width( list.width() - parseInt(listItems.css("padding-left")) - parseInt(listItems.css("padding-right")) );
       
   723 					}
       
   724                 }
       
   725 
       
   726             }
       
   727 		},
       
   728 		selected: function() {
       
   729 			var selected = listItems && listItems.filter("." + CLASSES.ACTIVE).removeClass(CLASSES.ACTIVE);
       
   730 			return selected && selected.length && $.data(selected[0], "ac_data");
       
   731 		},
       
   732 		emptyList: function (){
       
   733 			list && list.empty();
       
   734 		},
       
   735 		unbind: function() {
       
   736 			element && element.remove();
       
   737 		}
       
   738 	};
       
   739 };
       
   740 
       
   741 $.Autocompleter.Selection = function(field, start, end) {
       
   742 	if( field.createTextRange ){
       
   743 		var selRange = field.createTextRange();
       
   744 		selRange.collapse(true);
       
   745 		selRange.moveStart("character", start);
       
   746 		selRange.moveEnd("character", end);
       
   747 		selRange.select();
       
   748 	} else if( field.setSelectionRange ){
       
   749 		field.setSelectionRange(start, end);
       
   750 	} else {
       
   751 		if( field.selectionStart ){
       
   752 			field.selectionStart = start;
       
   753 			field.selectionEnd = end;
       
   754 		}
       
   755 	}
       
   756 	field.focus();
       
   757 };
       
   758 
       
   759 })(jQuery);