annotate media/js/jquery-autocomplete/jquery.autocomplete.js @ 141:861f7d5f1b23

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