|
54 | 54 | that = this, |
55 | 55 | defaults = { |
56 | 56 | autoSelectFirst: false, |
57 | | - appendTo: 'body', |
| 57 | + appendTo: document.body, |
58 | 58 | serviceUrl: null, |
59 | 59 | lookup: null, |
60 | 60 | onSelect: null, |
|
76 | 76 | dataType: 'text', |
77 | 77 | currentRequest: null, |
78 | 78 | triggerSelectOnValidInput: true, |
| 79 | + preventBadQueries: true, |
79 | 80 | lookupFilter: function (suggestion, originalQuery, queryLowerCase) { |
80 | 81 | return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; |
81 | 82 | }, |
82 | 83 | paramName: 'query', |
83 | 84 | transformResult: function (response) { |
84 | 85 | return typeof response === 'string' ? $.parseJSON(response) : response; |
85 | | - } |
| 86 | + }, |
| 87 | + showNoSuggestionNotice: false, |
| 88 | + noSuggestionNotice: 'No results', |
| 89 | + orientation: 'bottom', |
| 90 | + forceFixPosition: false |
86 | 91 | }; |
87 | 92 |
|
88 | 93 | // Shared variables: |
|
98 | 103 | that.onChange = null; |
99 | 104 | that.isLocal = false; |
100 | 105 | that.suggestionsContainer = null; |
| 106 | + that.noSuggestionsContainer = null; |
101 | 107 | that.options = $.extend({}, defaults, options); |
102 | 108 | that.classes = { |
103 | 109 | selected: 'autocomplete-selected', |
|
131 | 137 | suggestionSelector = '.' + that.classes.suggestion, |
132 | 138 | selected = that.classes.selected, |
133 | 139 | options = that.options, |
134 | | - container; |
| 140 | + container, |
| 141 | + noSuggestionsContainer; |
135 | 142 |
|
136 | 143 | // Remove autocomplete attribute to prevent native suggestions: |
137 | 144 | that.element.setAttribute('autocomplete', 'off'); |
|
143 | 150 | } |
144 | 151 | }; |
145 | 152 |
|
| 153 | + // html() deals with many types: htmlString or Element or Array or jQuery |
| 154 | + that.noSuggestionsContainer = $('<div class="autocomplete-no-suggestion"></div>') |
| 155 | + .html(this.options.noSuggestionNotice).get(0); |
| 156 | + |
146 | 157 | that.suggestionsContainer = Autocomplete.utils.createNode(options.containerClass); |
147 | 158 |
|
148 | 159 | container = $(that.suggestionsContainer); |
|
170 | 181 | that.select($(this).data('index')); |
171 | 182 | }); |
172 | 183 |
|
173 | | - that.fixPosition(); |
174 | | - |
175 | 184 | that.fixPositionCapture = function () { |
176 | 185 | if (that.visible) { |
177 | 186 | that.fixPosition(); |
|
211 | 220 | options.lookup = that.verifySuggestionsFormat(options.lookup); |
212 | 221 | } |
213 | 222 |
|
| 223 | + options.orientation = that.validateOrientation(options.orientation, 'bottom'); |
| 224 | + |
214 | 225 | // Adjust height, width and z-index: |
215 | 226 | $(that.suggestionsContainer).css({ |
216 | 227 | 'max-height': options.maxHeight + 'px', |
|
219 | 230 | }); |
220 | 231 | }, |
221 | 232 |
|
| 233 | + |
222 | 234 | clearCache: function () { |
223 | 235 | this.cachedResponse = {}; |
224 | 236 | this.badQueries = []; |
|
243 | 255 | }, |
244 | 256 |
|
245 | 257 | fixPosition: function () { |
246 | | - var that = this, |
247 | | - offset, |
248 | | - styles; |
| 258 | + // Use only when container has already its content |
249 | 259 |
|
250 | | - // Don't adjsut position if custom container has been specified: |
251 | | - if (that.options.appendTo !== 'body') { |
| 260 | + var that = this, |
| 261 | + $container = $(that.suggestionsContainer), |
| 262 | + containerParent = $container.parent().get(0); |
| 263 | + // Fix position automatically when appended to body. |
| 264 | + // In other cases force parameter must be given. |
| 265 | + if (containerParent !== document.body && !that.options.forceFixPosition) |
252 | 266 | return; |
| 267 | + |
| 268 | + // Choose orientation |
| 269 | + var orientation = that.options.orientation, |
| 270 | + containerHeight = $container.outerHeight(), |
| 271 | + height = that.el.outerHeight(), |
| 272 | + offset = that.el.offset(), |
| 273 | + styles = { 'top': offset.top, 'left': offset.left }; |
| 274 | + |
| 275 | + if (orientation == 'auto') { |
| 276 | + var viewPortHeight = $(window).height(), |
| 277 | + scrollTop = $(window).scrollTop(), |
| 278 | + top_overflow = -scrollTop + offset.top - containerHeight, |
| 279 | + bottom_overflow = scrollTop + viewPortHeight - (offset.top + height + containerHeight); |
| 280 | + |
| 281 | + if (Math.max(top_overflow, bottom_overflow) === top_overflow) |
| 282 | + orientation = 'top'; |
| 283 | + else |
| 284 | + orientation = 'bottom'; |
253 | 285 | } |
254 | 286 |
|
255 | | - offset = that.el.offset(); |
| 287 | + if (orientation === 'top') |
| 288 | + styles.top += -containerHeight; |
| 289 | + else |
| 290 | + styles.top += height; |
256 | 291 |
|
257 | | - styles = { |
258 | | - top: (offset.top + that.el.outerHeight()) + 'px', |
259 | | - left: offset.left + 'px' |
260 | | - }; |
| 292 | + // If container is not positioned to body, |
| 293 | + // correct its position using offset parent offset |
| 294 | + if(containerParent !== document.body) { |
| 295 | + var opacity = $container.css('opacity'), |
| 296 | + parentOffsetDiff; |
| 297 | + if (!that.visible) |
| 298 | + $container.css('opacity', 0).show(); |
| 299 | + |
| 300 | + parentOffsetDiff = $container.offsetParent().offset(); |
| 301 | + styles.top -= parentOffsetDiff.top; |
| 302 | + styles.left -= parentOffsetDiff.left; |
| 303 | + |
| 304 | + if (!that.visible) |
| 305 | + $container.css('opacity', opacity).hide(); |
| 306 | + } |
261 | 307 |
|
| 308 | + // -2px to account for suggestions border. |
262 | 309 | if (that.options.width === 'auto') { |
263 | 310 | styles.width = (that.el.outerWidth() - 2) + 'px'; |
264 | 311 | } |
265 | 312 |
|
266 | | - $(that.suggestionsContainer).css(styles); |
| 313 | + $container.css(styles); |
267 | 314 | }, |
268 | 315 |
|
269 | 316 | enableKillerFn: function () { |
|
473 | 520 | that = this, |
474 | 521 | options = that.options, |
475 | 522 | serviceUrl = options.serviceUrl, |
476 | | - data, |
| 523 | + params, |
477 | 524 | cacheKey; |
478 | 525 |
|
479 | 526 | options.params[options.paramName] = q; |
480 | | - data = options.ignoreParams ? null : options.params; |
| 527 | + params = options.ignoreParams ? null : options.params; |
481 | 528 |
|
482 | 529 | if (that.isLocal) { |
483 | 530 | response = that.getSuggestionsLocal(q); |
484 | 531 | } else { |
485 | 532 | if ($.isFunction(serviceUrl)) { |
486 | 533 | serviceUrl = serviceUrl.call(that.element, q); |
487 | 534 | } |
488 | | - cacheKey = serviceUrl + '?' + $.param(data || {}); |
| 535 | + cacheKey = serviceUrl + '?' + $.param(params || {}); |
489 | 536 | response = that.cachedResponse[cacheKey]; |
490 | 537 | } |
491 | 538 |
|
|
501 | 548 | } |
502 | 549 | that.currentRequest = $.ajax({ |
503 | 550 | url: serviceUrl, |
504 | | - data: data, |
| 551 | + data: params, |
505 | 552 | type: options.type, |
506 | 553 | dataType: options.dataType |
507 | 554 | }).done(function (data) { |
| 555 | + var result; |
508 | 556 | that.currentRequest = null; |
509 | | - that.processResponse(data, q, cacheKey); |
510 | | - options.onSearchComplete.call(that.element, q); |
| 557 | + result = options.transformResult(data); |
| 558 | + that.processResponse(result, q, cacheKey); |
| 559 | + options.onSearchComplete.call(that.element, q, result.suggestions); |
511 | 560 | }).fail(function (jqXHR, textStatus, errorThrown) { |
512 | 561 | options.onSearchError.call(that.element, q, jqXHR, textStatus, errorThrown); |
513 | 562 | }); |
514 | 563 | } |
515 | 564 | }, |
516 | 565 |
|
517 | 566 | isBadQuery: function (q) { |
| 567 | + if (!this.options.preventBadQueries){ |
| 568 | + return false; |
| 569 | + } |
| 570 | + |
518 | 571 | var badQueries = this.badQueries, |
519 | 572 | i = badQueries.length; |
520 | 573 |
|
|
537 | 590 |
|
538 | 591 | suggest: function () { |
539 | 592 | if (this.suggestions.length === 0) { |
540 | | - this.hide(); |
| 593 | + this.options.showNoSuggestionNotice ? this.noSuggestions() : this.hide(); |
541 | 594 | return; |
542 | 595 | } |
543 | 596 |
|
|
548 | 601 | className = that.classes.suggestion, |
549 | 602 | classSelected = that.classes.selected, |
550 | 603 | container = $(that.suggestionsContainer), |
| 604 | + noSuggestionsContainer = $(that.noSuggestionsContainer), |
551 | 605 | beforeRender = options.beforeRender, |
552 | 606 | html = '', |
553 | 607 | index, |
|
566 | 620 | html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>'; |
567 | 621 | }); |
568 | 622 |
|
569 | | - // If width is auto, adjust width before displaying suggestions, |
570 | | - // because if instance was created before input had width, it will be zero. |
571 | | - // Also it adjusts if input width has changed. |
572 | | - // -2px to account for suggestions border. |
573 | | - if (options.width === 'auto') { |
574 | | - width = that.el.outerWidth() - 2; |
575 | | - container.width(width > 0 ? width : 300); |
576 | | - } |
| 623 | + this.adjustContainerWidth(); |
577 | 624 |
|
| 625 | + noSuggestionsContainer.detach(); |
578 | 626 | container.html(html); |
579 | 627 |
|
580 | 628 | // Select first value by default: |
|
587 | 635 | beforeRender.call(that.element, container); |
588 | 636 | } |
589 | 637 |
|
| 638 | + that.fixPosition(); |
| 639 | + |
590 | 640 | container.show(); |
591 | 641 | that.visible = true; |
592 | 642 |
|
593 | 643 | that.findBestHint(); |
594 | 644 | }, |
595 | 645 |
|
| 646 | + noSuggestions: function() { |
| 647 | + var that = this, |
| 648 | + container = $(that.suggestionsContainer), |
| 649 | + noSuggestionsContainer = $(that.noSuggestionsContainer); |
| 650 | + |
| 651 | + this.adjustContainerWidth(); |
| 652 | + |
| 653 | + // Some explicit steps. Be careful here as it easy to get |
| 654 | + // noSuggestionsContainer removed from DOM if not detached properly. |
| 655 | + noSuggestionsContainer.detach(); |
| 656 | + container.empty(); // clean suggestions if any |
| 657 | + container.append(noSuggestionsContainer); |
| 658 | + |
| 659 | + that.fixPosition(); |
| 660 | + |
| 661 | + container.show(); |
| 662 | + that.visible = true; |
| 663 | + }, |
| 664 | + |
| 665 | + adjustContainerWidth: function() { |
| 666 | + var that = this, |
| 667 | + options = that.options, |
| 668 | + width, |
| 669 | + container = $(that.suggestionsContainer); |
| 670 | + |
| 671 | + // If width is auto, adjust width before displaying suggestions, |
| 672 | + // because if instance was created before input had width, it will be zero. |
| 673 | + // Also it adjusts if input width has changed. |
| 674 | + // -2px to account for suggestions border. |
| 675 | + if (options.width === 'auto') { |
| 676 | + width = that.el.outerWidth() - 2; |
| 677 | + container.width(width > 0 ? width : 300); |
| 678 | + } |
| 679 | + }, |
| 680 | + |
596 | 681 | findBestHint: function () { |
597 | 682 | var that = this, |
598 | 683 | value = that.el.val().toLowerCase(), |
|
637 | 722 | return suggestions; |
638 | 723 | }, |
639 | 724 |
|
640 | | - processResponse: function (response, originalQuery, cacheKey) { |
| 725 | + validateOrientation: function(orientation, fallback) { |
| 726 | + orientation = orientation.trim().toLowerCase(); |
| 727 | + if(['auto', 'bottom', 'top'].indexOf(orientation) == '-1') |
| 728 | + orientation = fallback; |
| 729 | + return orientation |
| 730 | + }, |
| 731 | + |
| 732 | + processResponse: function (result, originalQuery, cacheKey) { |
641 | 733 | var that = this, |
642 | | - options = that.options, |
643 | | - result = options.transformResult(response, originalQuery); |
| 734 | + options = that.options; |
644 | 735 |
|
645 | 736 | result.suggestions = that.verifySuggestionsFormat(result.suggestions); |
646 | 737 |
|
647 | 738 | // Cache results if cache is not disabled: |
648 | 739 | if (!options.noCache) { |
649 | 740 | that.cachedResponse[cacheKey] = result; |
650 | | - if (result.suggestions.length === 0) { |
651 | | - that.badQueries.push(cacheKey); |
| 741 | + if (options.preventBadQueries && result.suggestions.length === 0) { |
| 742 | + that.badQueries.push(originalQuery); |
652 | 743 | } |
653 | 744 | } |
654 | 745 |
|
|
754 | 845 | suggestion = that.suggestions[index]; |
755 | 846 |
|
756 | 847 | that.currentValue = that.getValue(suggestion.value); |
757 | | - that.el.val(that.currentValue); |
| 848 | + |
| 849 | + if (that.currentValue !== that.el.val()) { |
| 850 | + that.el.val(that.currentValue); |
| 851 | + } |
| 852 | + |
758 | 853 | that.signalHint(null); |
759 | 854 | that.suggestions = []; |
760 | 855 | that.selection = suggestion; |
|
794 | 889 | }; |
795 | 890 |
|
796 | 891 | // Create chainable jQuery plugin: |
797 | | - $.fn.autocomplete = function (options, args) { |
| 892 | + $.fn.autocomplete = function (options, args) { |
798 | 893 | var dataKey = 'autocomplete'; |
799 | 894 | // If function invoked without argument return |
800 | 895 | // instance of the first matched element: |
|
0 commit comments