integration/lib/tag-it.js
changeset 41 3ec2343f2b85
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/integration/lib/tag-it.js	Thu Nov 08 18:24:47 2012 +0100
@@ -0,0 +1,392 @@
+/*
+* jQuery UI Tag-it!
+*
+* @version v2.0 (06/2011)
+*
+* Copyright 2011, Levy Carneiro Jr.
+* Released under the MIT license.
+* http://aehlke.github.com/tag-it/LICENSE
+*
+* Homepage:
+*   http://aehlke.github.com/tag-it/
+*
+* Authors:
+*   Levy Carneiro Jr.
+*   Martin Rehfeld
+*   Tobias Schmidt
+*   Skylar Challand
+*   Alex Ehlke
+*
+* Maintainer:
+*   Alex Ehlke - Twitter: @aehlke
+*
+* Dependencies:
+*   jQuery v1.4+
+*   jQuery UI v1.8+
+*/
+(function($) {
+
+    $.widget('ui.tagit', {
+        options: {
+            itemName          : 'item',
+            fieldName         : 'tags',
+            availableTags     : [],
+            tagSource         : null,
+            removeConfirmation: false,
+            caseSensitive     : true,
+            placeholderText   : null,
+
+            // When enabled, quotes are not neccesary
+            // for inputting multi-word tags.
+            allowSpaces: false,
+
+            // Whether to animate tag removals or not.
+            animate: true,
+
+            // The below options are for using a single field instead of several
+            // for our form values.
+            //
+            // When enabled, will use a single hidden field for the form,
+            // rather than one per tag. It will delimit tags in the field
+            // with singleFieldDelimiter.
+            //
+            // The easiest way to use singleField is to just instantiate tag-it
+            // on an INPUT element, in which case singleField is automatically
+            // set to true, and singleFieldNode is set to that element. This 
+            // way, you don't need to fiddle with these options.
+            singleField: false,
+
+            singleFieldDelimiter: ',',
+
+            // Set this to an input DOM node to use an existing form field.
+            // Any text in it will be erased on init. But it will be
+            // populated with the text of tags as they are created,
+            // delimited by singleFieldDelimiter.
+            //
+            // If this is not set, we create an input node for it,
+            // with the name given in settings.fieldName, 
+            // ignoring settings.itemName.
+            singleFieldNode: null,
+
+            // Optionally set a tabindex attribute on the input that gets
+            // created for tag-it.
+            tabIndex: null,
+
+
+            // Event callbacks.
+            onTagAdded  : null,
+            onTagRemoved: null,
+            onTagClicked: null
+        },
+
+
+        _create: function() {
+            // for handling static scoping inside callbacks
+            var that = this;
+
+            // There are 2 kinds of DOM nodes this widget can be instantiated on:
+            //     1. UL, OL, or some element containing either of these.
+            //     2. INPUT, in which case 'singleField' is overridden to true,
+            //        a UL is created and the INPUT is hidden.
+            if (this.element.is('input')) {
+                this.tagList = $('<ul></ul>').insertAfter(this.element);
+                this.options.singleField = true;
+                this.options.singleFieldNode = this.element;
+                this.element.css('display', 'none');
+            } else {
+                this.tagList = this.element.find('ul, ol').andSelf().last();
+            }
+
+            this._tagInput = $('<input type="text" />').addClass('ui-widget-content');
+            if (this.options.tabIndex) {
+                this._tagInput.attr('tabindex', this.options.tabIndex);
+            }
+            if (this.options.placeholderText) {
+                this._tagInput.attr('placeholder', this.options.placeholderText);
+            }
+
+            this.options.tagSource = this.options.tagSource || function(search, showChoices) {
+                var filter = search.term.toLowerCase();
+                var choices = $.grep(this.options.availableTags, function(element) {
+                    // Only match autocomplete options that begin with the search term.
+                    // (Case insensitive.)
+                    return (element.toLowerCase().indexOf(filter) === 0);
+                });
+                showChoices(this._subtractArray(choices, this.assignedTags()));
+            };
+
+            // Bind tagSource callback functions to this context.
+            if ($.isFunction(this.options.tagSource)) {
+                this.options.tagSource = $.proxy(this.options.tagSource, this);
+            }
+
+            this.tagList
+                .addClass('tagit')
+                .addClass('ui-widget ui-widget-content ui-corner-all')
+                // Create the input field.
+                .append($('<li class="tagit-new"></li>').append(this._tagInput))
+                .click(function(e) {
+                    var target = $(e.target);
+                    if (target.hasClass('tagit-label')) {
+                        that._trigger('onTagClicked', e, target.closest('.tagit-choice'));
+                    } else {
+                        // Sets the focus() to the input field, if the user
+                        // clicks anywhere inside the UL. This is needed
+                        // because the input field needs to be of a small size.
+                        that._tagInput.focus();
+                    }
+                });
+
+            // Add existing tags from the list, if any.
+            this.tagList.children('li').each(function() {
+                if (!$(this).hasClass('tagit-new')) {
+                    that.createTag($(this).html(), $(this).attr('class'));
+                    $(this).remove();
+                }
+            });
+
+            // Single field support.
+            if (this.options.singleField) {
+                if (this.options.singleFieldNode) {
+                    // Add existing tags from the input field.
+                    var node = $(this.options.singleFieldNode);
+                    var tags = node.val().split(this.options.singleFieldDelimiter);
+                    node.val('');
+                    $.each(tags, function(index, tag) {
+                        that.createTag(tag);
+                    });
+                } else {
+                    // Create our single field input after our list.
+                    this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
+                }
+            }
+
+            // Events.
+            this._tagInput
+                .keydown(function(event) {
+                    // Backspace is not detected within a keypress, so it must use keydown.
+                    if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') {
+                        var tag = that._lastTag();
+                        if (!that.options.removeConfirmation || tag.hasClass('remove')) {
+                            // When backspace is pressed, the last tag is deleted.
+                            that.removeTag(tag);
+                        } else if (that.options.removeConfirmation) {
+                            tag.addClass('remove ui-state-highlight');
+                        }
+                    } else if (that.options.removeConfirmation) {
+                        that._lastTag().removeClass('remove ui-state-highlight');
+                    }
+
+                    // Comma/Space/Enter are all valid delimiters for new tags,
+                    // except when there is an open quote or if setting allowSpaces = true.
+                    // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught.
+                    if (
+                        event.which == $.ui.keyCode.COMMA ||
+                        event.which == $.ui.keyCode.ENTER ||
+                        (
+                            event.which == $.ui.keyCode.TAB &&
+                            that._tagInput.val() !== ''
+                        ) ||
+                        (
+                            event.which == $.ui.keyCode.SPACE &&
+                            that.options.allowSpaces !== true &&
+                            (
+                                $.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
+                                (
+                                    $.trim(that._tagInput.val()).charAt(0) == '"' &&
+                                    $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' &&
+                                    $.trim(that._tagInput.val()).length - 1 !== 0
+                                )
+                            )
+                        )
+                    ) {
+                        event.preventDefault();
+                        that.createTag(that._cleanedInput());
+
+                        // The autocomplete doesn't close automatically when TAB is pressed.
+                        // So let's ensure that it closes.
+                        that._tagInput.autocomplete('close');
+                    }
+                }).blur(function(e){
+                    // Create a tag when the element loses focus (unless it's empty).
+                    that.createTag(that._cleanedInput());
+                });
+                
+
+            // Autocomplete.
+            if (this.options.availableTags || this.options.tagSource) {
+                this._tagInput.autocomplete({
+                    source: this.options.tagSource,
+                    select: function(event, ui) {
+                        // Delete the last tag if we autocomplete something despite the input being empty
+                        // This happens because the input's blur event causes the tag to be created when
+                        // the user clicks an autocomplete item.
+                        // The only artifact of this is that while the user holds down the mouse button
+                        // on the selected autocomplete item, a tag is shown with the pre-autocompleted text,
+                        // and is changed to the autocompleted text upon mouseup.
+                        if (that._tagInput.val() === '') {
+                            that.removeTag(that._lastTag(), false);
+                        }
+                        that.createTag(ui.item.value);
+                        // Preventing the tag input to be updated with the chosen value.
+                        return false;
+                    }
+                });
+            }
+        },
+
+        _cleanedInput: function() {
+            // Returns the contents of the tag input, cleaned and ready to be passed to createTag
+            return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1'));
+        },
+
+        _lastTag: function() {
+            return this.tagList.children('.tagit-choice:last');
+        },
+
+        assignedTags: function() {
+            // Returns an array of tag string values
+            var that = this;
+            var tags = [];
+            if (this.options.singleField) {
+                tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
+                if (tags[0] === '') {
+                    tags = [];
+                }
+            } else {
+                this.tagList.children('.tagit-choice').each(function() {
+                    tags.push(that.tagLabel(this));
+                });
+            }
+            return tags;
+        },
+
+        _updateSingleTagsField: function(tags) {
+            // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
+            $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter));
+        },
+
+        _subtractArray: function(a1, a2) {
+            var result = [];
+            for (var i = 0; i < a1.length; i++) {
+                if ($.inArray(a1[i], a2) == -1) {
+                    result.push(a1[i]);
+                }
+            }
+            return result;
+        },
+
+        tagLabel: function(tag) {
+            // Returns the tag's string label.
+            if (this.options.singleField) {
+                return $(tag).children('.tagit-label').text();
+            } else {
+                return $(tag).children('input').val();
+            }
+        },
+
+        _isNew: function(value) {
+            var that = this;
+            var isNew = true;
+            this.tagList.children('.tagit-choice').each(function(i) {
+                if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) {
+                    isNew = false;
+                    return false;
+                }
+            });
+            return isNew;
+        },
+
+        _formatStr: function(str) {
+            if (this.options.caseSensitive) {
+                return str;
+            }
+            return $.trim(str.toLowerCase());
+        },
+
+        createTag: function(value, additionalClass) {
+            var that = this;
+            // Automatically trims the value of leading and trailing whitespace.
+            value = $.trim(value);
+
+            if (!this._isNew(value) || value === '') {
+                return false;
+            }
+
+            var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
+
+            // Create tag.
+            var tag = $('<li></li>')
+                .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
+                .addClass(additionalClass)
+                .append(label);
+
+            // Button for removing the tag.
+            var removeTagIcon = $('<span></span>')
+                .addClass('ui-icon ui-icon-close');
+            var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
+                .addClass('tagit-close')
+                .append(removeTagIcon)
+                .click(function(e) {
+                    // Removes a tag when the little 'x' is clicked.
+                    that.removeTag(tag);
+                });
+            tag.append(removeTag);
+
+            // Unless options.singleField is set, each tag has a hidden input field inline.
+            if (this.options.singleField) {
+                var tags = this.assignedTags();
+                tags.push(value);
+                this._updateSingleTagsField(tags);
+            } else {
+                var escapedValue = label.html();
+                tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]" />');
+            }
+
+            this._trigger('onTagAdded', null, tag);
+
+            // Cleaning the input.
+            this._tagInput.val('');
+
+            // insert tag
+            this._tagInput.parent().before(tag);
+        },
+        
+        removeTag: function(tag, animate) {
+            animate = animate || this.options.animate;
+
+            tag = $(tag);
+
+            this._trigger('onTagRemoved', null, tag);
+
+            if (this.options.singleField) {
+                var tags = this.assignedTags();
+                var removedTagLabel = this.tagLabel(tag);
+                tags = $.grep(tags, function(el){
+                    return el != removedTagLabel;
+                });
+                this._updateSingleTagsField(tags);
+            }
+            // Animate the removal.
+            if (animate) {
+                tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){
+                    tag.remove();
+                }).dequeue();
+            } else {
+                tag.remove();
+            }
+        },
+
+        removeAll: function() {
+            // Removes all tags.
+            var that = this;
+            this.tagList.children('.tagit-choice').each(function(index, tag) {
+                that.removeTag(tag, false);
+            });
+        }
+
+    });
+
+})(jQuery);
+
+