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