/* * 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, // 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 = $('').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 = $('').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(that.options.availableTags, function(element) { // Only match autocomplete options that begin with the search term. // (Case insensitive.) return (element.toLowerCase().indexOf(filter) === 0); }); showChoices(that._subtractArray(choices, that.assignedTags())); }; this.tagList .addClass('tagit') .addClass('ui-widget ui-widget-content ui-corner-all') // Create the input field. .append($('
  • ').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(''); } } // 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){ //If autocomplete is enabled and suggestion was clicked, don't add it if (that.options.tagSource && that._tagInput.data('autocomplete-open')) { that._cleanedInput(); } else { that.createTag(that._cleanedInput()); } }); // Autocomplete. if (this.options.availableTags || this.options.tagSource) { this._tagInput.autocomplete({ source: this.options.tagSource, open: function(){that._tagInput.data('autocomplete-open', true)}, close: function(){that._tagInput.data('autocomplete-open', false)}, select: function(event, ui) { 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; } }); return isNew; }, _formatStr: function(str) { if (this.options.caseSensitive) { return str; } return $.trim(str.toLowerCase()); }, createTag: function(value, additionalClass) { 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 ? '' : '').text(value); // Create tag. var tag = $('
  • ') .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') .addClass(additionalClass) .append(label); // Button for removing the tag. var removeTagIcon = $('') .addClass('ui-icon ui-icon-close'); var removeTag = $('\xd7') // \xd7 is an X .addClass('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(''); } this._trigger('onTagAdded', null, tag); // Cleaning the input. this._tagInput.val(''); // insert tag this._tagInput.parent().before(tag); }, removeTag: function(tag, animate) { if (typeof animate === 'undefined') { animate = true; } 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(); } this._trigger('onTagFinishRemoved', null, tag); }, removeAll: function() { // Removes all tags. Takes an optional `animate` argument. var that = this; this.tagList.children('.tagit-choice').each(function(index, tag) { that.removeTag(tag, false); }); } }); })(jQuery);