From 8b63dd982f137e31a3fa130d41a11f53b13df1d3 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 13 Aug 2014 08:58:10 +0000 Subject: [PATCH] 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 --- ietf/person/forms.py | 12 ++- static/js/lib/jquery.tokeninput.js | 141 +++++++++++++++++++++-------- static/js/tokenized-field.js | 7 +- 3 files changed, 116 insertions(+), 44 deletions(-) diff --git a/ietf/person/forms.py b/ietf/person/forms.py index f4a1c9d3e..c10092cdb 100644 --- a/ietf/person/forms.py +++ b/ietf/person/forms.py @@ -24,19 +24,25 @@ class EmailsField(forms.CharField): kwargs["max_length"] = 1000 if not "help_text" in kwargs: kwargs["help_text"] = "Type in name to search for person" + max_entries = kwargs.pop("max_entries", None) super(EmailsField, self).__init__(*args, **kwargs) self.widget.attrs["class"] = "tokenized-field" self.widget.attrs["data-ajax-url"] = lazy(urlreverse, str)("ajax_search_emails") # make this lazy to prevent initialization problem + if max_entries != None: + self.widget.attrs["data-max-entries"] = max_entries def parse_tokenized_value(self, value): return Email.objects.filter(address__in=[x.strip() for x in value.split(",") if x.strip()]).select_related("person") def prepare_value(self, value): if not value: - return "" - if isinstance(value, str) or isinstance(value, unicode): + value = "" + if isinstance(value, basestring): value = self.parse_tokenized_value(value) - return json_emails(value) + + self.widget.attrs["data-pre"] = json_emails(value) + + return ",".join(e.address for e in value) def clean(self, value): value = super(EmailsField, self).clean(value) diff --git a/static/js/lib/jquery.tokeninput.js b/static/js/lib/jquery.tokeninput.js index 4d01f392f..87641a57a 100644 --- a/static/js/lib/jquery.tokeninput.js +++ b/static/js/lib/jquery.tokeninput.js @@ -1,6 +1,6 @@ /* * jQuery Plugin: Tokenizing Autocomplete Text Entry - * Version 1.5.0 + * Version 1.6.0 * * Copyright (c) 2009 James Smith (http://loopj.com) * Licensed jointly under the GPL and MIT licenses, @@ -11,26 +11,46 @@ (function ($) { // Default settings var DEFAULT_SETTINGS = { + // Search settings + method: "GET", + contentType: "json", + queryParam: "q", + searchDelay: 300, + minChars: 1, + propertyToSearch: "name", + jsonContainer: null, + + // Display settings hintText: "Type in a search term", noResultsText: "No results", searchingText: "Searching...", deleteText: "×", - searchDelay: 300, - minChars: 1, + animateDropdown: true, + + // Tokenization settings tokenLimit: null, - jsonContainer: null, - method: "GET", - contentType: "json", - queryParam: "q", tokenDelimiter: ",", preventDuplicates: false, + + // Output settings + tokenValue: "id", + + // Prepopulation settings prePopulate: null, processPrePopulate: false, - animateDropdown: true, + + // Manipulation settings + idPrefix: "token-input-", + + // Formatters + resultsFormatter: function(item){ return "
  • " + item[this.propertyToSearch]+ "
  • " }, + tokenFormatter: function(item) { return "
  • " + item[this.propertyToSearch] + "

  • " }, + + // Callbacks onResult: null, onAdd: null, onDelete: null, - idPrefix: "token-input-" + onReady: null }; // Default classes to use when theming @@ -93,7 +113,10 @@ var methods = { remove: function(item) { this.data("tokenInputObject").remove(item); return this; - } + }, + get: function() { + return this.data("tokenInputObject").getTokens(); + } } // Expose the .tokenInput function to jQuery as a plugin @@ -113,16 +136,19 @@ $.TokenList = function (input, url_or_data, settings) { // // Configure the data source - if(typeof(url_or_data) === "string") { + if($.type(url_or_data) === "string" || $.type(url_or_data) === "function") { // Set the url to query against settings.url = url_or_data; + // If the URL is a function, evaluate it here to do our initalization work + var url = computeURL(); + // Make a smart guess about cross-domain if it wasn't explicitly specified if(settings.crossDomain === undefined) { - if(settings.url.indexOf("://") === -1) { + if(url.indexOf("://") === -1) { settings.crossDomain = false; } else { - settings.crossDomain = (location.href.split(/\/+/g)[1] !== settings.url.split(/\/+/g)[1]); + settings.crossDomain = (location.href.split(/\/+/g)[1] !== url.split(/\/+/g)[1]); } } } else if(typeof(url_or_data) === "object") { @@ -223,6 +249,7 @@ $.TokenList = function (input, url_or_data, settings) { if(!$(this).val().length) { if(selected_token) { delete_token($(selected_token)); + hidden_input.change(); } else if(previous_token.length) { select_token($(previous_token.get(0))); } @@ -242,6 +269,7 @@ $.TokenList = function (input, url_or_data, settings) { case KEY.COMMA: if(selected_dropdown_item) { add_token($(selected_dropdown_item).data("tokeninput")); + hidden_input.change(); return false; } break; @@ -346,6 +374,10 @@ $.TokenList = function (input, url_or_data, settings) { }); } + // Initialization is done + if($.isFunction(settings.onReady)) { + settings.onReady.call(); + } // // Public functions @@ -380,6 +412,10 @@ $.TokenList = function (input, url_or_data, settings) { } }); } + + this.getTokens = function() { + return saved_tokens; + } // // Private functions @@ -390,8 +426,6 @@ $.TokenList = function (input, url_or_data, settings) { input_box.hide(); hide_dropdown(); return; - } else { - input_box.focus(); } } @@ -413,7 +447,8 @@ $.TokenList = function (input, url_or_data, settings) { // Inner function to a token to the list function insert_token(item) { - var this_token = $("
  • "+ item.name +"

  • ") + var this_token = settings.tokenFormatter(item); + this_token = $(this_token) .addClass(settings.classes.token) .insertBefore(input_token); @@ -423,11 +458,13 @@ $.TokenList = function (input, url_or_data, settings) { .appendTo(this_token) .click(function () { delete_token($(this).parent()); + hidden_input.change(); return false; }); // Store data on the token - var token_data = {"id": item.id, "name": item.name}; + var token_data = {"id": item.id}; + token_data[settings.propertyToSearch] = item[settings.propertyToSearch]; $.data(this_token.get(0), "tokeninput", item); // Save this token for duplicate checking @@ -435,13 +472,16 @@ $.TokenList = function (input, url_or_data, settings) { selected_token_index++; // Update the hidden input - var token_ids = $.map(saved_tokens, function (el) { - return el.id; - }); - hidden_input.val(token_ids.join(settings.tokenDelimiter)); + update_hidden_input(saved_tokens, hidden_input); token_count += 1; + // Check the token limit + if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) { + input_box.hide(); + hide_dropdown(); + } + return this_token; } @@ -470,8 +510,10 @@ $.TokenList = function (input, url_or_data, settings) { } // Insert the new tokens - insert_token(item); - checkTokenLimit(); + if(settings.tokenLimit == null || token_count < settings.tokenLimit) { + insert_token(item); + checkTokenLimit(); + } // Clear input box input_box.val(""); @@ -553,10 +595,7 @@ $.TokenList = function (input, url_or_data, settings) { if(index < selected_token_index) selected_token_index--; // Update the hidden input - var token_ids = $.map(saved_tokens, function (el) { - return el.id; - }); - hidden_input.val(token_ids.join(settings.tokenDelimiter)); + update_hidden_input(saved_tokens, hidden_input); token_count -= 1; @@ -573,6 +612,15 @@ $.TokenList = function (input, url_or_data, settings) { } } + // Update the hidden input box value + function update_hidden_input(saved_tokens, hidden_input) { + var token_values = $.map(saved_tokens, function (el) { + return el[settings.tokenValue]; + }); + hidden_input.val(token_values.join(settings.tokenDelimiter)); + + } + // Hide and clear the results dropdown function hide_dropdown () { dropdown.hide().empty(); @@ -608,6 +656,10 @@ $.TokenList = function (input, url_or_data, settings) { function highlight_term(value, term) { return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); } + + function find_value_and_highlight_term(template, value, term) { + return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + value + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term)); + } // Populate the results dropdown with some results function populate_dropdown (query, results) { @@ -620,14 +672,18 @@ $.TokenList = function (input, url_or_data, settings) { }) .mousedown(function (event) { add_token($(event.target).closest("li").data("tokeninput")); + hidden_input.change(); return false; }) .hide(); $.each(results, function(index, value) { - var this_li = $("
  • " + highlight_term(value.name, query) + "
  • ") - .appendTo(dropdown_ul); - + var this_li = settings.resultsFormatter(value); + + this_li = find_value_and_highlight_term(this_li ,value[settings.propertyToSearch], query); + + this_li = $(this_li).appendTo(dropdown_ul); + if(index % 2) { this_li.addClass(settings.classes.dropdownItem); } else { @@ -699,17 +755,19 @@ $.TokenList = function (input, url_or_data, settings) { // Do the actual search function run_search(query) { - var cached_results = cache.get(query); + var cache_key = query + computeURL(); + var cached_results = cache.get(cache_key); if(cached_results) { populate_dropdown(query, cached_results); } else { // Are we doing an ajax search or local data search? if(settings.url) { + var url = computeURL(); // Extract exisiting get params var ajax_params = {}; ajax_params.data = {}; - if(settings.url.indexOf("?") > -1) { - var parts = settings.url.split("?"); + if(url.indexOf("?") > -1) { + var parts = url.split("?"); ajax_params.url = parts[0]; var param_array = parts[1].split("&"); @@ -718,7 +776,7 @@ $.TokenList = function (input, url_or_data, settings) { ajax_params.data[kv[0]] = kv[1]; }); } else { - ajax_params.url = settings.url; + ajax_params.url = url; } // Prepare the request @@ -734,7 +792,7 @@ $.TokenList = function (input, url_or_data, settings) { if($.isFunction(settings.onResult)) { results = settings.onResult.call(hidden_input, results); } - cache.add(query, settings.jsonContainer ? results[settings.jsonContainer] : results); + cache.add(cache_key, settings.jsonContainer ? results[settings.jsonContainer] : results); // only populate the dropdown if the results are associated with the active search query if(input_box.val().toLowerCase() === query) { @@ -747,17 +805,26 @@ $.TokenList = function (input, url_or_data, settings) { } else if(settings.local_data) { // Do the search through local data var results = $.grep(settings.local_data, function (row) { - return row.name.toLowerCase().indexOf(query.toLowerCase()) > -1; + return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1; }); if($.isFunction(settings.onResult)) { results = settings.onResult.call(hidden_input, results); } - cache.add(query, results); + cache.add(cache_key, results); populate_dropdown(query, results); } } } + + // compute the dynamic URL + function computeURL() { + var url = settings.url; + if(typeof settings.url == 'function') { + url = settings.url.call(); + } + return url; + } }; // Really basic cache for the results diff --git a/static/js/tokenized-field.js b/static/js/tokenized-field.js index b2040ecc5..5e3802e62 100644 --- a/static/js/tokenized-field.js +++ b/static/js/tokenized-field.js @@ -3,15 +3,14 @@ function setupTokenizedField(field) { return; // don't tokenize hidden template snippets var pre = []; - if (field.val()) - pre = JSON.parse((field.val() || "").replace(/"/g, '"')); - else if (field.data("pre")) + if (field.attr("data-pre")) pre = JSON.parse((field.attr("data-pre") || "").replace(/"/g, '"')); field.tokenInput(field.attr("data-ajax-url"), { hintText: "", preventDuplicates: true, - prePopulate: pre + prePopulate: pre, + tokenLimit: field.attr("data-max-entries") }); }