")
- .addClass(settings.classes.dropdown)
- .appendTo("body")
- .hide();
-
- // Magic element to help us resize the text input
- var input_resizer = $("
")
- .insertAfter(input_box)
- .css({
- position: "absolute",
- top: -9999,
- left: -9999,
- width: "auto",
- fontSize: input_box.css("fontSize"),
- fontFamily: input_box.css("fontFamily"),
- fontWeight: input_box.css("fontWeight"),
- letterSpacing: input_box.css("letterSpacing"),
- whiteSpace: "nowrap"
- });
-
- // Pre-populate list if items exist
- hidden_input.val("");
- var li_data = settings.prePopulate || hidden_input.data("pre");
- if(settings.processPrePopulate && $.isFunction(settings.onResult)) {
- li_data = settings.onResult.call(hidden_input, li_data);
- }
- if(li_data && li_data.length) {
- $.each(li_data, function (index, value) {
- insert_token(value);
- checkTokenLimit();
- });
- }
-
- // Check if widget should initialize as disabled
- if (settings.disabled) {
- toggleDisabled(true);
- }
-
- // Initialization is done
- if($.isFunction(settings.onReady)) {
- settings.onReady.call();
- }
-
- //
- // Public functions
- //
-
- this.clear = function() {
- token_list.children("li").each(function() {
- if ($(this).children("input").length === 0) {
- delete_token($(this));
- }
- });
- }
-
- this.add = function(item) {
- add_token(item);
- }
-
- this.remove = function(item) {
- token_list.children("li").each(function() {
- if ($(this).children("input").length === 0) {
- var currToken = $(this).data("tokeninput");
- var match = true;
- for (var prop in item) {
- if (item[prop] !== currToken[prop]) {
- match = false;
- break;
+ // Keep reference for placeholder
+ if (settings.placeholder) {
+ input_box.attr("placeholder", settings.placeholder);
+ }
+
+ // Keep a reference to the original input box
+ var hiddenInput = $(input)
+ .hide()
+ .val("")
+ .focus(function () {
+ focusWithTimeout(input_box);
+ })
+ .blur(function () {
+ input_box.blur();
+
+ //return the object to this can be referenced in the callback functions.
+ return hiddenInput;
+ })
+ ;
+
+ // Keep a reference to the selected token and dropdown item
+ var selected_token = null;
+ var selected_token_index = 0;
+ var selected_dropdown_item = null;
+
+ // The list to store the token items in
+ var token_list = $("
")
+ .addClass($(input).data("settings").classes.tokenList)
+ .click(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && li.get(0) && $.data(li.get(0), "tokeninput")) {
+ toggle_select_token(li);
+ } else {
+ // Deselect selected token
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
}
+
+ // Focus input box
+ focusWithTimeout(input_box);
}
- if (match) {
- delete_token($(this));
+ })
+ .mouseover(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && selected_token !== this) {
+ li.addClass($(input).data("settings").classes.highlightedToken);
}
- }
- });
- }
-
- this.getTokens = function() {
- return saved_tokens;
- }
-
- this.toggleDisabled = function(disable) {
- toggleDisabled(disable);
- }
-
- //
- // Private functions
- //
-
- // Toggles the widget between enabled and disabled state, or according
- // to the [disable] parameter.
- function toggleDisabled(disable) {
- if (typeof disable === 'boolean') {
- settings.disabled = disable
- } else {
- settings.disabled = !settings.disabled;
+ })
+ .mouseout(function (event) {
+ var li = $(event.target).closest("li");
+ if(li && selected_token !== this) {
+ li.removeClass($(input).data("settings").classes.highlightedToken);
+ }
+ })
+ .insertBefore(hiddenInput);
+
+ // The token holding the input box
+ var input_token = $("
")
+ .addClass($(input).data("settings").classes.inputToken)
+ .appendTo(token_list)
+ .append(input_box);
+
+ // The list to store the dropdown items in
+ var dropdown = $("
")
+ .addClass($(input).data("settings").classes.dropdown)
+ .appendTo("body")
+ .hide();
+
+ // Magic element to help us resize the text input
+ var input_resizer = $("
")
+ .insertAfter(input_box)
+ .css({
+ position: "absolute",
+ top: -9999,
+ left: -9999,
+ width: "auto",
+ fontSize: input_box.css("fontSize"),
+ fontFamily: input_box.css("fontFamily"),
+ fontWeight: input_box.css("fontWeight"),
+ letterSpacing: input_box.css("letterSpacing"),
+ whiteSpace: "nowrap"
+ });
+
+ // Pre-populate list if items exist
+ hiddenInput.val("");
+ var li_data = $(input).data("settings").prePopulate || hiddenInput.data("pre");
+
+ if ($(input).data("settings").processPrePopulate && $.isFunction($(input).data("settings").onResult)) {
+ li_data = $(input).data("settings").onResult.call(hiddenInput, li_data);
}
- input_box.prop('disabled', settings.disabled);
- token_list.toggleClass(settings.classes.disabled, settings.disabled);
- // if there is any token selected we deselect it
- if(selected_token) {
- deselect_token($(selected_token), POSITION.END);
+
+ if (li_data && li_data.length) {
+ $.each(li_data, function (index, value) {
+ insert_token(value);
+ checkTokenLimit();
+ input_box.attr("placeholder", null)
+ });
}
- hidden_input.prop('disabled', settings.disabled);
- }
-
- function checkTokenLimit() {
- if(settings.tokenLimit !== null && token_count >= settings.tokenLimit) {
- input_box.hide();
- hide_dropdown();
- return;
+
+ // Check if widget should initialize as disabled
+ if ($(input).data("settings").disabled) {
+ toggleDisabled(true);
}
- }
-
- function resize_input() {
- if(input_val === (input_val = input_box.val())) {return;}
-
- // Enter new content into resizer and resize input accordingly
- var escaped = input_val.replace(/&/g, '&').replace(/\s/g,' ').replace(//g, '>');
- input_resizer.html(escaped);
- input_box.width(input_resizer.width() + 30);
- }
-
- function is_printable_character(keycode) {
- return ((keycode >= 48 && keycode <= 90) || // 0-1a-z
- (keycode >= 96 && keycode <= 111) || // numpad 0-9 + - / * .
- (keycode >= 186 && keycode <= 192) || // ; = , - . / ^
- (keycode >= 219 && keycode <= 222)); // ( \ ) '
- }
-
- // Inner function to a token to the list
- function insert_token(item) {
- var this_token = settings.tokenFormatter(item);
- this_token = $(this_token)
- .addClass(settings.classes.token)
- .insertBefore(input_token);
-
- // The 'delete token' button
- $("
" + settings.deleteText + "")
- .addClass(settings.classes.tokenDelete)
- .appendTo(this_token)
- .on("click", function () {
- if (!settings.disabled) {
- delete_token($(this).parent());
- hidden_input.trigger('change');
- return false;
+
+ // Initialization is done
+ if (typeof($(input).data("settings").onReady) === "function") {
+ $(input).data("settings").onReady.call();
+ }
+
+ //
+ // Public functions
+ //
+
+ this.clear = function() {
+ token_list.children("li").each(function() {
+ if ($(this).children("input").length === 0) {
+ delete_token($(this));
}
});
-
- // Store data on the token
- var token_data = item;
- $.data(this_token.get(0), "tokeninput", item);
-
- // Save this token for duplicate checking
- saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
- selected_token_index++;
-
- // Update the hidden input
- 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;
- }
-
- // Add a token to the token list based on user input
- function add_token (item) {
- var callback = settings.onAdd;
-
- // See if the token already exists and select it if we don't want duplicates
- if(token_count > 0 && settings.preventDuplicates) {
- var found_existing_token = null;
- token_list.children().each(function () {
- var existing_token = $(this);
- var existing_data = $.data(existing_token.get(0), "tokeninput");
- if(existing_data && existing_data.id === item.id) {
- found_existing_token = existing_token;
- return false;
+ };
+
+ this.add = function(item) {
+ add_token(item);
+ };
+
+ this.remove = function(item) {
+ token_list.children("li").each(function() {
+ if ($(this).children("input").length === 0) {
+ var currToken = $(this).data("tokeninput");
+ var match = true;
+ for (var prop in item) {
+ if (item[prop] !== currToken[prop]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ delete_token($(this));
+ }
}
});
-
- if(found_existing_token) {
- select_token(found_existing_token);
- input_token.insertAfter(found_existing_token);
- input_box.focus();
+ };
+
+ this.getTokens = function() {
+ return saved_tokens;
+ };
+
+ this.toggleDisabled = function(disable) {
+ toggleDisabled(disable);
+ };
+
+ // Resize input to maximum width so the placeholder can be seen
+ resize_input();
+
+ //
+ // Private functions
+ //
+
+ function escapeHTML(text) {
+ return $(input).data("settings").enableHTML ? text : _escapeHTML(text);
+ }
+
+ // Toggles the widget between enabled and disabled state, or according
+ // to the [disable] parameter.
+ function toggleDisabled(disable) {
+ if (typeof disable === 'boolean') {
+ $(input).data("settings").disabled = disable
+ } else {
+ $(input).data("settings").disabled = !$(input).data("settings").disabled;
+ }
+ input_box.attr('disabled', $(input).data("settings").disabled);
+ token_list.toggleClass($(input).data("settings").classes.disabled, $(input).data("settings").disabled);
+ // if there is any token selected we deselect it
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+ hiddenInput.attr('disabled', $(input).data("settings").disabled);
+ }
+
+ function checkTokenLimit() {
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+ input_box.hide();
+ hide_dropdown();
return;
}
}
-
- // Insert the new tokens
- if(settings.tokenLimit == null || token_count < settings.tokenLimit) {
- insert_token(item);
- checkTokenLimit();
+
+ function resize_input() {
+ if(input_val === (input_val = input_box.val())) {return;}
+
+ // Get width left on the current line
+ var width_left = token_list.width() - input_box.offset().left - token_list.offset().left;
+ // Enter new content into resizer and resize input accordingly
+ input_resizer.html(_escapeHTML(input_val) || _escapeHTML(settings.placeholder));
+ // Get maximum width, minimum the size of input and maximum the widget's width
+ input_box.width(Math.min(token_list.width(),
+ Math.max(width_left, input_resizer.width() + 30)));
}
-
- // Clear input box
- input_box.val("");
-
- // Don't show the help dropdown, they've got the idea
- hide_dropdown();
-
- // Execute the onAdd callback if defined
- if($.isFunction(callback)) {
- callback.call(hidden_input,item);
+
+ function add_freetagging_tokens() {
+ var value = $.trim(input_box.val());
+ var tokens = value.split($(input).data("settings").tokenDelimiter);
+ $.each(tokens, function(i, token) {
+ if (!token) {
+ return;
+ }
+
+ if ($.isFunction($(input).data("settings").onFreeTaggingAdd)) {
+ token = $(input).data("settings").onFreeTaggingAdd.call(hiddenInput, token);
+ }
+ var object = {};
+ object[$(input).data("settings").tokenValue] = object[$(input).data("settings").propertyToSearch] = token;
+ add_token(object);
+ });
}
- }
-
- // Select a token in the token list
- function select_token (token) {
- if (!settings.disabled) {
- token.addClass(settings.classes.selectedToken);
- selected_token = token.get(0);
-
- // Hide input box
+
+ // Inner function to a token to the list
+ function insert_token(item) {
+ var $this_token = $($(input).data("settings").tokenFormatter(item));
+ var readonly = item.readonly === true;
+
+ if(readonly) $this_token.addClass($(input).data("settings").classes.tokenReadOnly);
+
+ $this_token.addClass($(input).data("settings").classes.token).insertBefore(input_token);
+
+ // The 'delete token' button
+ if(!readonly) {
+ $("
" + $(input).data("settings").deleteText + "")
+ .addClass($(input).data("settings").classes.tokenDelete)
+ .appendTo($this_token)
+ .click(function () {
+ if (!$(input).data("settings").disabled) {
+ delete_token($(this).parent());
+ hiddenInput.change();
+ return false;
+ }
+ });
+ }
+
+ // Store data on the token
+ var token_data = item;
+ $.data($this_token.get(0), "tokeninput", item);
+
+ // Save this token for duplicate checking
+ saved_tokens = saved_tokens.slice(0,selected_token_index).concat([token_data]).concat(saved_tokens.slice(selected_token_index));
+ selected_token_index++;
+
+ // Update the hidden input
+ update_hiddenInput(saved_tokens, hiddenInput);
+
+ token_count += 1;
+
+ // Check the token limit
+ if($(input).data("settings").tokenLimit !== null && token_count >= $(input).data("settings").tokenLimit) {
+ input_box.hide();
+ hide_dropdown();
+ }
+
+ return $this_token;
+ }
+
+ // Add a token to the token list based on user input
+ function add_token (item) {
+ var callback = $(input).data("settings").onAdd;
+
+ // See if the token already exists and select it if we don't want duplicates
+ if(token_count > 0 && $(input).data("settings").preventDuplicates) {
+ var found_existing_token = null;
+ token_list.children().each(function () {
+ var existing_token = $(this);
+ var existing_data = $.data(existing_token.get(0), "tokeninput");
+ if(existing_data && existing_data[settings.tokenValue] === item[settings.tokenValue]) {
+ found_existing_token = existing_token;
+ return false;
+ }
+ });
+
+ if(found_existing_token) {
+ select_token(found_existing_token);
+ input_token.insertAfter(found_existing_token);
+ focusWithTimeout(input_box);
+ return;
+ }
+ }
+
+ // Squeeze input_box so we force no unnecessary line break
+ input_box.width(1);
+
+ // Insert the new tokens
+ if($(input).data("settings").tokenLimit == null || token_count < $(input).data("settings").tokenLimit) {
+ insert_token(item);
+ // Remove the placeholder so it's not seen after you've added a token
+ input_box.attr("placeholder", null);
+ checkTokenLimit();
+ }
+
+ // Clear input box
input_box.val("");
-
- // Hide dropdown if it is visible (eg if we clicked to select token)
+
+ // Don't show the help dropdown, they've got the idea
hide_dropdown();
+
+ // Execute the onAdd callback if defined
+ if($.isFunction(callback)) {
+ callback.call(hiddenInput,item);
+ }
}
- }
-
- // Deselect a token in the token list
- function deselect_token (token, position) {
- token.removeClass(settings.classes.selectedToken);
- selected_token = null;
-
- if(position === POSITION.BEFORE) {
- input_token.insertBefore(token);
- selected_token_index--;
- } else if(position === POSITION.AFTER) {
- input_token.insertAfter(token);
- selected_token_index++;
- } else {
- input_token.appendTo(token_list);
- selected_token_index = token_count;
- }
-
- // Show the input box and give it focus again
- input_box.focus();
- }
-
- // Toggle selection of a token in the token list
- function toggle_select_token(token) {
- var previous_selected_token = selected_token;
-
- if(selected_token) {
- deselect_token($(selected_token), POSITION.END);
+
+ // Select a token in the token list
+ function select_token (token) {
+ if (!$(input).data("settings").disabled) {
+ token.addClass($(input).data("settings").classes.selectedToken);
+ selected_token = token.get(0);
+
+ // Hide input box
+ input_box.val("");
+
+ // Hide dropdown if it is visible (eg if we clicked to select token)
+ hide_dropdown();
+ }
}
-
- if(previous_selected_token === token.get(0)) {
- deselect_token(token, POSITION.END);
- } else {
- select_token(token);
+
+ // Deselect a token in the token list
+ function deselect_token (token, position) {
+ token.removeClass($(input).data("settings").classes.selectedToken);
+ selected_token = null;
+
+ if(position === POSITION.BEFORE) {
+ input_token.insertBefore(token);
+ selected_token_index--;
+ } else if(position === POSITION.AFTER) {
+ input_token.insertAfter(token);
+ selected_token_index++;
+ } else {
+ input_token.appendTo(token_list);
+ selected_token_index = token_count;
+ }
+
+ // Show the input box and give it focus again
+ focusWithTimeout(input_box);
}
- }
-
- // Delete a token from the token list
- function delete_token (token) {
- // Remove the id from the saved list
- var token_data = $.data(token.get(0), "tokeninput");
- var callback = settings.onDelete;
-
- var index = token.prevAll().length;
- if(index > selected_token_index) index--;
-
- // Delete the token
- token.remove();
- selected_token = null;
-
- // Show the input box and give it focus again
- input_box.focus();
-
- // Remove this token from the saved list
- saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
- if(index < selected_token_index) selected_token_index--;
-
- // Update the hidden input
- update_hidden_input(saved_tokens, hidden_input);
-
- token_count -= 1;
-
- if(settings.tokenLimit !== null) {
- input_box
- .show()
- .val("")
- .focus();
+
+ // Toggle selection of a token in the token list
+ function toggle_select_token(token) {
+ var previous_selected_token = selected_token;
+
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.END);
+ }
+
+ if(previous_selected_token === token.get(0)) {
+ deselect_token(token, POSITION.END);
+ } else {
+ select_token(token);
+ }
}
-
- // Execute the onDelete callback if defined
- if($.isFunction(callback)) {
- callback.call(hidden_input,token_data);
+
+ // Delete a token from the token list
+ function delete_token (token) {
+ // Remove the id from the saved list
+ var token_data = $.data(token.get(0), "tokeninput");
+ var callback = $(input).data("settings").onDelete;
+
+ var index = token.prevAll().length;
+ if(index > selected_token_index) index--;
+
+ // Delete the token
+ token.remove();
+ selected_token = null;
+
+ // Show the input box and give it focus again
+ focusWithTimeout(input_box);
+
+ // Remove this token from the saved list
+ saved_tokens = saved_tokens.slice(0,index).concat(saved_tokens.slice(index+1));
+ if (saved_tokens.length == 0) {
+ input_box.attr("placeholder", settings.placeholder)
+ }
+ if(index < selected_token_index) selected_token_index--;
+
+ // Update the hidden input
+ update_hiddenInput(saved_tokens, hiddenInput);
+
+ token_count -= 1;
+
+ if($(input).data("settings").tokenLimit !== null) {
+ input_box
+ .show()
+ .val("");
+ focusWithTimeout(input_box);
+ }
+
+ // Execute the onDelete callback if defined
+ if($.isFunction(callback)) {
+ callback.call(hiddenInput,token_data);
+ }
}
- }
-
- // Update the hidden input box value
- function update_hidden_input(saved_tokens, hidden_input) {
- var token_values = $.map(saved_tokens, function (el) {
- if(typeof settings.tokenValue == 'function')
- return settings.tokenValue.call(this, 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();
- selected_dropdown_item = null;
- }
-
- function show_dropdown() {
- dropdown
- .css({
- position: "absolute",
- top: $(token_list).offset().top + $(token_list).outerHeight(),
- left: $(token_list).offset().left,
- width: $(token_list).width(),
- 'z-index': 999
- })
- .show();
- }
-
- function show_dropdown_searching () {
- if(settings.searchingText) {
- dropdown.html("
"+settings.searchingText+"
");
- show_dropdown();
+
+ // Update the hidden input box value
+ function update_hiddenInput(saved_tokens, hiddenInput) {
+ var token_values = $.map(saved_tokens, function (el) {
+ if(typeof $(input).data("settings").tokenValue == 'function')
+ return $(input).data("settings").tokenValue.call(this, el);
+
+ return el[$(input).data("settings").tokenValue];
+ });
+ hiddenInput.val(token_values.join($(input).data("settings").tokenDelimiter));
+
}
- }
-
- function show_dropdown_hint () {
- if(settings.hintText) {
- dropdown.html("
"+settings.hintText+"
");
- show_dropdown();
+
+ // Hide and clear the results dropdown
+ function hide_dropdown () {
+ dropdown.hide().empty();
+ selected_dropdown_item = null;
}
- }
-
- // Highlight the query part of the search term
- 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) {
- if(results && results.length) {
- dropdown.empty();
- var dropdown_ul = $("
")
- .appendTo(dropdown)
- .mouseover(function (event) {
- select_dropdown_item($(event.target).closest("li"));
- })
- .mousedown(function (event) {
- add_token($(event.target).closest("li").data("tokeninput"));
- hidden_input.trigger('change');
- return false;
+
+ function show_dropdown() {
+ dropdown
+ .css({
+ position: "absolute",
+ top: token_list.offset().top + token_list.outerHeight(true),
+ left: token_list.offset().left,
+ width: token_list.width(),
+ 'z-index': $(input).data("settings").zindex
})
- .hide();
-
- $.each(results, function(index, value) {
- 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 {
- this_li.addClass(settings.classes.dropdownItem2);
- }
-
- if(index === 0) {
- select_dropdown_item(this_li);
- }
-
- $.data(this_li.get(0), "tokeninput", value);
- });
-
- show_dropdown();
-
- if(settings.animateDropdown) {
- dropdown_ul.slideDown("fast");
- } else {
- dropdown_ul.show();
- }
- } else {
- if(settings.noResultsText) {
- dropdown.html(""+settings.noResultsText+"
");
+ .show();
+ }
+
+ function show_dropdown_searching () {
+ if($(input).data("settings").searchingText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").searchingText) + "
");
show_dropdown();
}
}
- }
-
- // Highlight an item in the results dropdown
- function select_dropdown_item (item) {
- if(item) {
- if(selected_dropdown_item) {
- deselect_dropdown_item($(selected_dropdown_item));
+
+ function show_dropdown_hint () {
+ if($(input).data("settings").hintText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").hintText) + "
");
+ show_dropdown();
}
-
- item.addClass(settings.classes.selectedDropdownItem);
- selected_dropdown_item = item.get(0);
}
- }
-
- // Remove highlighting from an item in the results dropdown
- function deselect_dropdown_item (item) {
- item.removeClass(settings.classes.selectedDropdownItem);
- selected_dropdown_item = null;
- }
-
- // Do a search and show the "searching" dropdown if the input is longer
- // than settings.minChars
- function do_search() {
- var query = input_box.val();
-
- if(query && query.length) {
- if(selected_token) {
- deselect_token($(selected_token), POSITION.AFTER);
+
+ var regexp_special_chars = new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g');
+ function regexp_escape(term) {
+ return term.replace(regexp_special_chars, '\\$&');
+ }
+
+ // Highlight the query part of the search term
+ function highlight_term(value, term) {
+ return value.replace(
+ new RegExp(
+ "(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(term) + ")(?![^<>]*>)(?![^&;]+;)",
+ "gi"
+ ), function(match, p1) {
+ return "" + escapeHTML(p1) + "";
+ }
+ );
+ }
+
+ function find_value_and_highlight_term(template, value, term) {
+ return template.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + regexp_escape(value) + ")(?![^<>]*>)(?![^&;]+;)", "g"), highlight_term(value, term));
+ }
+
+ // exclude existing tokens from dropdown, so the list is clearer
+ function excludeCurrent(results) {
+ if ($(input).data("settings").excludeCurrent) {
+ var currentTokens = $(input).data("tokenInputObject").getTokens(),
+ trimmedList = [];
+ if (currentTokens.length) {
+ $.each(results, function(index, value) {
+ var notFound = true;
+ $.each(currentTokens, function(cIndex, cValue) {
+ if (value[$(input).data("settings").propertyToSearch] == cValue[$(input).data("settings").propertyToSearch]) {
+ notFound = false;
+ return false;
+ }
+ });
+
+ if (notFound) {
+ trimmedList.push(value);
+ }
+ });
+ results = trimmedList;
+ }
}
-
- if(query.length >= settings.minChars) {
- show_dropdown_searching();
- clearTimeout(timeout);
-
- timeout = setTimeout(function(){
- run_search(query);
- }, settings.searchDelay);
+
+ return results;
+ }
+
+ // Populate the results dropdown with some results
+ function populateDropdown (query, results) {
+ // exclude current tokens if configured
+ results = excludeCurrent(results);
+
+ if(results && results.length) {
+ dropdown.empty();
+ var dropdown_ul = $("")
+ .appendTo(dropdown)
+ .mouseover(function (event) {
+ select_dropdown_item($(event.target).closest("li"));
+ })
+ .mousedown(function (event) {
+ add_token($(event.target).closest("li").data("tokeninput"));
+ hiddenInput.change();
+ return false;
+ })
+ .hide();
+
+ if ($(input).data("settings").resultsLimit && results.length > $(input).data("settings").resultsLimit) {
+ results = results.slice(0, $(input).data("settings").resultsLimit);
+ }
+
+ $.each(results, function(index, value) {
+ var this_li = $(input).data("settings").resultsFormatter(value);
+
+ this_li = find_value_and_highlight_term(this_li ,value[$(input).data("settings").propertyToSearch], query);
+ this_li = $(this_li).appendTo(dropdown_ul);
+
+ if(index % 2) {
+ this_li.addClass($(input).data("settings").classes.dropdownItem);
+ } else {
+ this_li.addClass($(input).data("settings").classes.dropdownItem2);
+ }
+
+ if(index === 0 && $(input).data("settings").autoSelectFirstResult) {
+ select_dropdown_item(this_li);
+ }
+
+ $.data(this_li.get(0), "tokeninput", value);
+ });
+
+ show_dropdown();
+
+ if($(input).data("settings").animateDropdown) {
+ dropdown_ul.slideDown("fast");
+ } else {
+ dropdown_ul.show();
+ }
} else {
- hide_dropdown();
+ if($(input).data("settings").noResultsText) {
+ dropdown.html("" + escapeHTML($(input).data("settings").noResultsText) + "
");
+ show_dropdown();
+ }
}
}
- }
-
- // Do the actual search
- function run_search(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(url.indexOf("?") > -1) {
- var parts = url.split("?");
- ajax_params.url = parts[0];
-
- var param_array = parts[1].split("&");
- $.each(param_array, function (index, value) {
- var kv = value.split("=");
- ajax_params.data[kv[0]] = kv[1];
- });
+
+ // Highlight an item in the results dropdown
+ function select_dropdown_item (item) {
+ if(item) {
+ if(selected_dropdown_item) {
+ deselect_dropdown_item($(selected_dropdown_item));
+ }
+
+ item.addClass($(input).data("settings").classes.selectedDropdownItem);
+ selected_dropdown_item = item.get(0);
+ }
+ }
+
+ // Remove highlighting from an item in the results dropdown
+ function deselect_dropdown_item (item) {
+ item.removeClass($(input).data("settings").classes.selectedDropdownItem);
+ selected_dropdown_item = null;
+ }
+
+ // Do a search and show the "searching" dropdown if the input is longer
+ // than $(input).data("settings").minChars
+ function do_search() {
+ var query = input_box.val();
+
+ if(query && query.length) {
+ if(selected_token) {
+ deselect_token($(selected_token), POSITION.AFTER);
+ }
+
+ if(query.length >= $(input).data("settings").minChars) {
+ show_dropdown_searching();
+ clearTimeout(timeout);
+
+ timeout = setTimeout(function(){
+ run_search(query);
+ }, $(input).data("settings").searchDelay);
} else {
- ajax_params.url = url;
+ hide_dropdown();
}
-
- // Prepare the request
- ajax_params.data[settings.queryParam] = query;
- ajax_params.type = settings.method;
- ajax_params.dataType = settings.contentType;
- if(settings.crossDomain) {
- ajax_params.dataType = "jsonp";
+ }
+ }
+
+ // Do the actual search
+ function run_search(query) {
+ var cache_key = query + computeURL();
+ var cached_results = cache.get(cache_key);
+ if (cached_results) {
+ if ($.isFunction($(input).data("settings").onCachedResult)) {
+ cached_results = $(input).data("settings").onCachedResult.call(hiddenInput, cached_results);
}
-
- // Attach the success callback
- ajax_params.success = function(results) {
- if($.isFunction(settings.onResult)) {
- results = settings.onResult.call(hidden_input, 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() === query) {
- populate_dropdown(query, settings.jsonContainer ? results[settings.jsonContainer] : results);
- }
- };
-
- // Make the request
- $.ajax(ajax_params);
- } else if(settings.local_data) {
- // Do the search through local data
- var results = $.grep(settings.local_data, function (row) {
- return row[settings.propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
- });
-
- if($.isFunction(settings.onResult)) {
- results = settings.onResult.call(hidden_input, results);
+ populateDropdown(query, cached_results);
+ } else {
+ // Are we doing an ajax search or local data search?
+ if($(input).data("settings").url) {
+ var url = computeURL();
+ // Extract existing get params
+ var ajax_params = {};
+ ajax_params.data = {};
+ if(url.indexOf("?") > -1) {
+ var parts = url.split("?");
+ ajax_params.url = parts[0];
+
+ var param_array = parts[1].split("&");
+ $.each(param_array, function (index, value) {
+ var kv = value.split("=");
+ ajax_params.data[kv[0]] = kv[1];
+ });
+ } else {
+ ajax_params.url = url;
+ }
+
+ // Prepare the request
+ ajax_params.data[$(input).data("settings").queryParam] = query;
+ ajax_params.type = $(input).data("settings").method;
+ ajax_params.dataType = $(input).data("settings").contentType;
+ if ($(input).data("settings").crossDomain) {
+ ajax_params.dataType = "jsonp";
+ }
+
+ // exclude current tokens?
+ // send exclude list to the server, so it can also exclude existing tokens
+ if ($(input).data("settings").excludeCurrent) {
+ var currentTokens = $(input).data("tokenInputObject").getTokens();
+ var tokenList = $.map(currentTokens, function (el) {
+ if(typeof $(input).data("settings").tokenValue == 'function')
+ return $(input).data("settings").tokenValue.call(this, el);
+
+ return el[$(input).data("settings").tokenValue];
+ });
+
+ ajax_params.data[$(input).data("settings").excludeCurrentParameter] = tokenList.join($(input).data("settings").tokenDelimiter);
+ }
+
+ // Attach the success callback
+ ajax_params.success = function(results) {
+ cache.add(cache_key, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
+ if($.isFunction($(input).data("settings").onResult)) {
+ results = $(input).data("settings").onResult.call(hiddenInput, results);
+ }
+
+ // only populate the dropdown if the results are associated with the active search query
+ if(input_box.val() === query) {
+ populateDropdown(query, $(input).data("settings").jsonContainer ? results[$(input).data("settings").jsonContainer] : results);
+ }
+ };
+
+ // Provide a beforeSend callback
+ if (settings.onSend) {
+ settings.onSend(ajax_params);
+ }
+
+ // Make the request
+ $.ajax(ajax_params);
+ } else if($(input).data("settings").local_data) {
+ // Do the search through local data
+ var results = $.grep($(input).data("settings").local_data, function (row) {
+ return row[$(input).data("settings").propertyToSearch].toLowerCase().indexOf(query.toLowerCase()) > -1;
+ });
+
+ cache.add(cache_key, results);
+ if($.isFunction($(input).data("settings").onResult)) {
+ results = $(input).data("settings").onResult.call(hiddenInput, results);
+ }
+ populateDropdown(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(settings);
+
+ // compute the dynamic URL
+ function computeURL() {
+ var settings = $(input).data("settings");
+ return typeof settings.url == 'function' ? settings.url.call(settings) : settings.url;
}
- return url;
- }
-};
-
-// Really basic cache for the results
-$.TokenList.Cache = function (options) {
- var settings = $.extend({
- max_size: 500
- }, options);
-
- var data = {};
- var size = 0;
-
- var flush = function () {
+
+ // Bring browser focus to the specified object.
+ // Use of setTimeout is to get around an IE bug.
+ // (See, e.g., http://stackoverflow.com/questions/2600186/focus-doesnt-work-in-ie)
+ //
+ // obj: a jQuery object to focus()
+ function focusWithTimeout(object) {
+ setTimeout(
+ function() {
+ object.focus();
+ },
+ 50
+ );
+ }
+ };
+
+ // Really basic cache for the results
+ $.TokenList.Cache = function (options) {
+ var settings, data = {}, size = 0, flush;
+
+ settings = $.extend({ max_size: 500 }, options);
+
+ flush = function () {
data = {};
size = 0;
- };
-
- this.add = function (query, results) {
- if(size > settings.max_size) {
- flush();
+ };
+
+ this.add = function (query, results) {
+ if (size > settings.max_size) {
+ flush();
}
-
- if(!data[query]) {
- size += 1;
+
+ if (!data[query]) {
+ size += 1;
}
-
+
data[query] = results;
- };
-
- this.get = function (query) {
+ };
+
+ this.get = function (query) {
return data[query];
+ };
};
-};
-}(jQuery));
+
+ }(jQuery));
+
\ No newline at end of file
diff --git a/kitsune/sumo/static/sumo/js/messages.autocomplete.js b/kitsune/sumo/static/sumo/js/messages.autocomplete.js
new file mode 100644
index 00000000000..36340eb0cc9
--- /dev/null
+++ b/kitsune/sumo/static/sumo/js/messages.autocomplete.js
@@ -0,0 +1,75 @@
+import "sumo/js/libs/jquery.tokeninput";
+import { safeString, safeInterpolate } from "sumo/js/main";
+
+/*
+ * autocomplete.js
+ * A generic autocomplete widget for both groups and users.
+ */
+
+(function($) {
+
+ 'use strict';
+
+ function initAutocomplete(options) {
+ function wrapTerm(string, term) {
+ term = (term + '').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
+ var regex = new RegExp( '(' + term + ')', 'gi' );
+ return string.replace(regex, '$1');
+ }
+
+ var prefill = [];
+ var selector = options.selector;
+ var valueField = options.valueField;
+
+ if ($(selector).val()) {
+ prefill = $(selector).val().split(',').map(function(value) {
+ var item = {};
+ item[valueField] = safeString(value);
+ if (options.displayField) {
+ item[options.displayField] = safeString(value);
+ }
+ return item;
+ });
+ }
+
+ var tokenInputSettings = {
+ theme: 'facebook',
+ hintText: gettext(options.hintText),
+ queryParam: 'term',
+ propertyToSearch: valueField,
+ tokenValue: valueField,
+ prePopulate: prefill,
+ resultsFormatter: function(item) {
+ var term = $(`token-input-${selector}`).val();
+ if (options.resultsFormatter) {
+ return options.resultsFormatter(item, term);
+ }
+ return safeInterpolate('%(value)s
', {value: item['type']}, true);
+ },
+ onAdd: function (item) {
+ $(this).closest('.single').closest('form').submit();
+ }
+ };
+
+ $(`input${selector}`).tokenInput(options.apiEndpoint, tokenInputSettings);
+ }
+
+ // Initialize autocomplete for users or groups
+ $(function() {
+ initAutocomplete({
+ selector: '.user-autocomplete',
+ apiEndpoint: $('body').data('messages-api'),
+ valueField: 'name',
+ displayField: 'name',
+ hintText: 'Search for a user or group. Group mail requires Staff group membership.',
+ placeholder: 'Type a user or group name',
+ resultsFormatter: function(item) {
+ if ((item.display_name) && (item.type === 'user')) {
+ return safeInterpolate('%(display_name)s [%(name)s]
', item, true);
+ }
+ return safeInterpolate('%(name)s
', item, true);
+ }
+ });
+ });
+
+})(jQuery);
diff --git a/kitsune/sumo/static/sumo/scss/components/_inbox.scss b/kitsune/sumo/static/sumo/scss/components/_inbox.scss
index 3cfefece11a..19be628d01e 100644
--- a/kitsune/sumo/static/sumo/scss/components/_inbox.scss
+++ b/kitsune/sumo/static/sumo/scss/components/_inbox.scss
@@ -66,7 +66,7 @@
}
.avatar-details {
- width: 190px;
+ width: 100%;
flex: 0 0 auto;
}
}
@@ -81,6 +81,16 @@
.avatar-details {
display: flex;
align-items: center;
+ gap: 12px;
+
+ span {
+ flex: 2;
+ text-align: left;
+ }
+
+ .message-view {
+ flex: 2;
+ }
.avatar {
display: block;
@@ -94,11 +104,9 @@
}
}
- .user {
- flex: 1 1 auto;
+ .user, .group {
a {
- font-weight: bold;
color: var(--color-heading);
text-decoration: none;
@@ -108,11 +116,11 @@
}
}
- time {
- display: block;
- @include c.text-body-xs;
- font-weight: normal;
- color: var(--color-text);
+ p {
+ font-weight: bold;
+ color: var(--color-heading);
+ text-decoration: none;
+ padding-right:5px;
}
}
}
@@ -122,3 +130,31 @@
@include c.ulol;
}
}
+
+@media (max-width: 767px) {
+ .group, .user {
+ display: none;
+ }
+}
+
+time {
+ display: block;
+ @include c.text-body-xs;
+ font-weight: normal;
+ color: var(--color-text);
+}
+
+.to,
+.to-group {
+ p {
+ margin: 0;
+ padding: 0;
+ font-weight: bold;
+ color: var(--color-heading);
+ text-decoration: none;
+ }
+}
+
+.read-message {
+ margin-top: 20px;
+}
\ No newline at end of file
diff --git a/kitsune/sumo/templatetags/jinja_helpers.py b/kitsune/sumo/templatetags/jinja_helpers.py
index 88add0261b1..dd2faa27b58 100644
--- a/kitsune/sumo/templatetags/jinja_helpers.py
+++ b/kitsune/sumo/templatetags/jinja_helpers.py
@@ -30,6 +30,7 @@
from kitsune.sumo.urlresolvers import reverse
from kitsune.sumo.utils import is_trusted_user as is_trusted_user_func
from kitsune.sumo.utils import webpack_static as webpack_static_func
+from kitsune.sumo.utils import in_staff_group
from kitsune.users.models import Profile
from kitsune.wiki.showfor import showfor_data as _showfor_data
@@ -567,3 +568,6 @@ def show_header_fx_download(context):
@library.global_function
def is_trusted_user(user):
return is_trusted_user_func(user)
+
+
+library.global_function(in_staff_group)
diff --git a/kitsune/sumo/utils.py b/kitsune/sumo/utils.py
index b6569040fa8..2f97630940f 100644
--- a/kitsune/sumo/utils.py
+++ b/kitsune/sumo/utils.py
@@ -368,3 +368,8 @@ def is_trusted_user(user: User) -> bool:
user.is_staff,
]
)
+
+
+def in_staff_group(user: User | None) -> bool:
+ """Check if a user is in the Staff group."""
+ return bool(user and user.is_authenticated and user.profile.in_staff_group)
diff --git a/kitsune/users/models.py b/kitsune/users/models.py
index 8585a3b6993..b48e4e424d3 100644
--- a/kitsune/users/models.py
+++ b/kitsune/users/models.py
@@ -1,6 +1,7 @@
import logging
import re
from datetime import datetime
+from functools import cached_property
from django.conf import settings
from django.contrib.auth.models import User
@@ -227,6 +228,10 @@ def answer_helpfulness(self):
def is_subscriber(self):
return self.products.exists()
+ @cached_property
+ def in_staff_group(self):
+ return self.user.groups.filter(name=settings.STAFF_GROUP).exists()
+
class Setting(ModelBase):
"""User specific value per setting"""
diff --git a/kitsune/users/tests/test_tasks.py b/kitsune/users/tests/test_tasks.py
index b44d59a76d5..016255336d0 100644
--- a/kitsune/users/tests/test_tasks.py
+++ b/kitsune/users/tests/test_tasks.py
@@ -30,8 +30,8 @@ def test_process_delete_user(self):
# Populate inboxes and outboxes with messages between the user and other users.
other_users = UserFactory.create_batch(2)
for sender in other_users:
- send_message([user], "foo", sender=sender)
- send_message(other_users, "bar", sender=user)
+ send_message([user], to_group="", text="foo", sender=sender)
+ send_message(other_users, to_group="", text="bar", sender=user)
# Confirm the expected initial state.
self.assertTrue(user.is_active)
diff --git a/kitsune/users/tests/test_views.py b/kitsune/users/tests/test_views.py
index d666c2e9782..e0e8a02c343 100644
--- a/kitsune/users/tests/test_views.py
+++ b/kitsune/users/tests/test_views.py
@@ -438,8 +438,8 @@ def setUp(self):
# Populate inboxes and outboxes with messages between the user and other users.
self.other_users = UserFactory.create_batch(2)
for sender in self.other_users:
- send_message([self.user], "foo", sender=sender)
- send_message(self.other_users, "bar", sender=self.user)
+ send_message([self.user], to_group="", text="foo", sender=sender)
+ send_message(self.other_users, to_group="", text="bar", sender=self.user)
super(UserCloseAccountTests, self).setUp()
def tearDown(self):
diff --git a/webpack/entrypoints.js b/webpack/entrypoints.js
index 7325381c199..d559f733f8a 100644
--- a/webpack/entrypoints.js
+++ b/webpack/entrypoints.js
@@ -90,7 +90,7 @@ const entrypoints = {
"sumo/js/reportabuse.js",
],
messages: [
- "sumo/js/users.autocomplete.js",
+ "sumo/js/messages.autocomplete.js",
"sumo/js/messages.js",
],
groups: [