Skip to content

Commit 9f329d0

Browse files
author
Tomas Kirda
committed
Add ability to select suggestion if it matches typed value. Fixes #113.
1 parent 6e7bbf3 commit 9f329d0

File tree

6 files changed

+159
-87
lines changed

6 files changed

+159
-87
lines changed

index.htm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ <h2>Dynamic Width</h2>
4242
<script type="text/javascript" src="scripts/jquery-1.8.2.min.js"></script>
4343
<script type="text/javascript" src="scripts/jquery.mockjax.js"></script>
4444
<script type="text/javascript" src="src/jquery.autocomplete.js"></script>
45+
<script type="text/javascript" src="scripts/countries.js"></script>
4546
<script type="text/javascript" src="scripts/demo.js"></script>
4647
</body>
4748
</html>

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ The standard jquery.autocomplete.js file is around 2.7KB when minified via Closu
3333
* `onSearchStart`: `function (query) {}` called before ajax request. `this` is bound to input element.
3434
* `onSearchComplete`: `function (query) {}` called after ajax response is processed. `this` is bound to input element.
3535
* `onSearchError`: `function (query, jqXHR, textStatus, errorThrown) {}` called if ajax request fails. `this` is bound to input element.
36+
* `onInvalidateSelection`: `function () {}` called when input is altered after selection has been made. `this` is bound to input element.
37+
* `triggerSelectOnValidInput`: Boolean value indicating if `select` should be triggered if it matches suggestion. Default `true`.
3638
* `beforeRender`: `function (container) {}` called before displaying the suggestions. You may manipulate suggestions DOM before it is displayed.
3739
* `tabDisabled`: Default `false`. Set to true to leave the cursor in the input field after the user tabs to select a suggestion.
3840
* `paramName`: Default `query`. The name of the request parameter that contains the query.

content/countries.txt renamed to scripts/countries.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
var countries = {
22
"AD": "Andorra",
33
"AE": "United Arab Emirates",
44
"AF": "Afghanistan",

scripts/demo.js

Lines changed: 56 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,68 @@
11
/*jslint browser: true, white: true, plusplus: true */
2-
/*global $: true */
2+
/*global $, countries */
33

44
$(function () {
55
'use strict';
66

7-
// Load countries then initialize plugin:
8-
$.ajax({
9-
url: 'content/countries.txt',
10-
dataType: 'json'
11-
}).done(function (source) {
7+
var countriesArray = $.map(countries, function (value, key) { return { value: value, data: key }; });
128

13-
var countriesArray = $.map(source, function (value, key) { return { value: value, data: key }; }),
14-
countries = $.map(source, function (value) { return value; });
9+
// Setup jQuery ajax mock:
10+
$.mockjax({
11+
url: '*',
12+
responseTime: 2000,
13+
response: function (settings) {
14+
var query = settings.data.query,
15+
queryLowerCase = query.toLowerCase(),
16+
re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'),
17+
suggestions = $.grep(countriesArray, function (country) {
18+
// return country.value.toLowerCase().indexOf(queryLowerCase) === 0;
19+
return re.test(country.value);
20+
}),
21+
response = {
22+
query: query,
23+
suggestions: suggestions
24+
};
1525

16-
// Setup jQuery ajax mock:
17-
$.mockjax({
18-
url: '*',
19-
responseTime: 2000,
20-
response: function (settings) {
21-
var query = settings.data.query,
22-
queryLowerCase = query.toLowerCase(),
23-
re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi'),
24-
suggestions = $.grep(countriesArray, function (country) {
25-
// return country.value.toLowerCase().indexOf(queryLowerCase) === 0;
26-
return re.test(country.value);
27-
}),
28-
response = {
29-
query: query,
30-
suggestions: suggestions
31-
};
32-
33-
this.responseText = JSON.stringify(response);
34-
}
35-
});
36-
37-
// Initialize ajax autocomplete:
38-
$('#autocomplete-ajax').autocomplete({
39-
// serviceUrl: '/autosuggest/service/url',
40-
lookup: countriesArray,
41-
lookupFilter: function(suggestion, originalQuery, queryLowerCase) {
42-
var re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi');
43-
return re.test(suggestion.value);
44-
},
45-
onSelect: function(suggestion) {
46-
$('#selction-ajax').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
47-
},
48-
onHint: function (hint) {
49-
$('#autocomplete-ajax-x').val(hint);
50-
},
51-
onInvalidateSelection: function() {
52-
$('#selction-ajax').html('You selected: none');
53-
}
54-
});
26+
this.responseText = JSON.stringify(response);
27+
}
28+
});
5529

56-
// Initialize autocomplete with local lookup:
57-
$('#autocomplete').autocomplete({
58-
lookup: countriesArray,
59-
minChars: 0,
60-
onSelect: function (suggestion) {
61-
$('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
62-
}
63-
});
64-
65-
// Initialize autocomplete with custom appendTo:
66-
$('#autocomplete-custom-append').autocomplete({
67-
lookup: countriesArray,
68-
appendTo: '#suggestions-container'
69-
});
30+
// Initialize ajax autocomplete:
31+
$('#autocomplete-ajax').autocomplete({
32+
// serviceUrl: '/autosuggest/service/url',
33+
lookup: countriesArray,
34+
lookupFilter: function(suggestion, originalQuery, queryLowerCase) {
35+
var re = new RegExp('\\b' + $.Autocomplete.utils.escapeRegExChars(queryLowerCase), 'gi');
36+
return re.test(suggestion.value);
37+
},
38+
onSelect: function(suggestion) {
39+
$('#selction-ajax').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
40+
},
41+
onHint: function (hint) {
42+
$('#autocomplete-ajax-x').val(hint);
43+
},
44+
onInvalidateSelection: function() {
45+
$('#selction-ajax').html('You selected: none');
46+
}
47+
});
7048

71-
// Initialize autocomplete with custom appendTo:
72-
$('#autocomplete-dynamic').autocomplete({
73-
lookup: countriesArray
74-
});
75-
49+
// Initialize autocomplete with local lookup:
50+
$('#autocomplete').autocomplete({
51+
lookup: countriesArray,
52+
minChars: 0,
53+
onSelect: function (suggestion) {
54+
$('#selection').html('You selected: ' + suggestion.value + ', ' + suggestion.data);
55+
}
56+
});
57+
58+
// Initialize autocomplete with custom appendTo:
59+
$('#autocomplete-custom-append').autocomplete({
60+
lookup: countriesArray,
61+
appendTo: '#suggestions-container'
7662
});
7763

64+
// Initialize autocomplete with custom appendTo:
65+
$('#autocomplete-dynamic').autocomplete({
66+
lookup: countriesArray
67+
});
7868
});

spec/autocompleteBehavior.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,15 @@ describe('Autocomplete', function () {
6666
context,
6767
value,
6868
data,
69-
autocomplete = new $.Autocomplete(input, {
69+
autocomplete = $(input).autocomplete({
7070
lookup: [{ value: 'A', data: 'B' }],
71+
triggerSelectOnValidInput: false,
7172
onSelect: function (suggestion) {
7273
context = this;
7374
value = suggestion.value;
7475
data = suggestion.data;
7576
}
76-
});
77+
}).autocomplete();
7778

7879
input.value = 'A';
7980
autocomplete.onValueChange();
@@ -260,7 +261,7 @@ describe('Autocomplete', function () {
260261
ajaxExecuted = true;
261262
var response = {
262263
query: null,
263-
suggestions: ['A', 'B', 'C']
264+
suggestions: ['Aa', 'Bb', 'Cc']
264265
};
265266
this.responseText = JSON.stringify(response);
266267
}
@@ -276,7 +277,7 @@ describe('Autocomplete', function () {
276277
runs(function () {
277278
expect(ajaxExecuted).toBe(true);
278279
expect(autocomplete.suggestions.length).toBe(3);
279-
expect(autocomplete.suggestions[0].value).toBe('A');
280+
expect(autocomplete.suggestions[0].value).toBe('Aa');
280281
});
281282
});
282283

@@ -496,4 +497,44 @@ describe('Autocomplete', function () {
496497
expect(context).toBe(element);
497498
expect(elementCount).toBe(1);
498499
});
500+
501+
it('Should trigger select when input value matches suggestion', function () {
502+
var input = $('<input />'),
503+
instance,
504+
suggestionData = false;
505+
506+
input.autocomplete({
507+
lookup: [{ value: 'Jamaica', data: 'J' }],
508+
triggerSelectOnValidInput: true,
509+
onSelect: function (suggestion) {
510+
suggestionData = suggestion.data;
511+
}
512+
});
513+
514+
input.val('Jamaica');
515+
instance = input.autocomplete();
516+
instance.onValueChange();
517+
518+
expect(suggestionData).toBe('J');
519+
});
520+
521+
it('Should NOT trigger select when input value matches suggestion', function () {
522+
var input = $('<input />'),
523+
instance,
524+
suggestionData = null;
525+
526+
input.autocomplete({
527+
lookup: [{ value: 'Jamaica', data: 'J' }],
528+
triggerSelectOnValidInput: false,
529+
onSelect: function (suggestion) {
530+
suggestionData = suggestion.data;
531+
}
532+
});
533+
534+
input.val('Jamaica');
535+
instance = input.autocomplete();
536+
instance.onValueChange();
537+
538+
expect(suggestionData).toBeNull();
539+
});
499540
});

src/jquery.autocomplete.js

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
tabDisabled: false,
7676
dataType: 'text',
7777
currentRequest: null,
78+
triggerSelectOnValidInput: true,
7879
lookupFilter: function (suggestion, originalQuery, queryLowerCase) {
7980
return suggestion.value.toLowerCase().indexOf(queryLowerCase) !== -1;
8081
},
@@ -390,32 +391,57 @@
390391

391392
onValueChange: function () {
392393
var that = this,
393-
q;
394+
options = that.options,
395+
value = that.el.val(),
396+
query = that.getQuery(value),
397+
index;
394398

395399
if (that.selection) {
396400
that.selection = null;
397-
(that.options.onInvalidateSelection || $.noop)();
401+
(options.onInvalidateSelection || $.noop).call(that.element);
398402
}
399403

400404
clearInterval(that.onChangeInterval);
401-
that.currentValue = that.el.val();
402-
403-
q = that.getQuery(that.currentValue);
405+
that.currentValue = value;
404406
that.selectedIndex = -1;
405407

406-
if (q.length < that.options.minChars) {
408+
// Check existing suggestion for the match before proceeding:
409+
if (options.triggerSelectOnValidInput) {
410+
index = that.findSuggestionIndex(query);
411+
if (index !== -1) {
412+
that.select(index);
413+
return;
414+
}
415+
}
416+
417+
if (query.length < options.minChars) {
407418
that.hide();
408419
} else {
409-
that.getSuggestions(q);
420+
that.getSuggestions(query);
410421
}
411422
},
412423

424+
findSuggestionIndex: function (query) {
425+
var that = this,
426+
index = -1,
427+
queryLowerCase = query.toLowerCase();
428+
429+
$.each(that.suggestions, function (i, suggestion) {
430+
if (suggestion.value.toLowerCase() === queryLowerCase) {
431+
index = i;
432+
return false;
433+
}
434+
});
435+
436+
return index;
437+
},
438+
413439
getQuery: function (value) {
414440
var delimiter = this.options.delimiter,
415441
parts;
416442

417443
if (!delimiter) {
418-
return $.trim(value);
444+
return value;
419445
}
420446
parts = value.split(delimiter);
421447
return $.trim(parts[parts.length - 1]);
@@ -498,15 +524,25 @@
498524
}
499525

500526
var that = this,
501-
formatResult = that.options.formatResult,
527+
options = that.options,
528+
formatResult = options.formatResult,
502529
value = that.getQuery(that.currentValue),
503530
className = that.classes.suggestion,
504531
classSelected = that.classes.selected,
505532
container = $(that.suggestionsContainer),
506-
beforeRender = that.options.beforeRender,
533+
beforeRender = options.beforeRender,
507534
html = '',
535+
index,
508536
width;
509537

538+
if (options.triggerSelectOnValidInput) {
539+
index = that.findSuggestionIndex(value);
540+
if (index !== -1) {
541+
that.select(index);
542+
return;
543+
}
544+
}
545+
510546
// Build suggestions inner HTML:
511547
$.each(that.suggestions, function (i, suggestion) {
512548
html += '<div class="' + className + '" data-index="' + i + '">' + formatResult(suggestion, value) + '</div>';
@@ -516,15 +552,15 @@
516552
// because if instance was created before input had width, it will be zero.
517553
// Also it adjusts if input width has changed.
518554
// -2px to account for suggestions border.
519-
if (that.options.width === 'auto') {
555+
if (options.width === 'auto') {
520556
width = that.el.outerWidth() - 2;
521557
container.width(width > 0 ? width : 300);
522558
}
523559

524560
container.html(html);
525561

526562
// Select first value by default:
527-
if (that.options.autoSelectFirst) {
563+
if (options.autoSelectFirst) {
528564
that.selectedIndex = 0;
529565
container.children().first().addClass(classSelected);
530566
}
@@ -598,11 +634,13 @@
598634
}
599635
}
600636

601-
// Display suggestions only if returned query matches current value:
602-
if (originalQuery === that.getQuery(that.currentValue)) {
603-
that.suggestions = result.suggestions;
604-
that.suggest();
637+
// Return if originalQuery is not matching current query:
638+
if (originalQuery !== that.getQuery(that.currentValue)) {
639+
return;
605640
}
641+
642+
that.suggestions = result.suggestions;
643+
that.suggest();
606644
},
607645

608646
activate: function (index) {

0 commit comments

Comments
 (0)