integration/lib/tag-it.js
changeset 41 3ec2343f2b85
equal deleted inserted replaced
40:76efa2333f31 41:3ec2343f2b85
       
     1 /*
       
     2 * jQuery UI Tag-it!
       
     3 *
       
     4 * @version v2.0 (06/2011)
       
     5 *
       
     6 * Copyright 2011, Levy Carneiro Jr.
       
     7 * Released under the MIT license.
       
     8 * http://aehlke.github.com/tag-it/LICENSE
       
     9 *
       
    10 * Homepage:
       
    11 *   http://aehlke.github.com/tag-it/
       
    12 *
       
    13 * Authors:
       
    14 *   Levy Carneiro Jr.
       
    15 *   Martin Rehfeld
       
    16 *   Tobias Schmidt
       
    17 *   Skylar Challand
       
    18 *   Alex Ehlke
       
    19 *
       
    20 * Maintainer:
       
    21 *   Alex Ehlke - Twitter: @aehlke
       
    22 *
       
    23 * Dependencies:
       
    24 *   jQuery v1.4+
       
    25 *   jQuery UI v1.8+
       
    26 */
       
    27 (function($) {
       
    28 
       
    29     $.widget('ui.tagit', {
       
    30         options: {
       
    31             itemName          : 'item',
       
    32             fieldName         : 'tags',
       
    33             availableTags     : [],
       
    34             tagSource         : null,
       
    35             removeConfirmation: false,
       
    36             caseSensitive     : true,
       
    37             placeholderText   : null,
       
    38 
       
    39             // When enabled, quotes are not neccesary
       
    40             // for inputting multi-word tags.
       
    41             allowSpaces: false,
       
    42 
       
    43             // Whether to animate tag removals or not.
       
    44             animate: true,
       
    45 
       
    46             // The below options are for using a single field instead of several
       
    47             // for our form values.
       
    48             //
       
    49             // When enabled, will use a single hidden field for the form,
       
    50             // rather than one per tag. It will delimit tags in the field
       
    51             // with singleFieldDelimiter.
       
    52             //
       
    53             // The easiest way to use singleField is to just instantiate tag-it
       
    54             // on an INPUT element, in which case singleField is automatically
       
    55             // set to true, and singleFieldNode is set to that element. This 
       
    56             // way, you don't need to fiddle with these options.
       
    57             singleField: false,
       
    58 
       
    59             singleFieldDelimiter: ',',
       
    60 
       
    61             // Set this to an input DOM node to use an existing form field.
       
    62             // Any text in it will be erased on init. But it will be
       
    63             // populated with the text of tags as they are created,
       
    64             // delimited by singleFieldDelimiter.
       
    65             //
       
    66             // If this is not set, we create an input node for it,
       
    67             // with the name given in settings.fieldName, 
       
    68             // ignoring settings.itemName.
       
    69             singleFieldNode: null,
       
    70 
       
    71             // Optionally set a tabindex attribute on the input that gets
       
    72             // created for tag-it.
       
    73             tabIndex: null,
       
    74 
       
    75 
       
    76             // Event callbacks.
       
    77             onTagAdded  : null,
       
    78             onTagRemoved: null,
       
    79             onTagClicked: null
       
    80         },
       
    81 
       
    82 
       
    83         _create: function() {
       
    84             // for handling static scoping inside callbacks
       
    85             var that = this;
       
    86 
       
    87             // There are 2 kinds of DOM nodes this widget can be instantiated on:
       
    88             //     1. UL, OL, or some element containing either of these.
       
    89             //     2. INPUT, in which case 'singleField' is overridden to true,
       
    90             //        a UL is created and the INPUT is hidden.
       
    91             if (this.element.is('input')) {
       
    92                 this.tagList = $('<ul></ul>').insertAfter(this.element);
       
    93                 this.options.singleField = true;
       
    94                 this.options.singleFieldNode = this.element;
       
    95                 this.element.css('display', 'none');
       
    96             } else {
       
    97                 this.tagList = this.element.find('ul, ol').andSelf().last();
       
    98             }
       
    99 
       
   100             this._tagInput = $('<input type="text" />').addClass('ui-widget-content');
       
   101             if (this.options.tabIndex) {
       
   102                 this._tagInput.attr('tabindex', this.options.tabIndex);
       
   103             }
       
   104             if (this.options.placeholderText) {
       
   105                 this._tagInput.attr('placeholder', this.options.placeholderText);
       
   106             }
       
   107 
       
   108             this.options.tagSource = this.options.tagSource || function(search, showChoices) {
       
   109                 var filter = search.term.toLowerCase();
       
   110                 var choices = $.grep(this.options.availableTags, function(element) {
       
   111                     // Only match autocomplete options that begin with the search term.
       
   112                     // (Case insensitive.)
       
   113                     return (element.toLowerCase().indexOf(filter) === 0);
       
   114                 });
       
   115                 showChoices(this._subtractArray(choices, this.assignedTags()));
       
   116             };
       
   117 
       
   118             // Bind tagSource callback functions to this context.
       
   119             if ($.isFunction(this.options.tagSource)) {
       
   120                 this.options.tagSource = $.proxy(this.options.tagSource, this);
       
   121             }
       
   122 
       
   123             this.tagList
       
   124                 .addClass('tagit')
       
   125                 .addClass('ui-widget ui-widget-content ui-corner-all')
       
   126                 // Create the input field.
       
   127                 .append($('<li class="tagit-new"></li>').append(this._tagInput))
       
   128                 .click(function(e) {
       
   129                     var target = $(e.target);
       
   130                     if (target.hasClass('tagit-label')) {
       
   131                         that._trigger('onTagClicked', e, target.closest('.tagit-choice'));
       
   132                     } else {
       
   133                         // Sets the focus() to the input field, if the user
       
   134                         // clicks anywhere inside the UL. This is needed
       
   135                         // because the input field needs to be of a small size.
       
   136                         that._tagInput.focus();
       
   137                     }
       
   138                 });
       
   139 
       
   140             // Add existing tags from the list, if any.
       
   141             this.tagList.children('li').each(function() {
       
   142                 if (!$(this).hasClass('tagit-new')) {
       
   143                     that.createTag($(this).html(), $(this).attr('class'));
       
   144                     $(this).remove();
       
   145                 }
       
   146             });
       
   147 
       
   148             // Single field support.
       
   149             if (this.options.singleField) {
       
   150                 if (this.options.singleFieldNode) {
       
   151                     // Add existing tags from the input field.
       
   152                     var node = $(this.options.singleFieldNode);
       
   153                     var tags = node.val().split(this.options.singleFieldDelimiter);
       
   154                     node.val('');
       
   155                     $.each(tags, function(index, tag) {
       
   156                         that.createTag(tag);
       
   157                     });
       
   158                 } else {
       
   159                     // Create our single field input after our list.
       
   160                     this.options.singleFieldNode = this.tagList.after('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
       
   161                 }
       
   162             }
       
   163 
       
   164             // Events.
       
   165             this._tagInput
       
   166                 .keydown(function(event) {
       
   167                     // Backspace is not detected within a keypress, so it must use keydown.
       
   168                     if (event.which == $.ui.keyCode.BACKSPACE && that._tagInput.val() === '') {
       
   169                         var tag = that._lastTag();
       
   170                         if (!that.options.removeConfirmation || tag.hasClass('remove')) {
       
   171                             // When backspace is pressed, the last tag is deleted.
       
   172                             that.removeTag(tag);
       
   173                         } else if (that.options.removeConfirmation) {
       
   174                             tag.addClass('remove ui-state-highlight');
       
   175                         }
       
   176                     } else if (that.options.removeConfirmation) {
       
   177                         that._lastTag().removeClass('remove ui-state-highlight');
       
   178                     }
       
   179 
       
   180                     // Comma/Space/Enter are all valid delimiters for new tags,
       
   181                     // except when there is an open quote or if setting allowSpaces = true.
       
   182                     // Tab will also create a tag, unless the tag input is empty, in which case it isn't caught.
       
   183                     if (
       
   184                         event.which == $.ui.keyCode.COMMA ||
       
   185                         event.which == $.ui.keyCode.ENTER ||
       
   186                         (
       
   187                             event.which == $.ui.keyCode.TAB &&
       
   188                             that._tagInput.val() !== ''
       
   189                         ) ||
       
   190                         (
       
   191                             event.which == $.ui.keyCode.SPACE &&
       
   192                             that.options.allowSpaces !== true &&
       
   193                             (
       
   194                                 $.trim(that._tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
       
   195                                 (
       
   196                                     $.trim(that._tagInput.val()).charAt(0) == '"' &&
       
   197                                     $.trim(that._tagInput.val()).charAt($.trim(that._tagInput.val()).length - 1) == '"' &&
       
   198                                     $.trim(that._tagInput.val()).length - 1 !== 0
       
   199                                 )
       
   200                             )
       
   201                         )
       
   202                     ) {
       
   203                         event.preventDefault();
       
   204                         that.createTag(that._cleanedInput());
       
   205 
       
   206                         // The autocomplete doesn't close automatically when TAB is pressed.
       
   207                         // So let's ensure that it closes.
       
   208                         that._tagInput.autocomplete('close');
       
   209                     }
       
   210                 }).blur(function(e){
       
   211                     // Create a tag when the element loses focus (unless it's empty).
       
   212                     that.createTag(that._cleanedInput());
       
   213                 });
       
   214                 
       
   215 
       
   216             // Autocomplete.
       
   217             if (this.options.availableTags || this.options.tagSource) {
       
   218                 this._tagInput.autocomplete({
       
   219                     source: this.options.tagSource,
       
   220                     select: function(event, ui) {
       
   221                         // Delete the last tag if we autocomplete something despite the input being empty
       
   222                         // This happens because the input's blur event causes the tag to be created when
       
   223                         // the user clicks an autocomplete item.
       
   224                         // The only artifact of this is that while the user holds down the mouse button
       
   225                         // on the selected autocomplete item, a tag is shown with the pre-autocompleted text,
       
   226                         // and is changed to the autocompleted text upon mouseup.
       
   227                         if (that._tagInput.val() === '') {
       
   228                             that.removeTag(that._lastTag(), false);
       
   229                         }
       
   230                         that.createTag(ui.item.value);
       
   231                         // Preventing the tag input to be updated with the chosen value.
       
   232                         return false;
       
   233                     }
       
   234                 });
       
   235             }
       
   236         },
       
   237 
       
   238         _cleanedInput: function() {
       
   239             // Returns the contents of the tag input, cleaned and ready to be passed to createTag
       
   240             return $.trim(this._tagInput.val().replace(/^"(.*)"$/, '$1'));
       
   241         },
       
   242 
       
   243         _lastTag: function() {
       
   244             return this.tagList.children('.tagit-choice:last');
       
   245         },
       
   246 
       
   247         assignedTags: function() {
       
   248             // Returns an array of tag string values
       
   249             var that = this;
       
   250             var tags = [];
       
   251             if (this.options.singleField) {
       
   252                 tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
       
   253                 if (tags[0] === '') {
       
   254                     tags = [];
       
   255                 }
       
   256             } else {
       
   257                 this.tagList.children('.tagit-choice').each(function() {
       
   258                     tags.push(that.tagLabel(this));
       
   259                 });
       
   260             }
       
   261             return tags;
       
   262         },
       
   263 
       
   264         _updateSingleTagsField: function(tags) {
       
   265             // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
       
   266             $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter));
       
   267         },
       
   268 
       
   269         _subtractArray: function(a1, a2) {
       
   270             var result = [];
       
   271             for (var i = 0; i < a1.length; i++) {
       
   272                 if ($.inArray(a1[i], a2) == -1) {
       
   273                     result.push(a1[i]);
       
   274                 }
       
   275             }
       
   276             return result;
       
   277         },
       
   278 
       
   279         tagLabel: function(tag) {
       
   280             // Returns the tag's string label.
       
   281             if (this.options.singleField) {
       
   282                 return $(tag).children('.tagit-label').text();
       
   283             } else {
       
   284                 return $(tag).children('input').val();
       
   285             }
       
   286         },
       
   287 
       
   288         _isNew: function(value) {
       
   289             var that = this;
       
   290             var isNew = true;
       
   291             this.tagList.children('.tagit-choice').each(function(i) {
       
   292                 if (that._formatStr(value) == that._formatStr(that.tagLabel(this))) {
       
   293                     isNew = false;
       
   294                     return false;
       
   295                 }
       
   296             });
       
   297             return isNew;
       
   298         },
       
   299 
       
   300         _formatStr: function(str) {
       
   301             if (this.options.caseSensitive) {
       
   302                 return str;
       
   303             }
       
   304             return $.trim(str.toLowerCase());
       
   305         },
       
   306 
       
   307         createTag: function(value, additionalClass) {
       
   308             var that = this;
       
   309             // Automatically trims the value of leading and trailing whitespace.
       
   310             value = $.trim(value);
       
   311 
       
   312             if (!this._isNew(value) || value === '') {
       
   313                 return false;
       
   314             }
       
   315 
       
   316             var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
       
   317 
       
   318             // Create tag.
       
   319             var tag = $('<li></li>')
       
   320                 .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
       
   321                 .addClass(additionalClass)
       
   322                 .append(label);
       
   323 
       
   324             // Button for removing the tag.
       
   325             var removeTagIcon = $('<span></span>')
       
   326                 .addClass('ui-icon ui-icon-close');
       
   327             var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
       
   328                 .addClass('tagit-close')
       
   329                 .append(removeTagIcon)
       
   330                 .click(function(e) {
       
   331                     // Removes a tag when the little 'x' is clicked.
       
   332                     that.removeTag(tag);
       
   333                 });
       
   334             tag.append(removeTag);
       
   335 
       
   336             // Unless options.singleField is set, each tag has a hidden input field inline.
       
   337             if (this.options.singleField) {
       
   338                 var tags = this.assignedTags();
       
   339                 tags.push(value);
       
   340                 this._updateSingleTagsField(tags);
       
   341             } else {
       
   342                 var escapedValue = label.html();
       
   343                 tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.itemName + '[' + this.options.fieldName + '][]" />');
       
   344             }
       
   345 
       
   346             this._trigger('onTagAdded', null, tag);
       
   347 
       
   348             // Cleaning the input.
       
   349             this._tagInput.val('');
       
   350 
       
   351             // insert tag
       
   352             this._tagInput.parent().before(tag);
       
   353         },
       
   354         
       
   355         removeTag: function(tag, animate) {
       
   356             animate = animate || this.options.animate;
       
   357 
       
   358             tag = $(tag);
       
   359 
       
   360             this._trigger('onTagRemoved', null, tag);
       
   361 
       
   362             if (this.options.singleField) {
       
   363                 var tags = this.assignedTags();
       
   364                 var removedTagLabel = this.tagLabel(tag);
       
   365                 tags = $.grep(tags, function(el){
       
   366                     return el != removedTagLabel;
       
   367                 });
       
   368                 this._updateSingleTagsField(tags);
       
   369             }
       
   370             // Animate the removal.
       
   371             if (animate) {
       
   372                 tag.fadeOut('fast').hide('blind', {direction: 'horizontal'}, 'fast', function(){
       
   373                     tag.remove();
       
   374                 }).dequeue();
       
   375             } else {
       
   376                 tag.remove();
       
   377             }
       
   378         },
       
   379 
       
   380         removeAll: function() {
       
   381             // Removes all tags.
       
   382             var that = this;
       
   383             this.tagList.children('.tagit-choice').each(function(index, tag) {
       
   384                 that.removeTag(tag, false);
       
   385             });
       
   386         }
       
   387 
       
   388     });
       
   389 
       
   390 })(jQuery);
       
   391 
       
   392