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