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.addClass('tagit-hidden-field'); 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 && event.shiftKey === false) || 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.options.autocomplete.autoFocus && that.tagInput.data('autocomplete-open'))) { 270 that.tagInput.autocomplete('close'); 271 that.createTag(that._cleanedInput()); 272 } 273 } 274 }).blur(function(e){
275 // Create a tag when the element loses focus. 276 // If autocomplete is enabled and suggestion was clicked, don't add it. 277 if (!that.tagInput.data('autocomplete-open')) { 278 that.createTag(that._cleanedInput()); 279 } 280 }); 281 282 // Autocomplete. 283 if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { 284 var autocompleteOptions = { 285 select: function(event, ui) { 286 that.createTag(ui.item.value); 287 // Preventing the tag input to be updated with the chosen value. 288 return false; 289 } 290 }; 291 $.extend(autocompleteOptions, this.options.autocomplete); 292 293 // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, 294 // while tagSource is left null by default. 295 autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; 296 297 this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen.tagit', function(event, ui) { 298 that.tagInput.data('autocomplete-open', true); 299 }).bind('autocompleteclose.tagit', function(event, ui) { 300 that.tagInput.data('autocomplete-open', false); 301 }); 302 303 this.tagInput.autocomplete('widget').addClass('tagit-autocomplete'); 304 } 305 }, 306 307 destroy: function() { 308 $.Widget.prototype.destroy.call(this); 309 310 this.element.unbind('.tagit'); 311 this.tagList.unbind('.tagit'); 312 313 this.tagInput.removeData('autocomplete-open'); 314 315 this.tagList.removeClass([ 316 'tagit', 317 'ui-widget', 318 'ui-widget-content', 319 'ui-corner-all', 320 'tagit-hidden-field' 321 ].join(' ')); 322 323 if (this.element.is('input')) { 324 this.element.removeClass('tagit-hidden-field'); 325 this.tagList.remove(); 326 } else { 327 this.element.children('li').each(function() { 328 if ($(this).hasClass('tagit-new')) { 329 $(this).remove(); 330 } else { 331 $(this).removeClass([ 332 'tagit-choice', 333 'ui-widget-content', 334 'ui-state-default', 335 'ui-state-highlight', 336 'ui-corner-all', 337 'remove', 338 'tagit-choice-editable', 339 'tagit-choice-read-only' 340 ].join(' ')); 341 342 $(this).text($(this).children('.tagit-label').text()); 343 } 344 }); 345 346 if (this.singleFieldNode) { 347 this.singleFieldNode.remove(); 348 } 349 } 350 351 return this; 352 }, 353 354 _cleanedInput: function() { 355 // Returns the contents of the tag input, cleaned and ready to be passed to createTag 356 return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); 357 }, 358 359 _lastTag: function() { 360 return this.tagList.find('.tagit-choice:last:not(.removed)'); 361 }, 362 363 _tags: function() { 364 return this.tagList.find('.tagit-choice:not(.removed)'); 365 }, 366 367 assignedTags: function() { 368 // Returns an array of tag string values 369 var that = this; 370 var tags = []; 371 if (this.options.singleField) { 372 tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); 373 if (tags[0] === '') { 374 tags = []; 375 } 376 } else { 377 this._tags().each(function() { 378 tags.push(that.tagLabel(this)); 379 }); 380 } 381 return tags; 382 }, 383 384 _updateSingleTagsField: function(tags) { 385 // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter 386 $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); 387 }, 388 389 _subtractArray: function(a1, a2) { 390 var result = []; 391 for (var i = 0; i < a1.length; i++) { 392 if ($.inArray(a1[i], a2) == -1) { 393 result.push(a1[i]); 394 } 395 } 396 return result; 397 }, 398 399 tagLabel: function(tag) { 400 // Returns the tag's string label. 401 if (this.options.singleField) { 402 return $(tag).find('.tagit-label:first').text(); 403 } else {
404 return $(tag).find('input:first').val(); 405 } 406 }, 407 408 _showAutocomplete: function() { 409 this.tagInput.autocomplete('search', ''); 410 }, 411 412 _findTagByLabel: function(name) { 413 var that = this; 414 var tag = null; 415 this._tags().each(function(i) { 416 if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { 417 tag = $(this); 418 return false; 419 } 420 }); 421 return tag; 422 }, 423 424 _isNew: function(name) { 425 return !this._findTagByLabel(name); 426 }, 427 428 _formatStr: function(str) { 429 if (this.options.caseSensitive) { 430 return str; 431 } 432 return $.trim(str.toLowerCase()); 433 }, 434 435 _effectExists: function(name) { 436 return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); 437 }, 438 439 createTag: function(value, additionalClass, duringInitialization) { 440 var that = this; 441 442 value = $.trim(value); 443 444 if(this.options.preprocessTag) { 445 value = this.options.preprocessTag(value); 446 } 447 448 if (value === '') { 449 return false; 450 } 451 452 if (!this.options.allowDuplicates && !this._isNew(value)) { 453 var existingTag = this._findTagByLabel(value); 454 if (this._trigger('onTagExists', null, { 455 existingTag: existingTag, 456 duringInitialization: duringInitialization 457 }) !== false) { 458 if (this._effectExists('highlight')) { 459 existingTag.effect('highlight'); 460 } 461 } 462 return false; 463 } 464 465 if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { 466 this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); 467 return false; 468 } 469 470 var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value); 471 472 // Create tag. 473 var tag = $('<li></li>') 474 .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') 475 .addClass(additionalClass) 476 .append(label); 477 478 if (this.options.readOnly){ 479 tag.addClass('tagit-choice-read-only'); 480 } else { 481 tag.addClass('tagit-choice-editable'); 482 // Button for removing the tag. 483 var removeTagIcon = $('<span></span>') 484 .addClass('ui-icon ui-icon-close'); 485 var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X 486 .addClass('tagit-close') 487 .append(removeTagIcon) 488 .click(function(e) { 489 // Removes a tag when the little 'x' is clicked. 490 that.removeTag(tag); 491 }); 492 tag.append(removeTag); 493 } 494 495 // Unless options.singleField is set, each tag has a hidden input field inline. 496 if (!this.options.singleField) { 497 var escapedValue = label.html(); 498 tag.append('<input type="hidden" value="' + escapedValue + '" name="' + this.options.fieldName + '" class="tagit-hidden-field" />'); 499 } 500 501 if (this._trigger('beforeTagAdded', null, { 502 tag: tag, 503 tagLabel: this.tagLabel(tag), 504 duringInitialization: duringInitialization 505 }) === false) { 506 return; 507 } 508 509 if (this.options.singleField) { 510 var tags = this.assignedTags(); 511 tags.push(value); 512 this._updateSingleTagsField(tags); 513 } 514 515 // DEPRECATED. 516 this._trigger('onTagAdded', null, tag); 517 518 this.tagInput.val(''); 519 520 // Insert tag. 521 this.tagInput.parent().before(tag); 522 523 this._trigger('afterTagAdded', null, { 524 tag: tag, 525 tagLabel: this.tagLabel(tag), 526 duringInitialization: duringInitialization 527 }); 528 529 if (this.options.showAutocompleteOnFocus && !duringInitialization) { 530 setTimeout(function () { that._showAutocomplete(); }, 0); 531 } 532 }, 533 534 removeTag: function(tag, animate) { 535 animate = typeof animate === 'undefined' ? this.options.animate : animate; 536 537 tag = $(tag); 538 539 // DEPRECATED. 540 this._trigger('onTagRemoved', null, tag); 541 542 if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { 543 return; 544 } 545 546 if (this.options.singleField) { 547 var tags = this.assignedTags(); 548 var removedTagLabel = this.tagLabel(tag); 549 tags = $.grep(tags, function(el){ 550 return el != removedTagLabel; 551 }); 552 this._updateSingleTagsField(tags); 553 } 554 555 if (animate) { 556 tag.addClass('removed'); // Excludes this tag from _tags. 557 var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; 558 559 var thisTag = this; 560 hide_args.push(function() { 561 tag.remove(); 562 thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)}); 563 }); 564 565 tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); 566 } else { 567 tag.remove(); 568 this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); 569 } 570 571 }, 572 573 removeTagByLabel: function(tagLabel, animate) { 574 var toRemove = this._findTagByLabel(tagLabel); 575 if (!toRemove) { 576 throw "No such tag exists with the name '" + tagLabel + "'"; 577 } 578 this.removeTag(toRemove, animate); 579 }, 580 581 removeAll: function() { 582 // Removes all tags. 583 var that = this; 584 this._tags().each(function(index, tag) { 585 that.removeTag(tag, false); 586 }); 587 } 588 589 });
590})(jQuery); 591
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.