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 showChoices(this._subtractArray(choices, this.assignedTags())); 148 }; 149 } 150 151 if (this.options.showAutocompleteOnFocus) { 152 this.tagInput.focus(function(event, ui) { 153 that._showAutocomplete(); 154 }); 155 156 if (typeof this.options.autocomplete.minLength === 'undefined') { 157 this.options.autocomplete.minLength = 0; 158 } 159 } 160 161 // Bind autocomplete.source callback functions to this context. 162 if ($.isFunction(this.options.autocomplete.source)) { 163 this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this); 164 } 165 166 // DEPRECATED. 167 if ($.isFunction(this.options.tagSource)) { 168 this.options.tagSource = $.proxy(this.options.tagSource, this); 169 } 170 171 this.tagList 172 .addClass('tagit') 173 .addClass('ui-widget ui-widget-content ui-corner-all') 174 // Create the input field. 175 .append($('<li class="tagit-new"></li>').append(this.tagInput)) 176 .click(function(e) { 177 var target = $(e.target); 178 if (target.hasClass('tagit-label')) { 179 var tag = target.closest('.tagit-choice'); 180 if (!tag.hasClass('removed')) { 181 that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)}); 182 } 183 } else { 184 // Sets the focus() to the input field, if the user 185 // clicks anywhere inside the UL. This is needed 186 // because the input field needs to be of a small size. 187 that.tagInput.focus(); 188 } 189 }); 190 191 // Single field support. 192 var addedExistingFromSingleFieldNode = false;
193 if (this.options.singleField) { 194 if (this.options.singleFieldNode) { 195 // Add existing tags from the input field. 196 var node = $(this.options.singleFieldNode); 197 var tags = node.val().split(this.options.singleFieldDelimiter); 198 node.val(''); 199 $.each(tags, function(index, tag) { 200 that.createTag(tag, null, true); 201 addedExistingFromSingleFieldNode = true; 202 }); 203 } else { 204 // Create our single field input after our list. 205 this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />'); 206 this.tagList.after(this.options.singleFieldNode); 207 } 208 } 209 210 // Add existing tags from the list, if any. 211 if (!addedExistingFromSingleFieldNode) { 212 this.tagList.children('li').each(function() { 213 if (!$(this).hasClass('tagit-new')) { 214 that.createTag($(this).text(), $(this).attr('class'), true); 215 $(this).remove(); 216 } 217 }); 218 } 219 220 // Events. 221 this.tagInput 222 .keydown(function(event) { 223 // Backspace is not detected within a keypress, so it must use keydown. 224 if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') { 225 var tag = that._lastTag(); 226 if (!that.options.removeConfirmation || tag.hasClass('remove')) { 227 // When backspace is pressed, the last tag is deleted. 228 that.removeTag(tag); 229 } else if (that.options.removeConfirmation) { 230 tag.addClass('remove ui-state-highlight'); 231 } 232 } else if (that.options.removeConfirmation) { 233 that._lastTag().removeClass('remove ui-state-highlight'); 234 } 235 236 // Comma/Space/Enter are all valid delimiters for new tags, 237 // except when there is an open quote or if setting allowSpaces = true. 238 // Tab will also create a tag, unless the tag input is empty, 239 // in which case it isn't caught. 240 if ( 241 // NOTE: modified by VelaEasy on 2013-05-09 242 // Google Chrome returns the same event.keyCode for ',' (comma) and 'ม' (mor-mar in Thai) in 'keydown' event. 243 // And this prevents creating a tag with 'ม', so let's use 'keypress' here to fix this problem. 244 245 /* event.which === $.ui.keyCode.COMMA || */ 246 event.which === $.ui.keyCode.ENTER || 247 ( 248 event.which == $.ui.keyCode.TAB && 249 that.tagInput.val() !== '' 250 ) || 251 ( 252 event.which == $.ui.keyCode.SPACE && 253 that.options.allowSpaces !== true && 254 ( 255 $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || 256 ( 257 $.trim(that.tagInput.val()).charAt(0) == '"' && 258 $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' && 259 $.trim(that.tagInput.val()).length - 1 !== 0 260 ) 261 ) 262 ) 263 ) { 264 // Enter submits the form if there's no text in the input. 265 if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { 266 event.preventDefault(); 267 } 268 269 that.createTag(that._cleanedInput()); 270 271 // The autocomplete doesn't close automatically when TAB is pressed. 272 // So let's ensure that it closes. 273 that.tagInput.autocomplete('close'); 274 } 275 }).keypress(function(event) { 276 // NOTE: modified by VelaEasy on 2013-05-09 277 // Google Chrome returns the same event.keyCode for ',' (comma) and 'ม' (mor-mar in Thai) in 'keydown' event. 278 // And this prevents creating a tag with 'ม', so let's use 'keypress' here to fix this problem. 279 if (String.fromCharCode(event.charCode) == ',') { 280 that.createTag(that._cleanedInput()); 281 event.preventDefault(); 282 } 283 }).blur(function(e){ 284 // Create a tag when the element loses focus. 285 // If autocomplete is enabled and suggestion was clicked, don't add it. 286 if (!that.tagInput.data('autocomplete-open')) { 287 that.createTag(that._cleanedInput()); 288 } 289 }); 290 291 // Autocomplete. 292 if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { 293 var autocompleteOptions = { 294 select: function(event, ui) { 295 that.createTag(ui.item.value); 296 // Preventing the tag input to be updated with the chosen value. 297 return false;
298 } 299 }; 300 $.extend(autocompleteOptions, this.options.autocomplete); 301 302 // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, 303 // while tagSource is left null by default. 304 autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; 305 306 this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) { 307 that.tagInput.data('autocomplete-open', true); 308 }).bind('autocompleteclose', function(event, ui) { 309 that.tagInput.data('autocomplete-open', false) 310 }); 311 } 312 }, 313 314 _cleanedInput: function() { 315 // Returns the contents of the tag input, cleaned and ready to be passed to createTag 316 return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); 317 }, 318 319 _lastTag: function() { 320 return this.tagList.find('.tagit-choice:last:not(.removed)'); 321 }, 322 323 _tags: function() { 324 return this.tagList.find('.tagit-choice:not(.removed)'); 325 }, 326 327 assignedTags: function() { 328 // Returns an array of tag string values 329 var that = this; 330 var tags = []; 331 if (this.options.singleField) { 332 tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); 333 if (tags[0] === '') { 334 tags = []; 335 } 336 } else { 337 this._tags().each(function() { 338 tags.push(that.tagLabel(this)); 339 }); 340 } 341 return tags; 342 }, 343 344 _updateSingleTagsField: function(tags) { 345 // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter 346 $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); 347 }, 348 349 _subtractArray: function(a1, a2) { 350 var result = []; 351 for (var i = 0; i < a1.length; i++) { 352 if ($.inArray(a1[i], a2) == -1) { 353 result.push(a1[i]); 354 } 355 } 356 return result; 357 }, 358 359 tagLabel: function(tag) { 360 // Returns the tag's string label. 361 if (this.options.singleField) { 362 return $(tag).find('.tagit-label:first').text(); 363 } else { 364 return $(tag).find('input:first').val(); 365 } 366 }, 367 368 _showAutocomplete: function() { 369 this.tagInput.autocomplete('search', ''); 370 }, 371 372 _findTagByLabel: function(name) { 373 var that = this; 374 var tag = null; 375 this._tags().each(function(i) { 376 if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { 377 tag = $(this); 378 return false; 379 } 380 }); 381 return tag; 382 }, 383 384 _isNew: function(name) { 385 return !this._findTagByLabel(name); 386 }, 387 388 _formatStr: function(str) { 389 if (this.options.caseSensitive) { 390 return str; 391 } 392 return $.trim(str.toLowerCase()); 393 }, 394 395 _effectExists: function(name) { 396 return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); 397 }, 398 399 createTag: function(value, additionalClass, duringInitialization) { 400 var that = this; 401 402 value = $.trim(value); 403 404 if(this.options.preprocessTag) { 405 value = this.options.preprocessTag(value); 406 } 407 408 if (value === '') { 409 return false; 410 } 411 412 if (!this.options.allowDuplicates && !this._isNew(value)) { 413 var existingTag = this._findTagByLabel(value); 414 if (this._trigger('onTagExists', null, { 415 existingTag: existingTag, 416 duringInitialization: duringInitialization 417 }) !== false) { 418 if (this._effectExists('highlight')) { 419 existingTag.effect('highlight'); 420 } 421 } 422 return false;
423 } 424 425 if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { 426 this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); 427 return false; 428 } 429 430 var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value); 431 432 // Create tag. 433 var tag = $('<li></li>') 434 .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') 435 .addClass(additionalClass) 436 .append(label); 437 438 if (this.options.readOnly){ 439 tag.addClass('tagit-choice-read-only'); 440 } else { 441 tag.addClass('tagit-choice-editable'); 442 // Button for removing the tag. 443 var removeTagIcon = $('<span></span>') 444 .addClass('ui-icon ui-icon-close'); 445 var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X 446 .addClass('tagit-close') 447 .append(removeTagIcon) 448 .click(function(e) { 449 // Removes a tag when the little 'x' is clicked. 450 that.removeTag(tag); 451 }); 452 tag.append(removeTag); 453 } 454 455 // Unless options.singleField is set, each tag has a hidden input field inline. 456 if (!this.options.singleField) { 457 var escapedValue = label.html(); 458 tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />'); 459 } 460 461 if (this._trigger('beforeTagAdded', null, { 462 tag: tag, 463 tagLabel: this.tagLabel(tag), 464 duringInitialization: duringInitialization 465 }) === false) { 466 return; 467 } 468 469 if (this.options.singleField) { 470 var tags = this.assignedTags(); 471 tags.push(value); 472 this._updateSingleTagsField(tags); 473 } 474 475 // DEPRECATED. 476 this._trigger('onTagAdded', null, tag); 477 478 this.tagInput.val(''); 479 480 // Insert tag. 481 this.tagInput.parent().before(tag); 482 483 this._trigger('afterTagAdded', null, { 484 tag: tag, 485 tagLabel: this.tagLabel(tag), 486 duringInitialization: duringInitialization 487 }); 488 489 if (this.options.showAutocompleteOnFocus && !duringInitialization) { 490 setTimeout(function () { that._showAutocomplete(); }, 0); 491 } 492 }, 493 494 removeTag: function(tag, animate) { 495 animate = typeof animate === 'undefined' ? this.options.animate : animate; 496 497 tag = $(tag); 498 499 // DEPRECATED. 500 this._trigger('onTagRemoved', null, tag); 501 502 if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { 503 return; 504 } 505 506 if (this.options.singleField) { 507 var tags = this.assignedTags(); 508 var removedTagLabel = this.tagLabel(tag); 509 tags = $.grep(tags, function(el){ 510 return el != removedTagLabel; 511 }); 512 this._updateSingleTagsField(tags); 513 } 514 515 if (animate) { 516 tag.addClass('removed'); // Excludes this tag from _tags. 517 var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; 518 519 var thisTag = this; 520 hide_args.push(function() { 521 tag.remove(); 522 thisTag._trigger('afterTagRemoved', null, {tag: tag, tagLabel: thisTag.tagLabel(tag)}); 523 }); 524 525 tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); 526 } else { 527 tag.remove(); 528 this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); 529 } 530 531 }, 532 533 removeTagByLabel: function(tagLabel, animate) { 534 var toRemove = this._findTagByLabel(tagLabel); 535 if (!toRemove) { 536 throw "No such tag exists with the name '" + tagLabel + "'"; 537 } 538 this.removeTag(toRemove, animate); 539 }, 540 541 removeAll: function() { 542 // Removes all tags. 543 var that = this; 544 this._tags().each(function(index, tag) { 545 that.removeTag(tag, false); 546 }); 547 } 548 549 }); 550})(jQuery); 551
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.