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