Login with OpenID

"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);