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