integration/lib/tag-it/js/tag-it.js
changeset 6 547b3ddedf7f
equal deleted inserted replaced
2:78f71aa0a477 6:547b3ddedf7f
       
     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             allowDuplicates   : false,
       
    32             caseSensitive     : true,
       
    33             fieldName         : 'tags',
       
    34             placeholderText   : null,   // Sets `placeholder` attr on input field.
       
    35             readOnly          : false,  // Disables editing.
       
    36             removeConfirmation: false,  // Require confirmation to remove tags.
       
    37             tagLimit          : null,   // Max number of tags allowed (null for unlimited).
       
    38 
       
    39             // Used for autocomplete, unless you override `autocomplete.source`.
       
    40             availableTags     : [],
       
    41 
       
    42             // Use to override or add any options to the autocomplete widget.
       
    43             //
       
    44             // By default, autocomplete.source will map to availableTags,
       
    45             // unless overridden.
       
    46             autocomplete: {},
       
    47 
       
    48             // Shows autocomplete before the user even types anything.
       
    49             showAutocompleteOnFocus: false,
       
    50 
       
    51             // When enabled, quotes are unneccesary for inputting multi-word tags.
       
    52             allowSpaces: false,
       
    53 
       
    54             // The below options are for using a single field instead of several
       
    55             // for our form values.
       
    56             //
       
    57             // When enabled, will use a single hidden field for the form,
       
    58             // rather than one per tag. It will delimit tags in the field
       
    59             // with singleFieldDelimiter.
       
    60             //
       
    61             // The easiest way to use singleField is to just instantiate tag-it
       
    62             // on an INPUT element, in which case singleField is automatically
       
    63             // set to true, and singleFieldNode is set to that element. This
       
    64             // way, you don't need to fiddle with these options.
       
    65             singleField: false,
       
    66 
       
    67             // This is just used when preloading data from the field, and for
       
    68             // populating the field with delimited tags as the user adds them.
       
    69             singleFieldDelimiter: ',',
       
    70 
       
    71             // Set this to an input DOM node to use an existing form field.
       
    72             // Any text in it will be erased on init. But it will be
       
    73             // populated with the text of tags as they are created,
       
    74             // delimited by singleFieldDelimiter.
       
    75             //
       
    76             // If this is not set, we create an input node for it,
       
    77             // with the name given in settings.fieldName.
       
    78             singleFieldNode: null,
       
    79 
       
    80             // Whether to animate tag removals or not.
       
    81             animate: true,
       
    82 
       
    83             // Optionally set a tabindex attribute on the input that gets
       
    84             // created for tag-it.
       
    85             tabIndex: null,
       
    86 
       
    87             // Event callbacks.
       
    88             beforeTagAdded      : null,
       
    89             afterTagAdded       : null,
       
    90 
       
    91             beforeTagRemoved    : null,
       
    92             afterTagRemoved     : null,
       
    93 
       
    94             onTagClicked        : null,
       
    95             onTagLimitExceeded  : null,
       
    96 
       
    97 
       
    98             // DEPRECATED:
       
    99             //
       
   100             // /!\ These event callbacks are deprecated and WILL BE REMOVED at some
       
   101             // point in the future. They're here for backwards-compatibility.
       
   102             // Use the above before/after event callbacks instead.
       
   103             onTagAdded  : null,
       
   104             onTagRemoved: null,
       
   105             // `autocomplete.source` is the replacement for tagSource.
       
   106             tagSource: null
       
   107             // Do not use the above deprecated options.
       
   108         },
       
   109 
       
   110         _create: function() {
       
   111             // for handling static scoping inside callbacks
       
   112             var that = this;
       
   113 
       
   114             // There are 2 kinds of DOM nodes this widget can be instantiated on:
       
   115             //     1. UL, OL, or some element containing either of these.
       
   116             //     2. INPUT, in which case 'singleField' is overridden to true,
       
   117             //        a UL is created and the INPUT is hidden.
       
   118             if (this.element.is('input')) {
       
   119                 this.tagList = $('<ul></ul>').insertAfter(this.element);
       
   120                 this.options.singleField = true;
       
   121                 this.options.singleFieldNode = this.element;
       
   122                 this.element.css('display', 'none');
       
   123             } else {
       
   124                 this.tagList = this.element.find('ul, ol').andSelf().last();
       
   125             }
       
   126 
       
   127             this.tagInput = $('<input type="text" />').addClass('ui-widget-content');
       
   128 
       
   129             if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled');
       
   130 
       
   131             if (this.options.tabIndex) {
       
   132                 this.tagInput.attr('tabindex', this.options.tabIndex);
       
   133             }
       
   134 
       
   135             if (this.options.placeholderText) {
       
   136                 this.tagInput.attr('placeholder', this.options.placeholderText);
       
   137             }
       
   138 
       
   139             if (!this.options.autocomplete.source) {
       
   140                 this.options.autocomplete.source = function(search, showChoices) {
       
   141                     var filter = search.term.toLowerCase();
       
   142                     var choices = $.grep(this.options.availableTags, function(element) {
       
   143                         // Only match autocomplete options that begin with the search term.
       
   144                         // (Case insensitive.)
       
   145                         return (element.toLowerCase().indexOf(filter) === 0);
       
   146                     });
       
   147                     if (!this.options.allowDuplicates) {
       
   148                         choices = this._subtractArray(choices, this.assignedTags());
       
   149                     }
       
   150                     showChoices(choices);
       
   151                 };
       
   152             }
       
   153 
       
   154             if (this.options.showAutocompleteOnFocus) {
       
   155                 this.tagInput.focus(function(event, ui) {
       
   156                     that._showAutocomplete();
       
   157                 });
       
   158 
       
   159                 if (typeof this.options.autocomplete.minLength === 'undefined') {
       
   160                     this.options.autocomplete.minLength = 0;
       
   161                 }
       
   162             }
       
   163 
       
   164             // Bind autocomplete.source callback functions to this context.
       
   165             if ($.isFunction(this.options.autocomplete.source)) {
       
   166                 this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this);
       
   167             }
       
   168 
       
   169             // DEPRECATED.
       
   170             if ($.isFunction(this.options.tagSource)) {
       
   171                 this.options.tagSource = $.proxy(this.options.tagSource, this);
       
   172             }
       
   173 
       
   174             this.tagList
       
   175                 .addClass('tagit')
       
   176                 .addClass('ui-widget ui-widget-content ui-corner-all')
       
   177                 // Create the input field.
       
   178                 .append($('<li class="tagit-new"></li>').append(this.tagInput))
       
   179                 .click(function(e) {
       
   180                     var target = $(e.target);
       
   181                     if (target.hasClass('tagit-label')) {
       
   182                         var tag = target.closest('.tagit-choice');
       
   183                         if (!tag.hasClass('removed')) {
       
   184                             that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)});
       
   185                         }
       
   186                     } else {
       
   187                         // Sets the focus() to the input field, if the user
       
   188                         // clicks anywhere inside the UL. This is needed
       
   189                         // because the input field needs to be of a small size.
       
   190                         that.tagInput.focus();
       
   191                     }
       
   192                 });
       
   193 
       
   194             // Single field support.
       
   195             var addedExistingFromSingleFieldNode = false;
       
   196             if (this.options.singleField) {
       
   197                 if (this.options.singleFieldNode) {
       
   198                     // Add existing tags from the input field.
       
   199                     var node = $(this.options.singleFieldNode);
       
   200                     var tags = node.val().split(this.options.singleFieldDelimiter);
       
   201                     node.val('');
       
   202                     $.each(tags, function(index, tag) {
       
   203                         that.createTag(tag, null, true);
       
   204                         addedExistingFromSingleFieldNode = true;
       
   205                     });
       
   206                 } else {
       
   207                     // Create our single field input after our list.
       
   208                     this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />');
       
   209                     this.tagList.after(this.options.singleFieldNode);
       
   210                 }
       
   211             }
       
   212 
       
   213             // Add existing tags from the list, if any.
       
   214             if (!addedExistingFromSingleFieldNode) {
       
   215                 this.tagList.children('li').each(function() {
       
   216                     if (!$(this).hasClass('tagit-new')) {
       
   217                         that.createTag($(this).text(), $(this).attr('class'), true);
       
   218                         $(this).remove();
       
   219                     }
       
   220                 });
       
   221             }
       
   222 
       
   223             // Events.
       
   224             this.tagInput
       
   225                 .keydown(function(event) {
       
   226                     // Backspace is not detected within a keypress, so it must use keydown.
       
   227                     if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') {
       
   228                         var tag = that._lastTag();
       
   229                         if (!that.options.removeConfirmation || tag.hasClass('remove')) {
       
   230                             // When backspace is pressed, the last tag is deleted.
       
   231                             that.removeTag(tag);
       
   232                         } else if (that.options.removeConfirmation) {
       
   233                             tag.addClass('remove ui-state-highlight');
       
   234                         }
       
   235                     } else if (that.options.removeConfirmation) {
       
   236                         that._lastTag().removeClass('remove ui-state-highlight');
       
   237                     }
       
   238 
       
   239                     // Comma/Space/Enter are all valid delimiters for new tags,
       
   240                     // except when there is an open quote or if setting allowSpaces = true.
       
   241                     // Tab will also create a tag, unless the tag input is empty,
       
   242                     // in which case it isn't caught.
       
   243                     if (
       
   244                         event.which === $.ui.keyCode.COMMA ||
       
   245                         event.which === $.ui.keyCode.ENTER ||
       
   246                         (
       
   247                             event.which == $.ui.keyCode.TAB &&
       
   248                             that.tagInput.val() !== ''
       
   249                         ) ||
       
   250                         (
       
   251                             event.which == $.ui.keyCode.SPACE &&
       
   252                             that.options.allowSpaces !== true &&
       
   253                             (
       
   254                                 $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' ||
       
   255                                 (
       
   256                                     $.trim(that.tagInput.val()).charAt(0) == '"' &&
       
   257                                     $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' &&
       
   258                                     $.trim(that.tagInput.val()).length - 1 !== 0
       
   259                                 )
       
   260                             )
       
   261                         )
       
   262                     ) {
       
   263                         // Enter submits the form if there's no text in the input.
       
   264                         if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) {
       
   265                             event.preventDefault();
       
   266                         }
       
   267 
       
   268                         // Autocomplete will create its own tag from a selection and close automatically.
       
   269                         if (!that.tagInput.data('autocomplete-open')) {
       
   270                             that.createTag(that._cleanedInput());
       
   271                         }
       
   272                     }
       
   273                 }).blur(function(e){
       
   274                     // Create a tag when the element loses focus.
       
   275                     // If autocomplete is enabled and suggestion was clicked, don't add it.
       
   276                     if (!that.tagInput.data('autocomplete-open')) {
       
   277                         that.createTag(that._cleanedInput());
       
   278                     }
       
   279                 });
       
   280 
       
   281             // Autocomplete.
       
   282             if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) {
       
   283                 var autocompleteOptions = {
       
   284                     select: function(event, ui) {
       
   285                         that.createTag(ui.item.value);
       
   286                         // Preventing the tag input to be updated with the chosen value.
       
   287                         return false;
       
   288                     }
       
   289                 };
       
   290                 $.extend(autocompleteOptions, this.options.autocomplete);
       
   291 
       
   292                 // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default,
       
   293                 // while tagSource is left null by default.
       
   294                 autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source;
       
   295 
       
   296                 this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) {
       
   297                     that.tagInput.data('autocomplete-open', true);
       
   298                 }).bind('autocompleteclose', function(event, ui) {
       
   299                     that.tagInput.data('autocomplete-open', false)
       
   300                 });
       
   301             }
       
   302         },
       
   303 
       
   304         _cleanedInput: function() {
       
   305             // Returns the contents of the tag input, cleaned and ready to be passed to createTag
       
   306             return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1'));
       
   307         },
       
   308 
       
   309         _lastTag: function() {
       
   310             return this.tagList.find('.tagit-choice:last:not(.removed)');
       
   311         },
       
   312 
       
   313         _tags: function() {
       
   314             return this.tagList.find('.tagit-choice:not(.removed)');
       
   315         },
       
   316 
       
   317         assignedTags: function() {
       
   318             // Returns an array of tag string values
       
   319             var that = this;
       
   320             var tags = [];
       
   321             if (this.options.singleField) {
       
   322                 tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter);
       
   323                 if (tags[0] === '') {
       
   324                     tags = [];
       
   325                 }
       
   326             } else {
       
   327                 this._tags().each(function() {
       
   328                     tags.push(that.tagLabel(this));
       
   329                 });
       
   330             }
       
   331             return tags;
       
   332         },
       
   333 
       
   334         _updateSingleTagsField: function(tags) {
       
   335             // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter
       
   336             $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change');
       
   337         },
       
   338 
       
   339         _subtractArray: function(a1, a2) {
       
   340             var result = [];
       
   341             for (var i = 0; i < a1.length; i++) {
       
   342                 if ($.inArray(a1[i], a2) == -1) {
       
   343                     result.push(a1[i]);
       
   344                 }
       
   345             }
       
   346             return result;
       
   347         },
       
   348 
       
   349         tagLabel: function(tag) {
       
   350             // Returns the tag's string label.
       
   351             if (this.options.singleField) {
       
   352                 return $(tag).find('.tagit-label:first').text();
       
   353             } else {
       
   354                 return $(tag).find('input:first').val();
       
   355             }
       
   356         },
       
   357 
       
   358         _showAutocomplete: function() {
       
   359             this.tagInput.autocomplete('search', '');
       
   360         },
       
   361 
       
   362         _findTagByLabel: function(name) {
       
   363             var that = this;
       
   364             var tag = null;
       
   365             this._tags().each(function(i) {
       
   366                 if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) {
       
   367                     tag = $(this);
       
   368                     return false;
       
   369                 }
       
   370             });
       
   371             return tag;
       
   372         },
       
   373 
       
   374         _isNew: function(name) {
       
   375             return !this._findTagByLabel(name);
       
   376         },
       
   377 
       
   378         _formatStr: function(str) {
       
   379             if (this.options.caseSensitive) {
       
   380                 return str;
       
   381             }
       
   382             return $.trim(str.toLowerCase());
       
   383         },
       
   384 
       
   385         _effectExists: function(name) {
       
   386             return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name])));
       
   387         },
       
   388 
       
   389         createTag: function(value, additionalClass, duringInitialization) {
       
   390             var that = this;
       
   391 
       
   392             value = $.trim(value);
       
   393 
       
   394             if(this.options.preprocessTag) {
       
   395                 value = this.options.preprocessTag(value);
       
   396             }
       
   397 
       
   398             if (value === '') {
       
   399                 return false;
       
   400             }
       
   401 
       
   402             if (!this.options.allowDuplicates && !this._isNew(value)) {
       
   403                 var existingTag = this._findTagByLabel(value);
       
   404                 if (this._trigger('onTagExists', null, {
       
   405                     existingTag: existingTag,
       
   406                     duringInitialization: duringInitialization
       
   407                 }) !== false) {
       
   408                     if (this._effectExists('highlight')) {
       
   409                         existingTag.effect('highlight');
       
   410                     }
       
   411                 }
       
   412                 return false;
       
   413             }
       
   414 
       
   415             if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) {
       
   416                 this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization});
       
   417                 return false;
       
   418             }
       
   419 
       
   420             var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value);
       
   421 
       
   422             // Create tag.
       
   423             var tag = $('<li></li>')
       
   424                 .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all')
       
   425                 .addClass(additionalClass)
       
   426                 .append(label);
       
   427 
       
   428             if (this.options.readOnly){
       
   429                 tag.addClass('tagit-choice-read-only');
       
   430             } else {
       
   431                 tag.addClass('tagit-choice-editable');
       
   432                 // Button for removing the tag.
       
   433                 var removeTagIcon = $('<span></span>')
       
   434                     .addClass('ui-icon ui-icon-close');
       
   435                 var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X
       
   436                     .addClass('tagit-close')
       
   437                     .append(removeTagIcon)
       
   438                     .click(function(e) {
       
   439                         // Removes a tag when the little 'x' is clicked.
       
   440                         that.removeTag(tag);
       
   441                     });
       
   442                 tag.append(removeTag);
       
   443             }
       
   444 
       
   445             // Unless options.singleField is set, each tag has a hidden input field inline.
       
   446             if (!this.options.singleField) {
       
   447                 var escapedValue = label.html();
       
   448                 tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />');
       
   449             }
       
   450 
       
   451             if (this._trigger('beforeTagAdded', null, {
       
   452                 tag: tag,
       
   453                 tagLabel: this.tagLabel(tag),
       
   454                 duringInitialization: duringInitialization
       
   455             }) === false) {
       
   456                 return;
       
   457             }
       
   458 
       
   459             if (this.options.singleField) {
       
   460                 var tags = this.assignedTags();
       
   461                 tags.push(value);
       
   462                 this._updateSingleTagsField(tags);
       
   463             }
       
   464 
       
   465             // DEPRECATED.
       
   466             this._trigger('onTagAdded', null, tag);
       
   467 
       
   468             this.tagInput.val('');
       
   469 
       
   470             // Insert tag.
       
   471             this.tagInput.parent().before(tag);
       
   472 
       
   473             this._trigger('afterTagAdded', null, {
       
   474                 tag: tag,
       
   475                 tagLabel: this.tagLabel(tag),
       
   476                 duringInitialization: duringInitialization
       
   477             });
       
   478 
       
   479             if (this.options.showAutocompleteOnFocus && !duringInitialization) {
       
   480                 setTimeout(function () { that._showAutocomplete(); }, 0);
       
   481             }
       
   482         },
       
   483 
       
   484         removeTag: function(tag, animate) {
       
   485             animate = typeof animate === 'undefined' ? this.options.animate : animate;
       
   486 
       
   487             tag = $(tag);
       
   488 
       
   489             // DEPRECATED.
       
   490             this._trigger('onTagRemoved', null, tag);
       
   491 
       
   492             if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) {
       
   493                 return;
       
   494             }
       
   495 
       
   496             if (this.options.singleField) {
       
   497                 var tags = this.assignedTags();
       
   498                 var removedTagLabel = this.tagLabel(tag);
       
   499                 tags = $.grep(tags, function(el){
       
   500                     return el != removedTagLabel;
       
   501                 });
       
   502                 this._updateSingleTagsField(tags);
       
   503             }
       
   504 
       
   505             if (animate) {
       
   506                 tag.addClass('removed'); // Excludes this tag from _tags.
       
   507                 var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast'];
       
   508 
       
   509                 var thisTag = this;
       
   510                 hide_args.push(function() {
       
   511                     tag.remove();
       
   512                     thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)});
       
   513                 });
       
   514 
       
   515                 tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue();
       
   516             } else {
       
   517                 tag.remove();
       
   518                 this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)});
       
   519             }
       
   520 
       
   521         },
       
   522 
       
   523         removeTagByLabel: function(tagLabel, animate) {
       
   524             var toRemove = this._findTagByLabel(tagLabel);
       
   525             if (!toRemove) {
       
   526                 throw "No such tag exists with the name '" + tagLabel + "'";
       
   527             }
       
   528             this.removeTag(toRemove, animate);
       
   529         },
       
   530 
       
   531         removeAll: function() {
       
   532             // Removes all tags.
       
   533             var that = this;
       
   534             this._tags().each(function(index, tag) {
       
   535                 that.removeTag(tag, false);
       
   536             });
       
   537         }
       
   538 
       
   539     });
       
   540 })(jQuery);
       
   541