Skip to content

Commit 289226c

Browse files
committed
add a tokenizer so choices can be automatically created and selected while user is typing or pasting into the field. fixes select2#101 select2#81 select2#292
1 parent c2fa045 commit 289226c

1 file changed

Lines changed: 85 additions & 9 deletions

File tree

select2.js

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,59 @@
416416
return count;
417417
}
418418

419+
/**
420+
* Default tokenizer. This function uses breaks the input on substring match of any string from the
421+
* opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
422+
* two options have to be defined in order for the tokenizer to work.
423+
*
424+
* @param input text user has typed so far or pasted into the search field
425+
* @param selection currently selected choices
426+
* @param selectCallback function(choice) callback tho add the choice to selection
427+
* @param opts select2's opts
428+
* @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
429+
*/
430+
function defaultTokenizer(input, selection, selectCallback, opts) {
431+
var original = input, // store the original so we can compare and know if we need to tell the search to update its text
432+
dupe = false, // check for whether a token we extracted represents a duplicate selected choice
433+
token, // token
434+
index, // position at which the separator was found
435+
i, l, // looping variables
436+
separator; // the matched separator
437+
438+
if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
439+
440+
while (true) {
441+
index = -1;
442+
443+
for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
444+
separator = opts.tokenSeparators[i];
445+
index = input.indexOf(separator);
446+
if (index >= 0) break;
447+
}
448+
449+
if (index < 0) break; // did not find any token separator in the input string, bail
450+
451+
token = input.substring(0, index);
452+
input = input.substring(index + separator.length);
453+
454+
if (token.length > 0) {
455+
token = opts.createSearchChoice(token, selection);
456+
if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
457+
dupe = false;
458+
for (i = 0, l = selection.length; i < l; i++) {
459+
if (equal(opts.id(token), opts.id(selection[i]))) {
460+
dupe = true; break;
461+
}
462+
}
463+
464+
if (!dupe) selectCallback(token);
465+
}
466+
}
467+
}
468+
469+
if (original.localeCompare(input) != 0) return input;
470+
}
471+
419472
/**
420473
* blurs any Select2 container that has focus when an element outside them was clicked or received focus
421474
*
@@ -599,9 +652,6 @@
599652
this.select = select = opts.element;
600653
}
601654

602-
//Custom tags separator.
603-
opts.separator = opts.separator || ",";
604-
605655
if (select) {
606656
// these options are not allowed when attached to a select because they are picked up off the element itself
607657
$.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
@@ -1074,12 +1124,19 @@
10741124
}
10751125
},
10761126

1127+
/**
1128+
* Default tokenizer function which does nothing
1129+
*/
1130+
tokenize: function() {
1131+
1132+
},
1133+
10771134
/**
10781135
* @param initial whether or not this is the call to this method right after the dropdown has been opened
10791136
*/
10801137
// abstract
10811138
updateResults: function (initial) {
1082-
var search = this.search, results = this.results, opts = this.opts, data, self=this;
1139+
var search = this.search, results = this.results, opts = this.opts, data, self=this, input;
10831140

10841141
// if the search is currently hidden we do not alter the results
10851142
if (initial !== true && (this.showSearchInput === false || !this.opened())) {
@@ -1115,6 +1172,12 @@
11151172
render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
11161173
}
11171174

1175+
// give the tokenizer a chance to pre-process the input
1176+
input = this.tokenize();
1177+
if (input != undefined && input != null) {
1178+
search.val(input);
1179+
}
1180+
11181181
this.resultsPage = 1;
11191182
opts.query({
11201183
term: search.val(),
@@ -1889,6 +1952,18 @@
18891952
self.postprocessResults();
18901953
},
18911954

1955+
tokenize: function() {
1956+
var input = this.search.val();
1957+
input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
1958+
if (input != null && input != undefined) {
1959+
this.search.val(input);
1960+
if (input.length > 0) {
1961+
this.open();
1962+
}
1963+
}
1964+
1965+
},
1966+
18921967
// multi
18931968
onSelect: function (data) {
18941969
this.addSelectedChoice(data);
@@ -1898,10 +1973,9 @@
18981973
this.close();
18991974
this.search.width(10);
19001975
} else {
1901-
this.search.width(10);
1902-
this.resizeSearch();
1903-
19041976
if (this.countSelectableResults()>0) {
1977+
this.search.width(10);
1978+
this.resizeSearch();
19051979
this.positionDropdown();
19061980
} else {
19071981
// if nothing left to select close
@@ -2032,7 +2106,6 @@
20322106
containerLeft = this.selection.offset().left;
20332107

20342108
searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
2035-
20362109
if (searchWidth < minimumWidth) {
20372110
searchWidth = maxWidth - sideBorderPadding;
20382111
}
@@ -2232,7 +2305,10 @@
22322305
id: function (e) { return e.id; },
22332306
matcher: function(term, text) {
22342307
return text.toUpperCase().indexOf(term.toUpperCase()) >= 0;
2235-
}
2308+
},
2309+
separator: ",",
2310+
tokenSeparators: [],
2311+
tokenizer: defaultTokenizer
22362312
};
22372313

22382314
// exports

0 commit comments

Comments
 (0)