1/** 2 * @fileOverview jquery-autocomplete, the jQuery Autocompleter 3 * @author <a href="mailto:[email protected]">Dylan Verheul</a> 4 * @version 2.4.4 5 * @requires jQuery 1.6+ 6 * @license MIT | GPL | Apache 2.0, see LICENSE.txt 7 * @see https://github.com/dyve/jquery-autocomplete 8 */ 9(function($) { 10 "use strict"; 11 12 /** 13 * jQuery autocomplete plugin 14 * @param {object|string} options 15 * @returns (object} jQuery object 16 */ 17 $.fn.autocomplete = function(options) { 18 var url; 19 if (arguments.length > 1) { 20 url = options; 21 options = arguments[1]; 22 options.url = url; 23 } else if (typeof options === 'string') { 24 url = options; 25 options = { url: url }; 26 } 27 var opts = $.extend({}, $.fn.autocomplete.defaults, options); 28 return this.each(function() { 29 var $this = $(this); 30 $this.data('autocompleter', new $.Autocompleter( 31 $this, 32 $.meta ? $.extend({}, opts, $this.data()) : opts 33 )); 34 }); 35 }; 36 37 /** 38 * Store default options 39 * @type {object} 40 */ 41 $.fn.autocomplete.defaults = { 42 inputClass: 'acInput', 43 loadingClass: 'acLoading', 44 resultsClass: 'acResults', 45 selectClass: 'acSelect', 46 queryParamName: 'q', 47 extraParams: {}, 48 remoteDataType: false, 49 lineSeparator: '\n', 50 cellSeparator: '|', 51 minChars: 2, 52 maxItemsToShow: 10, 53 delay: 200, 54 useCache: false, 55 maxCacheLength: 10, 56 matchSubset: true, 57 matchCase: false, 58 matchInside: true, 59 mustMatch: false, 60 selectFirst: false, 61 selectOnly: false, 62 showResult: null, 63 preventDefaultReturn: 1, 64 preventDefaultTab: 0, 65 autoFill: false, 66 filterResults: true, 67 filter: true, 68 sortResults: true, 69 sortFunction: null, 70 onItemSelect: null, 71 onNoMatch: null, 72 onFinish: null, 73 matchStringConverter: null, 74 beforeUseConverter: null, 75 autoWidth: 'min-width', 76 useDelimiter: false, 77 delimiterChar: ',', 78 delimiterKeyCode: 188, 79 processData: null, 80 onError: null, 81 enabled: true 82 }; 83 84 /** 85 * Sanitize result 86 * @param {Object} result 87 * @returns {Object} object with members value (String) and data (Object) 88 * @private 89 */ 90 var sanitizeResult = function(result) { 91 var value, data; 92 var type = typeof result; 93 if (type === 'string') { 94 value = result; 95 data = {}; 96 } else if ($.isArray(result)) { 97 value = result[0]; 98 data = result.slice(1); 99 } else if (type === 'object') { 100 value = result.value; 101 data = result.data; 102 } 103 value = String(value); 104 if (typeof data !== 'object') { 105 data = {}; 106 } 107 return { 108 value: value, 109 data: data 110 }; 111 }; 112 113 /** 114 * Sanitize integer 115 * @param {mixed} value 116 * @param {Object} options 117 * @returns {Number} integer 118 * @private 119 */ 120 var sanitizeInteger = function(value, stdValue, options) { 121 var num = parseInt(value, 10); 122 options = options || {}; 123 if (isNaN(num) || (options.min && num < options.min)) { 124 num = stdValue; 125 } 126 return num; 127 }; 128 129 /** 130 * Create partial url for a name/value pair 131 */ 132 var makeUrlParam = function(name, value) { 133 return [name, encodeURIComponent(value)].join('='); 134 }; 135 136 /** 137 * Build an url 138 * @param {string} url Base url 139 * @param {object} [params] Dictionary of parameters 140 */ 141 var makeUrl = function(url, params) { 142 var urlAppend = []; 143 $.each(params, function(index, value) { 144 urlAppend.push(makeUrlParam(index, value)); 145 }); 146 if (urlAppend.length) { 147 url += url.indexOf('?') === -1 ? '?' : '&'; 148 url += urlAppend.join('&'); 149 } 150 return url; 151 }; 152 153 /** 154 * Default sort filter 155 * @param {object} a 156 * @param {object} b 157 * @param {boolean} matchCase 158 * @returns {number} 159 */ 160 var sortValueAlpha = function(a, b, matchCase) { 161 a = String(a.value); 162 b = String(b.value); 163 if (!matchCase) { 164 a = a.toLowerCase(); 165 b = b.toLowerCase(); 166 } 167 if (a > b) { 168 return 1; 169 } 170 if (a < b) { 171 return -1; 172 } 173 return 0; 174 }; 175 176 /** 177 * Parse data received in text format 178 * @param {string} text Plain text input 179 * @param {string} lineSeparator String that separates lines 180 * @param {string} cellSeparator String that separates cells 181 * @returns {array} Array of autocomplete data objects 182 */ 183 var plainTextParser = function(text, lineSeparator, cellSeparator) { 184 var results = []; 185 var i, j, data, line, value, lines; 186 // Be nice, fix linebreaks before splitting on lineSeparator 187 lines = String(text).replace('\r\n', '\n').split(lineSeparator); 188 for (i = 0; i < lines.length; i++) { 189 line = lines[i].split(cellSeparator); 190 data = []; 191 for (j = 0; j < line.length; j++) { 192 data.push(decodeURIComponent(encodeURIComponent(line[j]))); 193 } 194 value = data.shift(); 195 results.push({ value: value, data: data }); 196 } 197 return results; 198 }; 199 200 /** 201 * Autocompleter class 202 * @param {object} $elem jQuery object with one input tag 203 * @param {object} options Settings 204 * @constructor 205 */ 206 $.Autocompleter = function($elem, options) { 207 208 /** 209 * Assert parameters 210 */ 211 if (!$elem || !($elem instanceof $) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') { 212 throw new Error('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected.'); 213 } 214 215 /** 216 * @constant Link to this instance 217 * @type object 218 * @private 219 */ 220 var self = this; 221 222 /** 223 * @property {object} Options for this instance 224 * @public 225 */ 226 this.options = options; 227 228 /** 229 * @property object Cached data for this instance 230 * @private 231 */ 232 this.cacheData_ = {}; 233 234 /** 235 * @property {number} Number of cached data items 236 * @private 237 */ 238 this.cacheLength_ = 0; 239 240 /** 241 * @property {string}
241 Class name to mark selected item 242 * @private 243 */ 244 this.selectClass_ = 'jquery-autocomplete-selected-item'; 245 246 /** 247 * @property {number} Handler to activation timeout 248 * @private 249 */ 250 this.keyTimeout_ = null; 251 252 /** 253 * @property {number} Handler to finish timeout 254 * @private 255 */ 256 this.finishTimeout_ = null; 257 258 /** 259 * @property {number} Last key pressed in the input field (store for behavior) 260 * @private 261 */ 262 this.lastKeyPressed_ = null; 263 264 /** 265 * @property {string} Last value processed by the autocompleter 266 * @private 267 */ 268 this.lastProcessedValue_ = null; 269 270 /** 271 * @property {string} Last value selected by the user 272 * @private 273 */ 274 this.lastSelectedValue_ = null; 275 276 /** 277 * @property {boolean} Is this autocompleter active (showing results)? 278 * @see showResults 279 * @private 280 */ 281 this.active_ = false; 282 283 /** 284 * @property {boolean} Is this autocompleter allowed to finish on blur? 285 * @private 286 */ 287 this.finishOnBlur_ = true; 288 289 /** 290 * Sanitize options 291 */ 292 this.options.minChars = sanitizeInteger(this.options.minChars, $.fn.autocomplete.defaults.minChars, { min: 0 }); 293 this.options.maxItemsToShow = sanitizeInteger(this.options.maxItemsToShow, $.fn.autocomplete.defaults.maxItemsToShow, { min: 0 }); 294 this.options.maxCacheLength = sanitizeInteger(this.options.maxCacheLength, $.fn.autocomplete.defaults.maxCacheLength, { min: 1 }); 295 this.options.delay = sanitizeInteger(this.options.delay, $.fn.autocomplete.defaults.delay, { min: 0 }); 296 if (this.options.preventDefaultReturn != 2) { 297 this.options.preventDefaultReturn = this.options.preventDefaultReturn ? 1 : 0; 298 } 299 if (this.options.preventDefaultTab != 2) { 300 this.options.preventDefaultTab = this.options.preventDefaultTab ? 1 : 0; 301 } 302 303 /** 304 * Init DOM elements repository 305 */ 306 this.dom = {}; 307 308 /** 309 * Store the input element we're attached to in the repository 310 */ 311 this.dom.$elem = $elem; 312 313 /** 314 * Switch off the native autocomplete and add the input class 315 */ 316 this.dom.$elem.attr('autocomplete', 'off').addClass(this.options.inputClass); 317 318 /** 319 * Create DOM element to hold results, and force absolute position 320 */ 321 this.dom.$results = $('<div></div>').hide().addClass(this.options.resultsClass).css({ 322 position: 'absolute' 323 }); 324 $('body').append(this.dom.$results); 325 326 /** 327 * Attach keyboard monitoring to $elem 328 */ 329 $elem.keydown(function(e) { 330 self.lastKeyPressed_ = e.keyCode; 331 switch(self.lastKeyPressed_) { 332 333 case self.options.delimiterKeyCode: // comma = 188 334 if (self.options.useDelimiter && self.active_) { 335 self.selectCurrent(); 336 } 337 break; 338 339 // ignore navigational & special keys 340 case 35: // end 341 case 36: // home 342 case 16: // shift 343 case 17: // ctrl 344 case 18: // alt 345 case 37: // left 346 case 39: // right 347 break; 348 349 case 38: // up 350 e.preventDefault(); 351 if (self.active_) { 352 self.focusPrev(); 353 } else { 354 self.activate(); 355 } 356 return false; 357 358 case 40: // down 359 e.preventDefault(); 360 if (self.active_) { 361 self.focusNext(); 362 } else { 363 self.activate(); 364 } 365 return false; 366 367 case 9: // tab 368 if (self.active_) { 369 self.selectCurrent(); 370 if (self.options.preventDefaultTab) { 371 e.preventDefault(); 372 return false;
373 } 374 } 375 if (self.options.preventDefaultTab === 2) { 376 e.preventDefault(); 377 return false; 378 } 379 break; 380 381 case 13: // return 382 if (self.active_) { 383 self.selectCurrent(); 384 if (self.options.preventDefaultReturn) { 385 e.preventDefault(); 386 return false; 387 } 388 } 389 if (self.options.preventDefaultReturn === 2) { 390 e.preventDefault(); 391 return false; 392 } 393 break; 394 395 case 27: // escape 396 if (self.active_) { 397 e.preventDefault(); 398 self.deactivate(true); 399 return false; 400 } 401 break; 402 403 default: 404 self.activate(); 405 406 } 407 }); 408 409 /** 410 * Attach paste event listener because paste may occur much later then keydown or even without a keydown at all 411 */ 412 $elem.on('paste', function() { 413 self.activate(); 414 }); 415 416 /** 417 * Finish on blur event 418 * Use a timeout because instant blur gives race conditions 419 */ 420 var onBlurFunction = function() { 421 self.deactivate(true); 422 } 423 $elem.blur(function() { 424 if (self.finishOnBlur_) { 425 self.finishTimeout_ = setTimeout(onBlurFunction, 200); 426 } 427 }); 428 /** 429 * Catch a race condition on form submit 430 */ 431 $elem.parents('form').on('submit', onBlurFunction); 432 433 }; 434 435 /** 436 * Position output DOM elements 437 * @private 438 */ 439 $.Autocompleter.prototype.position = function() { 440 var offset = this.dom.$elem.offset(); 441 var height = this.dom.$results.outerHeight(); 442 var totalHeight = $(window).outerHeight(); 443 var inputBottom = offset.top + this.dom.$elem.outerHeight(); 444 var bottomIfDown = inputBottom + height; 445 // Set autocomplete results at the bottom of input 446 var position = {top: inputBottom, left: offset.left}; 447 if (bottomIfDown > totalHeight) { 448 // Try to set autocomplete results at the top of input 449 var topIfUp = offset.top - height; 450 if (topIfUp >= 0) { 451 position.top = topIfUp; 452 } 453 } 454 this.dom.$results.css(position); 455 }; 456 457 /** 458 * Read from cache 459 * @private 460 */ 461 $.Autocompleter.prototype.cacheRead = function(filter) { 462 var filterLength, searchLength, search, maxPos, pos; 463 if (this.options.useCache) { 464 filter = String(filter); 465 filterLength = filter.length; 466 if (this.options.matchSubset) { 467 searchLength = 1; 468 } else { 469 searchLength = filterLength; 470 } 471 while (searchLength <= filterLength) { 472 if (this.options.matchInside) { 473 maxPos = filterLength - searchLength; 474 } else { 475 maxPos = 0; 476 } 477 pos = 0; 478 while (pos <= maxPos) { 479 search = filter.substr(0, searchLength); 480 if (this.cacheData_[search] !== undefined) { 481 return this.cacheData_[search]; 482 } 483 pos++; 484 } 485 searchLength++; 486 } 487 } 488 return false; 489 }; 490 491 /** 492 * Write to cache 493 * @private 494 */ 495 $.Autocompleter.prototype.cacheWrite = function(filter, data) { 496 if (this.options.useCache) { 497 if (this.cacheLength_ >= this.options.maxCacheLength) { 498 this.cacheFlush(); 499 } 500 filter = String(filter); 501 if (this.cacheData_[filter] !== undefined) { 502 this.cacheLength_++; 503 } 504 this.cacheData_[filter] = data; 505 return this.cacheData_[filter]; 506 } 507 return false;
508 }; 509 510 /** 511 * Flush cache 512 * @public 513 */ 514 $.Autocompleter.prototype.cacheFlush = function() { 515 this.cacheData_ = {}; 516 this.cacheLength_ = 0; 517 }; 518 519 /** 520 * Call hook 521 * Note that all called hooks are passed the autocompleter object 522 * @param {string} hook 523 * @param data 524 * @returns Result of called hook, false if hook is undefined 525 */ 526 $.Autocompleter.prototype.callHook = function(hook, data) { 527 var f = this.options[hook]; 528 if (f && $.isFunction(f)) { 529 return f(data, this); 530 } 531 return false; 532 }; 533 534 /** 535 * Set timeout to activate autocompleter 536 */ 537 $.Autocompleter.prototype.activate = function() { 538 if (!this.options.enabled) return; 539 var self = this; 540 if (this.keyTimeout_) { 541 clearTimeout(this.keyTimeout_); 542 } 543 this.keyTimeout_ = setTimeout(function() { 544 self.activateNow(); 545 }, this.options.delay); 546 }; 547 548 /** 549 * Activate autocompleter immediately 550 */ 551 $.Autocompleter.prototype.activateNow = function() { 552 var value = this.beforeUseConverter(this.dom.$elem.val()); 553 if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) { 554 this.fetchData(value); 555 } 556 }; 557 558 /** 559 * Get autocomplete data for a given value 560 * @param {string} value Value to base autocompletion on 561 * @private 562 */ 563 $.Autocompleter.prototype.fetchData = function(value) { 564 var self = this; 565 var processResults = function(results, filter) { 566 if (self.options.processData) { 567 results = self.options.processData(results); 568 } 569 self.showResults(self.filterResults(results, filter), filter); 570 }; 571 this.lastProcessedValue_ = value; 572 if (value.length < this.options.minChars) { 573 processResults([], value); 574 } else if (this.options.data) { 575 processResults(this.options.data, value); 576 } else { 577 this.fetchRemoteData(value, function(remoteData) { 578 processResults(remoteData, value); 579 }); 580 } 581 }; 582 583 /** 584 * Get remote autocomplete data for a given value 585 * @param {string} filter The filter to base remote data on 586 * @param {function} callback The function to call after data retrieval 587 * @private 588 */ 589 $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) { 590 var data = this.cacheRead(filter); 591 if (data) { 592 callback(data); 593 } else { 594 var self = this; 595 var dataType = self.options.remoteDataType === 'json' ? 'json' : 'text'; 596 var ajaxCallback = function(data) { 597 var parsed = false; 598 if (data !== false) { 599 parsed = self.parseRemoteData(data); 600 self.cacheWrite(filter, parsed); 601 } 602 self.dom.$elem.removeClass(self.options.loadingClass); 603 callback(parsed); 604 }; 605 this.dom.$elem.addClass(this.options.loadingClass); 606 $.ajax({ 607 url: this.makeUrl(filter), 608 success: ajaxCallback, 609 error: function(jqXHR, textStatus, errorThrown) { 610 if($.isFunction(self.options.onError)) { 611 self.options.onError(jqXHR, textStatus, errorThrown); 612 } else { 613 ajaxCallback(false); 614 } 615 }, 616 dataType: dataType 617 }); 618 } 619 }; 620 621 /** 622 * Create or update an extra parameter for the remote request 623 * @param {string} name Parameter name 624 * @param {string} value Parameter value 625 * @public 626 */ 627 $.Autocompleter.prototype.setExtraParam = function(name, value) { 628 var index = $.trim(String(name)); 629 if (index) { 630 if (!this.options.extraParams) { 631 this.options.extraParams = {}; 632 } 633 if (this.options.extraParams[index] !== value) { 634 this.options.extraParams[index] = value; 635 this.cacheFlush(); 636 } 637 } 638 639 return this; 640 }; 641 642 /** 643 * Build the url for a remote request 644 * If options.queryParamName === false, append query to url instead of using a GET parameter 645 * @param {string} param The value parameter to pass to the backend 646 * @returns {string} The finished url with parameters 647 */ 648 $.Autocompleter.prototype.makeUrl = function(param) { 649 var self = this; 650 var url = this.options.url; 651 var params = $.extend({}, this.options.extraParams); 652 653 if (this.options.queryParamName === false) { 654 url += encodeURIComponent(param); 655 } else { 656 params[this.options.queryParamName] = param; 657 } 658 659 return makeUrl(url, params); 660 }; 661 662 /** 663 * Parse data received from server 664 * @param remoteData Data received from remote server 665 * @returns {array} Parsed data 666 */ 667 $.Autocompleter.prototype.parseRemoteData = function(remoteData) { 668 var remoteDataType; 669 var data = remoteData; 670 if (this.options.remoteDataType === 'json') { 671 remoteDataType = typeof(remoteData); 672 switch (remoteDataType) { 673 case 'object': 674 data = remoteData; 675 break; 676 case 'string': 677 data = $.parseJSON(remoteData); 678 break; 679 default: 680 throw new Error("Unexpected remote data type: " + remoteDataType); 681 } 682 return data; 683 } 684 return plainTextParser(data, this.options.lineSeparator, this.options.cellSeparator); 685 }; 686 687 /** 688 * Default filter for results 689 * @param {Object} result 690 * @param {String} filter 691 * @returns {boolean} Include this result 692 * @private 693 */ 694 $.Autocompleter.prototype.defaultFilter = function(result, filter) { 695 if (!result.value) { 696 return false;
697 } 698 if (this.options.filterResults) { 699 var pattern = this.matchStringConverter(filter); 700 var testValue = this.matchStringConverter(result.value); 701 if (!this.options.matchCase) { 702 pattern = pattern.toLowerCase(); 703 testValue = testValue.toLowerCase(); 704 } 705 var patternIndex = testValue.indexOf(pattern); 706 if (this.options.matchInside) { 707 return patternIndex > -1; 708 } else { 709 return patternIndex === 0; 710 } 711 } 712 return true; 713 }; 714 715 /** 716 * Filter result 717 * @param {Object} result 718 * @param {String} filter 719 * @returns {boolean} Include this result 720 * @private 721 */ 722 $.Autocompleter.prototype.filterResult = function(result, filter) { 723 // No filter 724 if (this.options.filter === false) { 725 return true; 726 } 727 // Custom filter 728 if ($.isFunction(this.options.filter)) { 729 return this.options.filter(result, filter); 730 } 731 // Default filter 732 return this.defaultFilter(result, filter); 733 }; 734 735 /** 736 * Filter results 737 * @param results 738 * @param filter 739 */ 740 $.Autocompleter.prototype.filterResults = function(results, filter) { 741 var filtered = []; 742 var i, result; 743 744 for (i = 0; i < results.length; i++) { 745 result = sanitizeResult(results[i]); 746 if (this.filterResult(result, filter)) { 747 filtered.push(result); 748 } 749 } 750 if (this.options.sortResults) { 751 filtered = this.sortResults(filtered, filter); 752 } 753 if (this.options.maxItemsToShow > 0 && this.options.maxItemsToShow < filtered.length) { 754 filtered.length = this.options.maxItemsToShow; 755 } 756 return filtered; 757 }; 758 759 /** 760 * Sort results 761 * @param results 762 * @param filter 763 */ 764 $.Autocompleter.prototype.sortResults = function(results, filter) { 765 var self = this; 766 var sortFunction = this.options.sortFunction; 767 if (!$.isFunction(sortFunction)) { 768 sortFunction = function(a, b, f) { 769 return sortValueAlpha(a, b, self.options.matchCase); 770 }; 771 } 772 results.sort(function(a, b) { 773 return sortFunction(a, b, filter, self.options); 774 }); 775 return results; 776 }; 777 778 /** 779 * Convert string before matching 780 * @param s 781 * @param a 782 * @param b 783 */ 784 $.Autocompleter.prototype.matchStringConverter = function(s, a, b) { 785 var converter = this.options.matchStringConverter; 786 if ($.isFunction(converter)) { 787 s = converter(s, a, b); 788 } 789 return s; 790 }; 791 792 /** 793 * Convert string before use 794 * @param {String} s 795 */ 796 $.Autocompleter.prototype.beforeUseConverter = function(s) { 797 s = this.getValue(s); 798 var converter = this.options.beforeUseConverter; 799 if ($.isFunction(converter)) { 800 s = converter(s); 801 } 802 return s; 803 }; 804 805 /** 806 * Enable finish on blur event 807 */ 808 $.Autocompleter.prototype.enableFinishOnBlur = function() { 809 this.finishOnBlur_ = true; 810 }; 811 812 /** 813 * Disable finish on blur event 814 */ 815 $.Autocompleter.prototype.disableFinishOnBlur = function() { 816 this.finishOnBlur_ = false; 817 }; 818 819 /** 820 * Create a results item (LI element) from a result 821 * @param result 822 */ 823 $.Autocompleter.prototype.createItemFromResult = function(result) { 824 var self = this; 825 var $li = $('<li/>'); 826 $li.html(this.showResult(result.value, result.data)); 827 $li.data({value: result.value, data: result.data}) 828 .click(function() { 829 self.selectItem($li); 830 }) 831 .mousedown(self.disableFinishOnBlur) 832 .mouseup(self.enableFinishOnBlur) 833 ; 834 return $li; 835 }; 836 837 /** 838 * Get all items from the results list 839 * @param result 840 */ 841 $.Autocompleter.prototype.getItems = function() { 842 return $('>ul>li', this.dom.$results); 843 }; 844 845 /** 846 * Show all results 847 * @param results 848 * @param filter 849 */ 850 $.Autocompleter.prototype.showResults = function(results, filter) { 851 var numResults = results.length; 852 var self = this; 853 var $ul = $('<ul></ul>'); 854 var i, result, $li, autoWidth, first = false, $first = false;
855 856 if (numResults) { 857 for (i = 0; i < numResults; i++) { 858 result = results[i]; 859 $li = this.createItemFromResult(result); 860 $ul.append($li); 861 if (first === false) { 862 first = String(result.value); 863 $first = $li; 864 $li.addClass(this.options.firstItemClass); 865 } 866 if (i === numResults - 1) { 867 $li.addClass(this.options.lastItemClass); 868 } 869 } 870 871 this.dom.$results.html($ul).show(); 872 873 // Always recalculate position since window size or 874 // input element location may have changed. 875 this.position(); 876 if (this.options.autoWidth) { 877 autoWidth = this.dom.$elem.outerWidth() - this.dom.$results.outerWidth() + this.dom.$results.width(); 878 this.dom.$results.css(this.options.autoWidth, autoWidth); 879 } 880 this.getItems().hover( 881 function() { self.focusItem(this); }, 882 function() { /* void */ } 883 ); 884 if (this.autoFill(first, filter) || this.options.selectFirst || (this.options.selectOnly && numResults === 1)) { 885 this.focusItem($first); 886 } 887 this.active_ = true; 888 } else { 889 this.hideResults(); 890 this.active_ = false; 891 } 892 }; 893 894 $.Autocompleter.prototype.showResult = function(value, data) { 895 if ($.isFunction(this.options.showResult)) { 896 return this.options.showResult(value, data); 897 } else { 898 return $('<p></p>').text(value).html(); 899 } 900 }; 901 902 $.Autocompleter.prototype.autoFill = function(value, filter) { 903 var lcValue, lcFilter, valueLength, filterLength; 904 if (this.options.autoFill && this.lastKeyPressed_ !== 8) { 905 lcValue = String(value).toLowerCase(); 906 lcFilter = String(filter).toLowerCase(); 907 valueLength = value.length; 908 filterLength = filter.length; 909 if (lcValue.substr(0, filterLength) === lcFilter) { 910 var d = this.getDelimiterOffsets(); 911 var pad = d.start ? ' ' : ''; // if there is a preceding delimiter 912 this.setValue( pad + value ); 913 var start = filterLength + d.start + pad.length; 914 var end = valueLength + d.start + pad.length; 915 this.selectRange(start, end); 916 return true; 917 } 918 } 919 return false; 920 }; 921 922 $.Autocompleter.prototype.focusNext = function() { 923 this.focusMove(+1); 924 }; 925 926 $.Autocompleter.prototype.focusPrev = function() { 927 this.focusMove(-1); 928 }; 929 930 $.Autocompleter.prototype.focusMove = function(modifier) { 931 var $items = this.getItems(); 932 modifier = sanitizeInteger(modifier, 0); 933 if (modifier) { 934 for (var i = 0; i < $items.length; i++) { 935 if ($($items[i]).hasClass(this.selectClass_)) { 936 this.focusItem(i + modifier); 937 return; 938 } 939 } 940 } 941 this.focusItem(0); 942 }; 943 944 $.Autocompleter.prototype.focusItem = function(item) { 945 var $item, $items = this.getItems(); 946 if ($items.length) { 947 $items.removeClass(this.selectClass_).removeClass(this.options.selectClass); 948 if (typeof item === 'number') { 949 if (item < 0) { 950 item = 0; 951 } else if (item >= $items.length) { 952 item = $items.length - 1; 953 } 954 $item = $($items[item]); 955 } else { 956 $item = $(item); 957 } 958 if ($item) { 959 $item.addClass(this.selectClass_).addClass(this.options.selectClass); 960 } 961 } 962 }; 963 964 $.Autocompleter.prototype.selectCurrent = function() { 965 var $item = $('li.' + this.selectClass_, this.dom.$results); 966 if ($item.length === 1) { 967 this.selectItem($item); 968 } else { 969 this.deactivate(false); 970 } 971 }; 972 973 $.Autocompleter.prototype.selectItem = function($li) { 974 var value = $li.data('value'); 975 var data = $li.data('data'); 976 var displayValue = this.displayValue(value, data); 977 var processedDisplayValue = this.beforeUseConverter(displayValue); 978 this.lastProcessedValue_ = processedDisplayValue; 979 this.lastSelectedValue_ = processedDisplayValue; 980 var d = this.getDelimiterOffsets(); 981 var delimiter = this.options.delimiterChar; 982 var elem = this.dom.$elem; 983 var extraCaretPos = 0; 984 if ( this.options.useDelimiter ) { 985 // if there is a preceding delimiter, add a space after the delimiter 986 if ( elem.val().substring(d.start-1, d.start) == delimiter && delimiter != ' ' ) { 987 displayValue = ' ' + displayValue; 988 } 989 // if there is not already a delimiter trailing this value, add it 990 if ( elem.val().substring(d.end, d.end+1) != delimiter && this.lastKeyPressed_ != this.options.delimiterKeyCode ) { 991 displayValue = displayValue + delimiter; 992 } else { 993 // move the cursor after the existing trailing delimiter 994 extraCaretPos = 1; 995 } 996 } 997 this.setValue(displayValue); 998 this.setCaret(d.start + displayValue.length + extraCaretPos); 999 this.callHook('onItemSelect', { value: value, data: data }); 1000 this.deactivate(true); 1001 elem.focus(); 1002 }; 1003 1004 $.Autocompleter.prototype.displayValue = function(value, data) { 1005 if ($.isFunction(this.options.displayValue)) { 1006 return this.options.displayValue(value, data); 1007 } 1008 return value; 1009 }; 1010 1011 $.Autocompleter.prototype.hideResults = function() { 1012 this.dom.$results.hide(); 1013 }; 1014 1015 $.Autocompleter.prototype.deactivate = function(finish) { 1016 if (this.finishTimeout_) { 1017 clearTimeout(this.finishTimeout_); 1018 } 1019 if (this.keyTimeout_) { 1020 clearTimeout(this.keyTimeout_); 1021 } 1022 if (finish) { 1023 if (this.lastProcessedValue_ !== this.lastSelectedValue_) { 1024 if (this.options.mustMatch) { 1025 this.setValue(''); 1026 } 1027 this.callHook('onNoMatch'); 1028 } 1029 if (this.active_) { 1030 this.callHook('onFinish'); 1031 } 1032 this.lastKeyPressed_ = null; 1033 this.lastProcessedValue_ = null; 1034 this.lastSelectedValue_ = null; 1035 this.active_ = false;
1036 } 1037 this.hideResults(); 1038 }; 1039 1040 $.Autocompleter.prototype.selectRange = function(start, end) { 1041 var input = this.dom.$elem.get(0); 1042 if (input.setSelectionRange) { 1043 input.focus(); 1044 input.setSelectionRange(start, end); 1045 } else if (input.createTextRange) { 1046 var range = input.createTextRange(); 1047 range.collapse(true); 1048 range.moveEnd('character', end); 1049 range.moveStart('character', start); 1050 range.select(); 1051 } 1052 }; 1053 1054 /** 1055 * Move caret to position 1056 * @param {Number} pos 1057 */ 1058 $.Autocompleter.prototype.setCaret = function(pos) { 1059 this.selectRange(pos, pos); 1060 }; 1061 1062 /** 1063 * Get caret position 1064 */ 1065 $.Autocompleter.prototype.getCaret = function() { 1066 var $elem = this.dom.$elem; 1067 var elem = $elem[0]; 1068 var val, selection, range, start, end, stored_range; 1069 if (elem.createTextRange) { // IE 1070 selection = document.selection; 1071 if (elem.tagName.toLowerCase() != 'textarea') { 1072 val = $elem.val(); 1073 range = selection.createRange().duplicate(); 1074 range.moveEnd('character', val.length); 1075 if (range.text === '') { 1076 start = val.length; 1077 } else { 1078 start = val.lastIndexOf(range.text); 1079 } 1080 range = selection.createRange().duplicate(); 1081 range.moveStart('character', -val.length); 1082 end = range.text.length; 1083 } else { 1084 range = selection.createRange(); 1085 stored_range = range.duplicate(); 1086 stored_range.moveToElementText(elem); 1087 stored_range.setEndPoint('EndToEnd', range); 1088 start = stored_range.text.length - range.text.length; 1089 end = start + range.text.length; 1090 } 1091 } else { 1092 start = $elem[0].selectionStart; 1093 end = $elem[0].selectionEnd; 1094 } 1095 return { 1096 start: start, 1097 end: end 1098 }; 1099 }; 1100 1101 /** 1102 * Set the value that is currently being autocompleted 1103 * @param {String} value 1104 */ 1105 $.Autocompleter.prototype.setValue = function(value) { 1106 if ( this.options.useDelimiter ) { 1107 // set the substring between the current delimiters 1108 var val = this.dom.$elem.val(); 1109 var d = this.getDelimiterOffsets(); 1110 var preVal = val.substring(0, d.start); 1111 var postVal = val.substring(d.end); 1112 value = preVal + value + postVal; 1113 } 1114 this.dom.$elem.val(value); 1115 }; 1116 1117 /** 1118 * Get the value currently being autocompleted 1119 * @param {String} value 1120 */ 1121 $.Autocompleter.prototype.getValue = function(value) { 1122 if ( this.options.useDelimiter ) { 1123 var d = this.getDelimiterOffsets(); 1124 return value.substring(d.start, d.end).trim(); 1125 } else { 1126 return value; 1127 } 1128 }; 1129 1130 /** 1131 * Get the offsets of the value currently being autocompleted 1132 */ 1133 $.Autocompleter.prototype.getDelimiterOffsets = function() { 1134 var val = this.dom.$elem.val(); 1135 if ( this.options.useDelimiter ) { 1136 var preCaretVal = val.substring(0, this.getCaret().start); 1137 var start = preCaretVal.lastIndexOf(this.options.delimiterChar) + 1; 1138 var postCaretVal = val.substring(this.getCaret().start); 1139 var end = postCaretVal.indexOf(this.options.delimiterChar); 1140 if ( end == -1 ) end = val.length; 1141 end += this.getCaret().start; 1142 } else { 1143 start = 0; 1144 end = val.length; 1145 } 1146 return { 1147 start: start, 1148 end: end 1149 }; 1150 }; 1151 1152})(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.