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