2014-04-22 19:22:36 +00:00
|
|
|
(function ($) {
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
var defaultOptions = {
|
|
|
|
tagClass: function(item) {
|
|
|
|
return 'label label-info';
|
|
|
|
},
|
|
|
|
itemValue: function(item) {
|
|
|
|
return item ? item.toString() : item;
|
|
|
|
},
|
|
|
|
itemText: function(item) {
|
|
|
|
return this.itemValue(item);
|
|
|
|
},
|
|
|
|
freeInput : true
|
|
|
|
};
|
|
|
|
|
|
|
|
function TagsInput(element, options) {
|
|
|
|
this.itemsArray = [];
|
|
|
|
|
|
|
|
this.$element = $(element);
|
|
|
|
this.$element.hide();
|
|
|
|
|
|
|
|
this.isSelect = (element.tagName === 'SELECT');
|
|
|
|
this.multiple = (this.isSelect && element.hasAttribute('multiple'));
|
|
|
|
this.objectItems = options && options.itemValue;
|
|
|
|
|
|
|
|
this.$container = $('<div class="bootstrap-tagsinput"></div>');
|
2014-04-22 19:42:39 +00:00
|
|
|
this.$input = $('<input size="100" type="text" />').appendTo(this.$container);
|
2014-04-22 19:22:36 +00:00
|
|
|
|
|
|
|
this.$element.after(this.$container);
|
|
|
|
|
|
|
|
this.build(options);
|
|
|
|
}
|
|
|
|
|
|
|
|
TagsInput.prototype = {
|
|
|
|
constructor: TagsInput,
|
|
|
|
|
|
|
|
add: function(item, dontPushVal) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
// Ignore falsey values, except false
|
|
|
|
if (item !== false && !item)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Throw an error when trying to add an object while the itemValue option was not set
|
|
|
|
if (typeof item === "object" && !self.objectItems)
|
|
|
|
throw("Can't add objects when itemValue option is not set");
|
|
|
|
|
|
|
|
// Ignore strings only containg whitespace
|
|
|
|
if (item.toString().match(/^\s*$/))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// If SELECT but not multiple, remove current tag
|
|
|
|
if (self.isSelect && !self.multiple && self.itemsArray.length > 0)
|
|
|
|
self.remove(self.itemsArray[0]);
|
|
|
|
|
|
|
|
if (typeof item === "string" && this.$element[0].tagName === 'INPUT') {
|
|
|
|
var items = item.split(',');
|
|
|
|
if (items.length > 1) {
|
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
|
|
this.add(items[i], true);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!dontPushVal)
|
|
|
|
self.pushVal();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ignore items allready added
|
|
|
|
var itemValue = self.options.itemValue(item),
|
|
|
|
itemText = self.options.itemText(item),
|
|
|
|
tagClass = self.options.tagClass(item);
|
|
|
|
|
|
|
|
if ($.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0])
|
|
|
|
return;
|
|
|
|
|
|
|
|
// register item in internal array and map
|
|
|
|
self.itemsArray.push(item);
|
|
|
|
|
|
|
|
// add a tag element
|
|
|
|
var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '"><span class="tag-item">' + htmlEncode(itemText) + '</span><span class="remove" data-role="remove"></span></span>');
|
|
|
|
$tag.data('item', item);
|
|
|
|
self.$input.before($tag);
|
|
|
|
|
|
|
|
// add <option /> if item represents a value not present in one of the <select />'s options
|
|
|
|
if (self.isSelect && !$('option[value="' + escape(itemValue) + '"]')[0]) {
|
|
|
|
var $option = $('<option selected>' + htmlEncode(itemText) + '</option>');
|
|
|
|
$option.data('item', item);
|
|
|
|
$option.attr('value', itemValue);
|
|
|
|
self.$element.append($option);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!dontPushVal)
|
|
|
|
self.pushVal();
|
|
|
|
|
|
|
|
self.$element.trigger($.Event('itemAdded', { item: item }));
|
|
|
|
},
|
|
|
|
|
|
|
|
remove: function(item, dontPushVal) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
if (self.objectItems) {
|
|
|
|
if (typeof item === "object")
|
|
|
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } )[0];
|
|
|
|
else
|
|
|
|
item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } )[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (item) {
|
|
|
|
$('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove();
|
|
|
|
$('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove();
|
|
|
|
self.itemsArray.splice(self.itemsArray.indexOf(item), 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!dontPushVal)
|
|
|
|
self.pushVal();
|
|
|
|
|
|
|
|
self.$element.trigger($.Event('itemRemoved', { item: item }));
|
|
|
|
},
|
|
|
|
|
|
|
|
removeAll: function() {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
$('.tag', self.$container).remove();
|
|
|
|
$('option', self.$element).remove();
|
|
|
|
|
|
|
|
while(self.itemsArray.length > 0)
|
|
|
|
self.itemsArray.pop();
|
|
|
|
|
|
|
|
self.pushVal();
|
|
|
|
},
|
|
|
|
|
|
|
|
refresh: function() {
|
|
|
|
var self = this;
|
|
|
|
$('.tag', self.$container).each(function() {
|
|
|
|
var $tag = $(this),
|
|
|
|
item = $tag.data('item'),
|
|
|
|
itemValue = self.options.itemValue(item),
|
|
|
|
itemText = self.options.itemText(item),
|
|
|
|
tagClass = self.options.tagClass(item);
|
|
|
|
|
|
|
|
// Update tag's class and inner text
|
|
|
|
$tag.attr('class', null);
|
|
|
|
$tag.addClass('tag ' + htmlEncode(tagClass));
|
|
|
|
$tag.contents().filter(function() {
|
|
|
|
return this.nodeType == 3;
|
|
|
|
})[0].nodeValue = htmlEncode(itemText);
|
|
|
|
|
|
|
|
if (self.isSelect) {
|
|
|
|
var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; });
|
|
|
|
option.attr('value', itemValue);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
// Returns the items added as tags
|
|
|
|
items: function() {
|
|
|
|
return this.itemsArray;
|
|
|
|
},
|
|
|
|
|
|
|
|
// Assembly value by retrieving the value of each item, and set it on the element.
|
|
|
|
pushVal: function() {
|
|
|
|
var self = this,
|
|
|
|
val = $.map(self.items(), function(item) {
|
|
|
|
return self.options.itemValue(item).toString();
|
|
|
|
});
|
|
|
|
|
|
|
|
self.$element.val(val, true).trigger('change');
|
|
|
|
},
|
|
|
|
|
|
|
|
build: function(options) {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
self.options = $.extend({}, defaultOptions, options);
|
|
|
|
var typeahead = self.options.typeahead || {};
|
|
|
|
|
|
|
|
// When itemValue is set, freeInput should always be false
|
|
|
|
if (self.objectItems)
|
|
|
|
self.options.freeInput = false;
|
|
|
|
|
|
|
|
makeOptionItemFunction(self.options, 'itemValue');
|
|
|
|
makeOptionItemFunction(self.options, 'itemText');
|
|
|
|
makeOptionItemFunction(self.options, 'tagClass');
|
|
|
|
|
|
|
|
// for backwards compatibility, self.options.source is deprecated
|
|
|
|
if (self.options.source)
|
|
|
|
typeahead.source = self.options.source;
|
|
|
|
|
|
|
|
if (typeahead.source && $.fn.typeahead) {
|
|
|
|
makeOptionFunction(typeahead, 'source');
|
|
|
|
|
|
|
|
self.$input.typeahead({
|
|
|
|
source: function (query, process) {
|
|
|
|
function processItems(items) {
|
|
|
|
var texts = [];
|
|
|
|
|
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
|
|
var text = self.options.itemText(items[i]);
|
|
|
|
map[text] = items[i];
|
|
|
|
texts.push(text);
|
|
|
|
}
|
|
|
|
process(texts);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.map = {};
|
|
|
|
var map = this.map,
|
|
|
|
data = typeahead.source(query);
|
|
|
|
|
|
|
|
if ($.isFunction(data.success)) {
|
|
|
|
// support for Angular promises
|
|
|
|
data.success(processItems);
|
|
|
|
} else {
|
|
|
|
// support for functions and jquery promises
|
|
|
|
$.when(data)
|
|
|
|
.then(processItems);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
updater: function (text) {
|
|
|
|
self.add(this.map[text]);
|
|
|
|
},
|
|
|
|
matcher: function (text) {
|
|
|
|
return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1);
|
|
|
|
},
|
|
|
|
sorter: function (texts) {
|
|
|
|
return texts.sort();
|
|
|
|
},
|
|
|
|
highlighter: function (text) {
|
|
|
|
var regex = new RegExp( '(' + this.query + ')', 'gi' );
|
|
|
|
return text.replace( regex, "<strong>$1</strong>" );
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
self.$container.on('click', $.proxy(function(event) {
|
|
|
|
self.$input.focus();
|
|
|
|
}, self));
|
|
|
|
|
|
|
|
self.$container.on('keydown', 'input', $.proxy(function(event) {
|
|
|
|
var $input = $(event.target);
|
|
|
|
switch (event.which) {
|
|
|
|
// BACKSPACE
|
|
|
|
case 8:
|
|
|
|
if (doGetCaretPosition($input[0]) === 0) {
|
|
|
|
var prev = $input.prev();
|
|
|
|
if (prev) {
|
|
|
|
self.remove(prev.data('item'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
// DELETE
|
|
|
|
case 46:
|
|
|
|
if (doGetCaretPosition($input[0]) === 0) {
|
|
|
|
var next = $input.next();
|
|
|
|
if (next) {
|
|
|
|
self.remove(next.data('item'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
// LEFT ARROW
|
|
|
|
case 37:
|
|
|
|
// Try to move the input before the previous tag
|
|
|
|
var $prevTag = $input.prev();
|
|
|
|
if ($input.val().length === 0 && $prevTag[0]) {
|
|
|
|
$prevTag.before($input);
|
|
|
|
$input.focus();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
// LEFT ARROW
|
|
|
|
case 39:
|
|
|
|
// Try to move the input before the previous tag
|
|
|
|
var $nextTag = $input.next();
|
|
|
|
if ($input.val().length === 0 && $nextTag[0]) {
|
|
|
|
$nextTag.after($input);
|
|
|
|
$input.focus();
|
|
|
|
}
|
|
|
|
break;
|
2014-04-22 20:49:09 +00:00
|
|
|
case 188: // COMMA
|
|
|
|
case 13: // ENTER
|
2014-04-22 19:22:36 +00:00
|
|
|
if (self.options.freeInput) {
|
|
|
|
self.add($input.val());
|
|
|
|
$input.val('');
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2014-04-22 19:42:39 +00:00
|
|
|
$input.attr('size', Math.max(100, $input.val().length));
|
2014-04-22 19:22:36 +00:00
|
|
|
}, self));
|
|
|
|
|
|
|
|
// Remove icon clicked
|
|
|
|
self.$container.on('click', '[data-role=remove]', $.proxy(function(event) {
|
|
|
|
self.remove($(event.target).closest('.tag').data('item'));
|
|
|
|
}, self));
|
|
|
|
|
|
|
|
if (self.$element[0].tagName === 'INPUT') {
|
|
|
|
self.add(self.$element.val());
|
|
|
|
} else {
|
|
|
|
$('option', self.$element).each(function() {
|
|
|
|
self.add($(this).attr('value'), true);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
|
|
|
var self = this;
|
|
|
|
|
|
|
|
// Unbind events
|
|
|
|
self.$container.off('keypress', 'input');
|
|
|
|
self.$container.off('click', '[50role=remove]');
|
|
|
|
|
|
|
|
self.$container.remove();
|
|
|
|
self.$element.removeData('tagsinput');
|
|
|
|
self.$element.show();
|
|
|
|
},
|
|
|
|
|
|
|
|
focus: function() {
|
|
|
|
this.$input.focus();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// Register JQuery plugin
|
|
|
|
$.fn.tagsinput = function(arg1, arg2) {
|
|
|
|
var results = [];
|
|
|
|
|
|
|
|
this.each(function() {
|
|
|
|
var tagsinput = $(this).data('tagsinput');
|
|
|
|
|
|
|
|
// Initialize a new tags input
|
|
|
|
if (!tagsinput) {
|
|
|
|
tagsinput = new TagsInput(this, arg1);
|
|
|
|
$(this).data('tagsinput', tagsinput);
|
|
|
|
results.push(tagsinput);
|
|
|
|
|
|
|
|
if (this.tagName === 'SELECT') {
|
|
|
|
$('option', $(this)).attr('selected', 'selected');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Init tags from $(this).val()
|
|
|
|
$(this).val($(this).val());
|
|
|
|
} else {
|
|
|
|
// Invoke function on existing tags input
|
|
|
|
var retVal = tagsinput[arg1](arg2);
|
|
|
|
if (retVal !== undefined)
|
|
|
|
results.push(retVal);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if ( typeof arg1 == 'string') {
|
|
|
|
// Return the results from the invoked function calls
|
|
|
|
return results.length > 1 ? results : results[0];
|
|
|
|
} else {
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
$.fn.tagsinput.Constructor = TagsInput;
|
|
|
|
|
|
|
|
// Most options support both a string or number as well as a function as
|
|
|
|
// option value. This function makes sure that the option with the given
|
|
|
|
// key in the given options is wrapped in a function
|
|
|
|
function makeOptionItemFunction(options, key) {
|
|
|
|
if (typeof options[key] !== 'function') {
|
|
|
|
var value = options[key];
|
|
|
|
options[key] = function(item) { return item[value]; };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function makeOptionFunction(options, key) {
|
|
|
|
if (typeof options[key] !== 'function') {
|
|
|
|
var value = options[key];
|
|
|
|
options[key] = function() { return value; };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// HtmlEncodes the given value
|
|
|
|
var htmlEncodeContainer = $('<div />');
|
|
|
|
function htmlEncode(value) {
|
|
|
|
if (value) {
|
|
|
|
return htmlEncodeContainer.text(value).html();
|
|
|
|
} else {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Returns the position of the caret in the given input field
|
|
|
|
// http://flightschool.acylt.com/devnotes/caret-position-woes/
|
|
|
|
function doGetCaretPosition(oField) {
|
|
|
|
var iCaretPos = 0;
|
|
|
|
if (document.selection) {
|
|
|
|
oField.focus ();
|
|
|
|
var oSel = document.selection.createRange();
|
|
|
|
oSel.moveStart ('character', -oField.value.length);
|
|
|
|
iCaretPos = oSel.text.length;
|
|
|
|
} else if (oField.selectionStart || oField.selectionStart == '0') {
|
|
|
|
iCaretPos = oField.selectionStart;
|
|
|
|
}
|
|
|
|
return (iCaretPos);
|
|
|
|
}
|
|
|
|
|
|
|
|
$(function() {
|
|
|
|
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
|
|
|
|
});
|
|
|
|
})(window.jQuery);
|