From 3542ada13f275f8090cb56df4d37413376d0019c Mon Sep 17 00:00:00 2001 From: Tomas Kirda Date: Sat, 23 Nov 2013 21:40:52 -0600 Subject: [PATCH] Add ability to select suggestion if it matches typed value. Fixes #112. --- index.htm | 1 + readme.md | 2 + content/countries.txt => scripts/countries.js | 2 +- scripts/demo.js | 126 ++++++++---------- spec/autocompleteBehavior.js | 49 ++++++- src/jquery.autocomplete.js | 70 +++++++--- 6 files changed, 161 insertions(+), 89 deletions(-) rename content/countries.txt => scripts/countries.js (99%) diff --git a/index.htm b/index.htm index 1477289..3a3109a 100644 --- a/index.htm +++ b/index.htm @@ -42,6 +42,7 @@ + diff --git a/readme.md b/readme.md index e444709..3520953 100644 --- a/readme.md +++ b/readme.md @@ -33,6 +33,8 @@ The standard jquery.autocomplete.js file is around 2.7KB when minified via Closu * `onSearchStart`: `function (query) {}` called before ajax request. `this` is bound to input element. * `onSearchComplete`: `function (query) {}` called after ajax response is processed. `this` is bound to input element. * `onSearchError`: `function (query, jqXHR, textStatus, errorThrown) {}` called if ajax request fails. `this` is bound to input element. + * `onInvalidateSelection`: `function () {}` called when input is altered after selection has been made. `this` is bound to input element. + * `triggerSelectOnValidInput`: Boolean value indicating if `select` should be triggered if it matches suggestion. Default `true`. * `beforeRender`: `function (container) {}` called before displaying the suggestions. You may manipulate suggestions DOM before it is displayed. * `tabDisabled`: Default `false`. Set to true to leave the cursor in the input field after the user tabs to select a suggestion. * `paramName`: Default `query`. The name of the request parameter that contains the query. diff --git a/content/countries.txt b/scripts/countries.js similarity index 99% rename from content/countries.txt rename to scripts/countries.js index 440081e..1e5bee6 100644 --- a/content/countries.txt +++ b/scripts/countries.js @@ -1,4 +1,4 @@ -{ +var countries = { "AD": "Andorra", "AE": "United Arab Emirates", "AF": "Afghanistan", diff --git a/scripts/demo.js b/scripts/demo.js index 6368ff6..fa78f4f 100644 --- a/scripts/demo.js +++ b/scripts/demo.js @@ -1,78 +1,68 @@ /*jslint browser: true, white: true, plusplus: true */ -/*global $: true */ +/*global $, countries */ $(function () { 'use strict'; - // Load countries then initialize plugin: - $.ajax({ - url: 'content/countries.txt', - dataType: 'json' - }).done(function (source) { + var countriesArray = $.map(countries, function (value, key) { return { value: value, data: key }; }); - var countriesArray = $.map(source, function (value, key) { return { value: value, data: key }; }), - countries = $.map(source, function (value) { return value; }); + // Setup jQuery ajax mock: + $.mockjax({ + url: '*', + responseTime: 2000, + response: function (settings) { + var query = settings.data.query, + queryLowerCase = query.toLowerCase(), + re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'), + suggestions = $.grep(countriesArray, function (country) { + // return country.value.toLowerCase().indexOf(queryLowerCase) === 0; + return re.test(country.value); + }), + response = { + query: query, + suggestions: suggestions + }; - // Setup jQuery ajax mock: - $.mockjax({ - url: '*', - responseTime: 2000, - response: function (settings) { - var query = settings.data.query, - queryLowerCase = query.toLowerCase(), - re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'), - suggestions = $.grep(countriesArray, function (country) { - // return country.value.toLowerCase().indexOf(queryLowerCase) === 0; - return re.test(country.value); - }), - response = { - query: query, - suggestions: suggestions - }; - - this.responseText = JSON.stringify(response); - } - }); - - // Initialize ajax autocomplete: - $('#autocomplete-ajax').autocomplete({ - // serviceUrl: '/autosuggest/service/url', - lookup: countriesArray, - lookupFilter: function(suggestion, originalQuery, queryLowerCase) { - var re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'); - return re.test(suggestion.value); - }, - onSelect: function(suggestion) { - $('#selction-ajax').html('You selected: ' + suggestion.value + ', ' + suggestion.data); - }, - onHint: function (hint) { - $('#autocomplete-ajax-x').val(hint); - }, - onInvalidateSelection: function() { - $('#selction-ajax').html('You selected: none'); - } - }); - - // Initialize autocomplete with local lookup: - $('#autocomplete').autocomplete({ - lookup: countriesArray, - minChars: 0, - onSelect: function (suggestion) { - $('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data); - } - }); - - // Initialize autocomplete with custom appendTo: - $('#autocomplete-custom-append').autocomplete({ - lookup: countriesArray, - appendTo: '#suggestions-container' - }); - - // Initialize autocomplete with custom appendTo: - $('#autocomplete-dynamic').autocomplete({ - lookup: countriesArray - }); - + this.responseText = JSON.stringify(response); + } }); + // Initialize ajax autocomplete: + $('#autocomplete-ajax').autocomplete({ + // serviceUrl: '/autosuggest/service/url', + lookup: countriesArray, + lookupFilter: function(suggestion, originalQuery, queryLowerCase) { + var re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'); + return re.test(suggestion.value); + }, + onSelect: function(suggestion) { + $('#selction-ajax').html('You selected: ' + suggestion.value + ', ' + suggestion.data); + }, + onHint: function (hint) { + $('#autocomplete-ajax-x').val(hint); + }, + onInvalidateSelection: function() { + $('#selction-ajax').html('You selected: none'); + } + }); + + // Initialize autocomplete with local lookup: + $('#autocomplete').autocomplete({ + lookup: countriesArray, + minChars: 0, + onSelect: function (suggestion) { + $('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data); + } + }); + + // Initialize autocomplete with custom appendTo: + $('#autocomplete-custom-append').autocomplete({ + lookup: countriesArray, + appendTo: '#suggestions-container' + }); + + // Initialize autocomplete with custom appendTo: + $('#autocomplete-dynamic').autocomplete({ + lookup: countriesArray + }); }); \ No newline at end of file diff --git a/spec/autocompleteBehavior.js b/spec/autocompleteBehavior.js index 6f90ea7..4d31f9a 100644 --- a/spec/autocompleteBehavior.js +++ b/spec/autocompleteBehavior.js @@ -66,14 +66,15 @@ describe('Autocomplete', function () { context, value, data, - autocomplete = new $.Autocomplete(input, { + autocomplete = $(input).autocomplete({ lookup: [{ value: 'A', data: 'B' }], + triggerSelectOnValidInput: false, onSelect: function (suggestion) { context = this; value = suggestion.value; data = suggestion.data; } - }); + }).autocomplete(); input.value = 'A'; autocomplete.onValueChange(); @@ -260,7 +261,7 @@ describe('Autocomplete', function () { ajaxExecuted = true; var response = { query: null, - suggestions: ['A', 'B', 'C'] + suggestions: ['Aa', 'Bb', 'Cc'] }; this.responseText = JSON.stringify(response); } @@ -276,7 +277,7 @@ describe('Autocomplete', function () { runs(function () { expect(ajaxExecuted).toBe(true); expect(autocomplete.suggestions.length).toBe(3); - expect(autocomplete.suggestions[0].value).toBe('A'); + expect(autocomplete.suggestions[0].value).toBe('Aa'); }); }); @@ -496,4 +497,44 @@ describe('Autocomplete', function () { expect(context).toBe(element); expect(elementCount).toBe(1); }); + + it('Should trigger select when input value matches suggestion', function () { + var input = $(''), + instance, + suggestionData = false; + + input.autocomplete({ + lookup: [{ value: 'Jamaica', data: 'J' }], + triggerSelectOnValidInput: true, + onSelect: function (suggestion) { + suggestionData = suggestion.data; + } + }); + + input.val('Jamaica'); + instance = input.autocomplete(); + instance.onValueChange(); + + expect(suggestionData).toBe('J'); + }); + + it('Should NOT trigger select when input value matches suggestion', function () { + var input = $(''), + instance, + suggestionData = null; + + input.autocomplete({ + lookup: [{ value: 'Jamaica', data: 'J' }], + triggerSelectOnValidInput: false, + onSelect: function (suggestion) { + suggestionData = suggestion.data; + } + }); + + input.val('Jamaica'); + instance = input.autocomplete(); + instance.onValueChange(); + + expect(suggestionData).toBeNull(); + }); }); \ No newline at end of file diff --git a/src/jquery.autocomplete.js b/src/jquery.autocomplete.js index 7f331bb..3cf2954 100644 --- a/src/jquery.autocomplete.js +++ b/src/jquery.autocomplete.js @@ -75,6 +75,7 @@ tabDisabled: false, dataType: 'text', currentRequest: null, + triggerSelectOnValidInput: true, lookupFilter: function (suggestion, originalQuery, queryLowerCase) { return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1; }, @@ -390,32 +391,57 @@ onValueChange: function () { var that = this, - q; + options = that.options, + value = that.el.val(), + query = that.getQuery(value), + index; if (that.selection) { that.selection = null; - (that.options.onInvalidateSelection || $.noop)(); + (options.onInvalidateSelection || $.noop).call(that.element); } clearInterval(that.onChangeInterval); - that.currentValue = that.el.val(); - - q = that.getQuery(that.currentValue); + that.currentValue = value; that.selectedIndex = -1; - if (q.length < that.options.minChars) { + // Check existing suggestion for the match before proceeding: + if (options.triggerSelectOnValidInput) { + index = that.findSuggestionIndex(query); + if (index !== -1) { + that.select(index); + return; + } + } + + if (query.length < options.minChars) { that.hide(); } else { - that.getSuggestions(q); + that.getSuggestions(query); } }, + findSuggestionIndex: function (query) { + var that = this, + index = -1, + queryLowerCase = query.toLowerCase(); + + $.each(that.suggestions, function (i, suggestion) { + if (suggestion.value.toLowerCase() === queryLowerCase) { + index = i; + return false; + } + }); + + return index; + }, + getQuery: function (value) { var delimiter = this.options.delimiter, parts; if (!delimiter) { - return $.trim(value); + return value; } parts = value.split(delimiter); return $.trim(parts[parts.length - 1]); @@ -498,15 +524,25 @@ } var that = this, - formatResult = that.options.formatResult, + options = that.options, + formatResult = options.formatResult, value = that.getQuery(that.currentValue), className = that.classes.suggestion, classSelected = that.classes.selected, container = $(that.suggestionsContainer), - beforeRender = that.options.beforeRender, + beforeRender = options.beforeRender, html = '', + index, width; + if (options.triggerSelectOnValidInput) { + index = that.findSuggestionIndex(value); + if (index !== -1) { + that.select(index); + return; + } + } + // Build suggestions inner HTML: $.each(that.suggestions, function (i, suggestion) { html += '
' + formatResult(suggestion, value) + '
'; @@ -516,7 +552,7 @@ // because if instance was created before input had width, it will be zero. // Also it adjusts if input width has changed. // -2px to account for suggestions border. - if (that.options.width === 'auto') { + if (options.width === 'auto') { width = that.el.outerWidth() - 2; container.width(width > 0 ? width : 300); } @@ -524,7 +560,7 @@ container.html(html); // Select first value by default: - if (that.options.autoSelectFirst) { + if (options.autoSelectFirst) { that.selectedIndex = 0; container.children().first().addClass(classSelected); } @@ -598,11 +634,13 @@ } } - // Display suggestions only if returned query matches current value: - if (originalQuery === that.getQuery(that.currentValue)) { - that.suggestions = result.suggestions; - that.suggest(); + // Return if originalQuery is not matching current query: + if (originalQuery !== that.getQuery(that.currentValue)) { + return; } + + that.suggestions = result.suggestions; + that.suggest(); }, activate: function (index) {