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