"Tagger" Plugin for jQuery
The Problem
Tags are really cool but can be clumsy to edit as comma-seperated lists in a tiny text box:
In the example above, it is difficult to see many tags at once. The user must also be very careful of where the commas are placed, as they will be interepreted as tag separators.
The Solution
Each tag should be wrapped in its own container to clearly distinguish it as a separate entity to the user. This helps eliminate confusion while allowing for editing at the tag level (as opposed to editing at the tag-list level).
The tag input box should have a generous width, about as long as a few average-size tags. The tag entry box should also grow vertically as more tags are added so that all tags can be seen.
Demo
The Tagger plugin makes it easy to convert any standard text-input box into an easy-to-use Tagger box. Just click and type as usual, but when you press "," or [Enter], the current text is wrapped in its own little box, making it easy to see each tag.
You can also click an existing tag to edit it, or clear it by deleting the text.
Usage
// Simple
$('input[name=tags]').tagger();
// With options
$('input[name=tags]').tagger({
'class': 'tagger', // specify a custom class name for the tagger container
inputAutosize: true, // if true, the size of the fake input box will grow with the text (recommended)
beforeAdd: function(tag, tagger) {}, // if the callback function returns false, the tag will not be added
afterAdd: function(tag, tagger) {}, // called after a tag has been added
beforeEdit: function(tag, tagger) {}, // if the callback function returns false, the tag will not be editable
afterEdit: function(tag, tagger) {}, // called after the tag has been edited
beforeDelete: function(tag, tagger) {}, // if the callback function returns false, the tag will not be deleted
beforeDelete: function(tag, tagger) {} // called after the tag has been deleted
});
Source
/*
Tagger - Converts any text input box into an easy-to-edit multiple-tag interface.
*/
(function($){
var trim = function(str)
{
return str.replace(/^\s+|\s+$/g, '');
};
var emptyFunction = function(){};
var Tagger = function(element, options)
{
var obj = this;
var element = $(element);
var container = $('<div></div>');
var input = $('<input type="text"/>');
var defaults = {
'class': 'tagger',
inputAutosize: true,
beforeAdd: emptyFunction,
afterAdd: emptyFunction,
beforeEdit: emptyFunction,
afterEdit: emptyFunction,
beforeDelete: emptyFunction,
afterDelete: emptyFunction
};
var config = $.extend(defaults, options || {});
this.getContainer = function()
{
return container;
};
this.getConfig = function()
{
return config;
};
this.addTag = function(tag)
{
if (false === config.beforeAdd(tag, obj)) return obj;
if (tag.getContainer !== undefined) {
input.before(tag.getContainer());
}
obj.updateRealInput();
config.afterAdd(tag, obj);
return obj;
};
if (config['class']) container.addClass(config['class']);
element.hide(0);
element.after(container);
container.append(input);
container.click(function()
{
input.focus();
});
this.insertCurrentInput = function()
{
var val = input.val();
if (val) {
var tag = new Tag(obj);
tag.setTag(val);
obj.addTag(tag);
}
input.val('');
};
input.keydown(function(event)
{
switch (event.keyCode) {
// comma, enter
case 188:
case 13:
obj.insertCurrentInput();
event.preventDefault();
break;
}
});
input.keypress(function(event)
{
// Detect backspace
if (event.keyCode != 8) return;
if (!input.val()) {
// "click" last item
container.find('span.tag:last').click();
event.preventDefault();
}
});
input.blur(obj.insertCurrentInput);
this.autogrow = function()
{
if (true != config.inputAutosize) return;
var size = input.val().length + 1;
input.attr('size', size);
};
this.getInput = function()
{
return input;
};
this.createTag = function(tagName)
{
var tag = new Tag(obj);
tag.setTag(tagName);
obj.addTag(tag);
};
this.updateRealInput = function()
{
var allTags = [];
container.find('span.tag').each(function()
{
allTags.push($(this).text());
});
var tags = allTags.join(', ');
element.val(tags);
};
input.change(this.autogrow);
input.keyup(this.autogrow);
// Handle current value
var tagString = element.val();
if (tagString) {
var tags = tagString.split(',');
$.each(tags, function(index, tagName)
{
obj.createTag(tagName);
});
}
};
var Tag = function(tagger)
{
var obj = this;
var tagger = tagger;
var container = $('<span></span>');
container.addClass('tag');
this.getContainer = function()
{
return container;
};
this.remove = function()
{
return container.remove();
};
this.setTag = function(newTag)
{
tag = trim(newTag);
if (!tag) {
if (false === tagger.getConfig().beforeDelete(obj, tagger)) return;
var previous = container.prev('span.tag');
if (previous.length > 0) {
previous.click();
} else {
tagger.getInput().show(0).focus();
}
container.remove();
tagger.getConfig().afterDelete(obj, tagger);
return;
}
tagger.getInput().show(0).focus();
container.text(tag);
tagger.updateRealInput();
};
this.edit = function(event)
{
if (false === tagger.getConfig().beforeEdit(obj, tagger)) return;
tagger.getInput().hide(0);
var input = $('<input type="text"/>');
var doneEditing = function()
{
obj.setTag(input.val());
input.remove();
container.removeClass('editing');
};
input.css({width: container.outerWidth() + 'px'});
input.val(container.text());
input.blur(doneEditing);
input.keydown(function(event)
{
switch (event.keyCode) {
// comma, enter
case 188:
case 13:
doneEditing();
event.preventDefault();
break;
}
});
input.keypress(function(event)
{
// Backspace
if (event.keyCode != 8) return;
if (!input.val()) {
doneEditing();
event.preventDefault();
}
});
input.click(function(event)
{
event.stopPropagation();
});
container.html(input);
container.addClass('editing');
input.focus();
event.stopPropagation();
tagger.getConfig().afterEdit(obj, tagger);
};
container.click(this.edit);
};
$.fn.extend({
tagger: function(options)
{
return this.each(function()
{
if ($(this).data('tagger')) return;
var tagger = new Tagger(this, options);
$(this).data('tagger', tagger);
});
}
});
})(jQuery);
