Skip to content

Commit 8b63dd9

Browse files
committed
Add support for customizing max number of entries in EmailsField,
update to version 1.6 of jquery.tokeninput.js and fix problem with reloading EmailsField pages by adding a data-pre attribute instead of hacking the value attribute - Legacy-Id: 8264
1 parent 3f1951d commit 8b63dd9

3 files changed

Lines changed: 116 additions & 44 deletions

File tree

ietf/person/forms.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,25 @@ def __init__(self, *args, **kwargs):
2424
kwargs["max_length"] = 1000
2525
if not "help_text" in kwargs:
2626
kwargs["help_text"] = "Type in name to search for person"
27+
max_entries = kwargs.pop("max_entries", None)
2728
super(EmailsField, self).__init__(*args, **kwargs)
2829
self.widget.attrs["class"] = "tokenized-field"
2930
self.widget.attrs["data-ajax-url"] = lazy(urlreverse, str)("ajax_search_emails") # make this lazy to prevent initialization problem
31+
if max_entries != None:
32+
self.widget.attrs["data-max-entries"] = max_entries
3033

3134
def parse_tokenized_value(self, value):
3235
return Email.objects.filter(address__in=[x.strip() for x in value.split(",") if x.strip()]).select_related("person")
3336

3437
def prepare_value(self, value):
3538
if not value:
36-
return ""
37-
if isinstance(value, str) or isinstance(value, unicode):
39+
value = ""
40+
if isinstance(value, basestring):
3841
value = self.parse_tokenized_value(value)
39-
return json_emails(value)
42+
43+
self.widget.attrs["data-pre"] = json_emails(value)
44+
45+
return ",".join(e.address for e in value)
4046

4147
def clean(self, value):
4248
value = super(EmailsField, self).clean(value)

static/js/lib/jquery.tokeninput.js

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* jQuery Plugin: Tokenizing Autocomplete Text Entry
3-
* Version 1.5.0
3+
* Version 1.6.0
44
*
55
* Copyright (c) 2009 James Smith (http://loopj.com)
66
* Licensed jointly under the GPL and MIT licenses,
@@ -11,26 +11,46 @@
1111
(function ($) {
1212
// Default settings
1313
var DEFAULT_SETTINGS = {
14+
// Search settings
15+
method: "GET",
16+
contentType: "json",
17+
queryParam: "q",
18+
searchDelay: 300,
19+
minChars: 1,
20+
propertyToSearch: "name",
21+
jsonContainer: null,
22+
23+
// Display settings
1424
hintText: "Type in a search term",
1525
noResultsText: "No results",
1626
searchingText: "Searching...",
1727
deleteText: "×",
18-
searchDelay: 300,
19-
minChars: 1,
28+
animateDropdown: true,
29+
30+
// Tokenization settings
2031
tokenLimit: null,
21-
jsonContainer: null,
22-
method: "GET",
23-
contentType: "json",
24-
queryParam: "q",
2532
tokenDelimiter: ",",
2633
preventDuplicates: false,
34+
35+
// Output settings
36+
tokenValue: "id",
37+
38+
// Prepopulation settings
2739
prePopulate: null,
2840
processPrePopulate: false,
29-
animateDropdown: true,
41+
42+
// Manipulation settings
43+
idPrefix: "token-input-",
44+
45+
// Formatters
46+
resultsFormatter: function(item){ return "<li>" + item[this.propertyToSearch]+ "</li>" },
47+
tokenFormatter: function(item) { return "<li><p>" + item[this.propertyToSearch] + "</p></li>" },
48+
49+
// Callbacks
3050
onResult: null,
3151
onAdd: null,
3252
onDelete: null,
33-
idPrefix: "token-input-"
53+
onReady: null
3454
};
3555

3656
// Default classes to use when theming
@@ -93,7 +113,10 @@ var methods = {
93113
remove: function(item) {
94114
this.data("tokenInputObject").remove(item);
95115
return this;
96-
}
116+
},
117+
get: function() {
118+
return this.data("tokenInputObject").getTokens();
119+
}
97120
}
98121

99122
// Expose the .tokenInput function to jQuery as a plugin
@@ -113,16 +136,19 @@ $.TokenList = function (input, url_or_data, settings) {
113136
//
114137

115138
// Configure the data source
116-
if(typeof(url_or_data) === "string") {
139+
if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") {
117140
// Set the url to query against
118141
settings.url = url_or_data;
119142

143+
// If the URL is a function, evaluate it here to do our initalization work
144+
var url = computeURL();
145+
120146
// Make a smart guess about cross-domain if it wasn't explicitly specified
121147
if(settings.crossDomain === undefined) {
122-
if(settings.url.indexOf("://") === -1) {
148+
if(url.indexOf("://") === -1) {
123149
settings.crossDomain = false;
124150
} else {
125-
settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]);
151+
settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]);
126152
}
127153
}
128154
} else if(typeof(url_or_data) === "object") {
@@ -223,6 +249,7 @@ $.TokenList = function (input, url_or_data, settings) {
223249
if(!$(this).val().length) {
224250
if(selected_token) {
225251
delete_token($(selected_token));
252+
hidden_input.change();
226253
} else if(previous_token.length) {
227254
select_token($(previous_token.get(0)));
228255
}
@@ -242,6 +269,7 @@ $.TokenList = function (input, url_or_data, settings) {
242269
case KEY.COMMA:
243270
if(selected_dropdown_item) {
244271
add_token($(selected_dropdown_item).data("tokeninput"));
272+
hidden_input.change();
245273
return false;
246274
}
247275
break;
@@ -346,6 +374,10 @@ $.TokenList = function (input, url_or_data, settings) {
346374
});
347375
}
348376

377+
// Initialization is done
378+
if($.isFunction(settings.onReady)) {
379+
settings.onReady.call();
380+
}
349381

350382
//
351383
// Public functions
@@ -380,6 +412,10 @@ $.TokenList = function (input, url_or_data, settings) {
380412
}
381413
});
382414
}
415+
416+
this.getTokens = function() {
417+
return saved_tokens;
418+
}
383419

384420
//
385421
// Private functions
@@ -390,8 +426,6 @@ $.TokenList = function (input, url_or_data, settings) {
390426
input_box.hide();
391427
hide_dropdown();
392428
return;
393-
} else {
394-
input_box.focus();
395429
}
396430
}
397431

@@ -413,7 +447,8 @@ $.TokenList = function (input, url_or_data, settings) {
413447

414448
// Inner function to a token to the list
415449
function insert_token(item) {
416-
var this_token = $("<li><p>"+ item.name +"</p></li>")
450+
var this_token = settings.tokenFormatter(item);
451+
this_token = $(this_token)
417452
.addClass(settings.classes.token)
418453
.insertBefore(input_token);
419454

@@ -423,25 +458,30 @@ $.TokenList = function (input, url_or_data, settings) {
423458
.appendTo(this_token)
424459
.click(function () {
425460
delete_token($(this).parent());
461+
hidden_input.change();
426462
return false;
427463
});
428464

429465
// Store data on the token
430-
var token_data = {"id": item.id, "name": item.name};
466+
var token_data = {"id": item.id};
467+
token_data[settings.propertyToSearch] = item[settings.propertyToSearch];
431468
$.data(this_token.get(0), "tokeninput", item);
432469

433470
// Save this token for duplicate checking
434471
saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
435472
selected_token_index++;
436473

437474
// Update the hidden input
438-
var token_ids = $.map(saved_tokens, function (el) {
439-
return el.id;
440-
});
441-
hidden_input.val(token_ids.join(settings.tokenDelimiter));
475+
update_hidden_input(saved_tokens, hidden_input);
442476

443477
token_count += 1;
444478

479+
// Check the token limit
480+
if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
481+
input_box.hide();
482+
hide_dropdown();
483+
}
484+
445485
return this_token;
446486
}
447487

@@ -470,8 +510,10 @@ $.TokenList = function (input, url_or_data, settings) {
470510
}
471511

472512
// Insert the new tokens
473-
insert_token(item);
474-
checkTokenLimit();
513+
if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
514+
insert_token(item);
515+
checkTokenLimit();
516+
}
475517

476518
// Clear input box
477519
input_box.val("");
@@ -553,10 +595,7 @@ $.TokenList = function (input, url_or_data, settings) {
553595
if(index < selected_token_index) selected_token_index--;
554596

555597
// Update the hidden input
556-
var token_ids = $.map(saved_tokens, function (el) {
557-
return el.id;
558-
});
559-
hidden_input.val(token_ids.join(settings.tokenDelimiter));
598+
update_hidden_input(saved_tokens, hidden_input);
560599

561600
token_count -= 1;
562601

@@ -573,6 +612,15 @@ $.TokenList = function (input, url_or_data, settings) {
573612
}
574613
}
575614

615+
// Update the hidden input box value
616+
function update_hidden_input(saved_tokens, hidden_input) {
617+
var token_values = $.map(saved_tokens, function (el) {
618+
return el[settings.tokenValue];
619+
});
620+
hidden_input.val(token_values.join(settings.tokenDelimiter));
621+
622+
}
623+
576624
// Hide and clear the results dropdown
577625
function hide_dropdown () {
578626
dropdown.hide().empty();
@@ -608,6 +656,10 @@ $.TokenList = function (input, url_or_data, settings) {
608656
function highlight_term(value, term) {
609657
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
610658
}
659+
660+
function find_value_and_highlight_term(template, value, term) {
661+
return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
662+
}
611663

612664
// Populate the results dropdown with some results
613665
function populate_dropdown (query, results) {
@@ -620,14 +672,18 @@ $.TokenList = function (input, url_or_data, settings) {
620672
})
621673
.mousedown(function (event) {
622674
add_token($(event.target).closest("li").data("tokeninput"));
675+
hidden_input.change();
623676
return false;
624677
})
625678
.hide();
626679

627680
$.each(results, function(index, value) {
628-
var this_li = $("<li>" + highlight_term(value.name, query) + "</li>")
629-
.appendTo(dropdown_ul);
630-
681+
var this_li = settings.resultsFormatter(value);
682+
683+
this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query);
684+
685+
this_li = $(this_li).appendTo(dropdown_ul);
686+
631687
if(index % 2) {
632688
this_li.addClass(settings.classes.dropdownItem);
633689
} else {
@@ -699,17 +755,19 @@ $.TokenList = function (input, url_or_data, settings) {
699755

700756
// Do the actual search
701757
function run_search(query) {
702-
var cached_results = cache.get(query);
758+
var cache_key = query + computeURL();
759+
var cached_results = cache.get(cache_key);
703760
if(cached_results) {
704761
populate_dropdown(query, cached_results);
705762
} else {
706763
// Are we doing an ajax search or local data search?
707764
if(settings.url) {
765+
var url = computeURL();
708766
// Extract exisiting get params
709767
var ajax_params = {};
710768
ajax_params.data = {};
711-
if(settings.url.indexOf("?") > -1) {
712-
var parts = settings.url.split("?");
769+
if(url.indexOf("?") > -1) {
770+
var parts = url.split("?");
713771
ajax_params.url = parts[0];
714772

715773
var param_array = parts[1].split("&");
@@ -718,7 +776,7 @@ $.TokenList = function (input, url_or_data, settings) {
718776
ajax_params.data[kv[0]] = kv[1];
719777
});
720778
} else {
721-
ajax_params.url = settings.url;
779+
ajax_params.url = url;
722780
}
723781

724782
// Prepare the request
@@ -734,7 +792,7 @@ $.TokenList = function (input, url_or_data, settings) {
734792
if($.isFunction(settings.onResult)) {
735793
results = settings.onResult.call(hidden_input, results);
736794
}
737-
cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
795+
cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results);
738796

739797
// only populate the dropdown if the results are associated with the active search query
740798
if(input_box.val().toLowerCase() === query) {
@@ -747,17 +805,26 @@ $.TokenList = function (input, url_or_data, settings) {
747805
} else if(settings.local_data) {
748806
// Do the search through local data
749807
var results = $.grep(settings.local_data, function (row) {
750-
return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1;
808+
return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
751809
});
752810

753811
if($.isFunction(settings.onResult)) {
754812
results = settings.onResult.call(hidden_input, results);
755813
}
756-
cache.add(query, results);
814+
cache.add(cache_key, results);
757815
populate_dropdown(query, results);
758816
}
759817
}
760818
}
819+
820+
// compute the dynamic URL
821+
function computeURL() {
822+
var url = settings.url;
823+
if(typeof settings.url == 'function') {
824+
url = settings.url.call();
825+
}
826+
return url;
827+
}
761828
};
762829

763830
// Really basic cache for the results

static/js/tokenized-field.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@ function setupTokenizedField(field) {
33
return; // don't tokenize hidden template snippets
44

55
var pre = [];
6-
if (field.val())
7-
pre = JSON.parse((field.val() || "").replace(/&quot;/g, '"'));
8-
else if (field.data("pre"))
6+
if (field.attr("data-pre"))
97
pre = JSON.parse((field.attr("data-pre") || "").replace(/&quot;/g, '"'));
108

119
field.tokenInput(field.attr("data-ajax-url"), {
1210
hintText: "",
1311
preventDuplicates: true,
14-
prePopulate: pre
12+
prePopulate: pre,
13+
tokenLimit: field.attr("data-max-entries")
1514
});
1615
}
1716

0 commit comments

Comments
 (0)