1/* 2 * bootstrap-tagsinput v0.8.0 3 * 4 */ 5 6(function ($) { 7 "use strict"; 8 9 var defaultOptions = { 10 tagClass: function(item) { 11 return 'label label-info'; 12 }, 13 focusClass: 'focus', 14 itemValue: function(item) { 15 return item ? item.toString() : item; 16 }, 17 itemText: function(item) { 18 return this.itemValue(item); 19 }, 20 itemTitle: function(item) { 21 return null; 22 }, 23 freeInput: true, 24 addOnBlur: true, 25 maxTags: undefined, 26 maxChars: undefined, 27 confirmKeys: [13, 44], 28 delimiter: ',', 29 delimiterRegex: null, 30 cancelConfirmKeysOnEmpty: false, 31 onTagExists: function(item, $tag) { 32 $tag.hide().fadeIn(); 33 }, 34 trimValue: false, 35 allowDuplicates: false, 36 triggerChange: true 37 }; 38 39 /** 40 * Constructor function 41 */ 42 function TagsInput(element, options) { 43 this.isInit = true; 44 this.itemsArray = []; 45 46 this.$element = $(element); 47 this.$element.hide(); 48 49 this.isSelect = (element.tagName === 'SELECT'); 50 this.multiple = (this.isSelect && element.hasAttribute('multiple')); 51 this.objectItems = options && options.itemValue; 52 this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; 53 this.inputSize = Math.max(1, this.placeholderText.length); 54
55 this.$container = $('<div class="bootstrap-tagsinput"></div>'); 56 this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container); 57 58 this.$element.before(this.$container); 59 60 this.build(options); 61 this.isInit = false; 62 } 63 64 TagsInput.prototype = { 65 constructor: TagsInput, 66 67 /** 68 * Adds the given item as a new tag. Pass true to dontPushVal to prevent 69 * updating the elements val() 70 */ 71 add: function(item, dontPushVal, options) { 72 var self = this; 73 74 if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) 75 return; 76 77 // Ignore falsey values, except false 78 if (item !== false && !item) 79 return; 80 81 // Trim value 82 if (typeof item === "string" && self.options.trimValue) { 83 item = $.trim(item); 84 } 85 86 // Throw an error when trying to add an object while the itemValue option was not set 87 if (typeof item === "object" && !self.objectItems) 88 throw("Can't add objects when itemValue option is not set"); 89 90 // Ignore strings only containg whitespace 91 if (item.toString().match(/^\s*$/)) 92 return; 93 94 // If SELECT but not multiple, remove current tag 95 if (self.isSelect && !self.multiple && self.itemsArray.length > 0) 96 self.remove(self.itemsArray[0]); 97 98 if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { 99 var delimiter = (self.options.delimiterRegex) ? self.options.delimiterRegex : self.options.delimiter; 100 var items = item.split(delimiter); 101 if (items.length > 1) { 102 for (var i = 0; i < items.length; i++) { 103 this.add(items[i], true); 104 } 105 106 if (!dontPushVal) 107 self.pushVal(self.options.triggerChange); 108 return; 109 } 110 } 111 112 var itemValue = self.options.itemValue(item), 113 itemText = self.options.itemText(item), 114 tagClass = self.options.tagClass(item), 115 itemTitle = self.options.itemTitle(item); 116 117 // Ignore items allready added 118 var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; 119 if (existing && !self.options.allowDuplicates) { 120 // Invoke onTagExists 121 if (self.options.onTagExists) { 122 var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); 123 self.options.onTagExists(item, $existingTag); 124 } 125 return; 126 } 127 128 // if length greater than limit 129 if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) 130 return; 131 132 // raise beforeItemAdd arg 133 var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false, options: options}); 134 self.$element.trigger(beforeItemAddEvent); 135 if (beforeItemAddEvent.cancel) 136 return; 137 138 // register item in internal array and map 139 self.itemsArray.push(item); 140 141 // add a tag element 142 143 var $tag = $('<span class="tag ' + htmlEncode(tagClass) + (itemTitle !== null ? ('" title="' + itemTitle) : '') + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>'); 144 $tag.data('item', item); 145 self.findInputWrapper().before($tag); 146 $tag.after(' '); 147 148 // Check to see if the tag exists in its raw or uri-encoded form 149 var optionExists = ( 150 $('option[value="' + encodeURIComponent(itemValue) + '"]', self.$element).length || 151 $('option[value="' + htmlEncode(itemValue) + '"]', self.$element).length 152 ); 153 154 // add <option /> if item represents a value not present in one of the <select />'s options 155 if (self.isSelect && !optionExists) { 156 var $option = $('<option selected>' + htmlEncode(itemText) + '</option>'); 157 $option.data('item', item); 158 $option.attr('value', itemValue); 159 self.$element.append($option); 160 } 161 162 if (!dontPushVal) 163 self.pushVal(self.options.triggerChange); 164 165 // Add class when reached maxTags 166 if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInput
166Length) 167 self.$container.addClass('bootstrap-tagsinput-max'); 168 169 // If using typeahead, once the tag has been added, clear the typeahead value so it does not stick around in the input. 170 if ($('.typeahead, .twitter-typeahead', self.$container).length) { 171 self.$input.typeahead('val', ''); 172 } 173 174 if (this.isInit) { 175 self.$element.trigger($.Event('itemAddedOnInit', { item: item, options: options })); 176 } else { 177 self.$element.trigger($.Event('itemAdded', { item: item, options: options })); 178 } 179 }, 180 181 /** 182 * Removes the given item. Pass true to dontPushVal to prevent updating the 183 * elements val() 184 */ 185 remove: function(item, dontPushVal, options) { 186 var self = this; 187 188 if (self.objectItems) { 189 if (typeof item === "object") 190 item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } ); 191 else 192 item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } ); 193 194 item = item[item.length-1]; 195 } 196 197 if (item) { 198 var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false, options: options }); 199 self.$element.trigger(beforeItemRemoveEvent); 200 if (beforeItemRemoveEvent.cancel) 201 return; 202 203 $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove(); 204 $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove(); 205 if($.inArray(item, self.itemsArray) !== -1) 206 self.itemsArray.splice($.inArray(item, self.itemsArray), 1); 207 } 208 209 if (!dontPushVal) 210 self.pushVal(self.options.triggerChange); 211 212 // Remove class when reached maxTags 213 if (self.options.maxTags > self.itemsArray.length) 214 self.$container.removeClass('bootstrap-tagsinput-max'); 215 216 self.$element.trigger($.Event('itemRemoved', { item: item, options: options })); 217 }, 218 219 /** 220 * Removes all items 221 */ 222 removeAll: function() { 223 var self = this; 224 225 $('.tag', self.$container).remove(); 226 $('option', self.$element).remove(); 227 228 while(self.itemsArray.length > 0) 229 self.itemsArray.pop(); 230 231 self.pushVal(self.options.triggerChange); 232 }, 233 234 /** 235 * Refreshes the tags so they match the text/value of their corresponding 236 * item. 237 */ 238 refresh: function() { 239 var self = this; 240 $('.tag', self.$container).each(function() { 241 var $tag = $(this), 242 item = $tag.data('item'), 243 itemValue = self.options.itemValue(item), 244 itemText = self.options.itemText(item), 245 tagClass = self.options.tagClass(item); 246 247 // Update tag's class and inner text 248 $tag.attr('class', null); 249 $tag.addClass('tag ' + htmlEncode(tagClass)); 250 $tag.contents().filter(function() { 251 return this.nodeType == 3; 252 })[0].nodeValue = htmlEncode(itemText); 253 254 if (self.isSelect) { 255 var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; }); 256 option.attr('value', itemValue); 257 } 258 }); 259 }, 260 261 /** 262 * Returns the items added as tags 263 */ 264 items: function() { 265 return this.itemsArray; 266 }, 267 268 /** 269 * Assembly value by retrieving the value of each item, and set it on the 270 * element. 271 */ 272 pushVal: function() { 273 var self = this, 274 val = $.map(self.items(), function(item) { 275 return self.options.itemValue(item).toString(); 276 }); 277 278 self.$element.val(val, true); 279 280 if (self.options.triggerChange) 281 self.$element.trigger('change'); 282 }, 283 284 /** 285 * Initializes the tags input behaviour on the element 286 */ 287 build: function(options) { 288 var self = this; 289 290 self.options = $.extend({}, defaultOptions, options); 291 // When itemValue is set, freeInput should always be false 292 if (self.objectItems) 293 self.options.freeInput = false;
294 295 makeOptionItemFunction(self.options, 'itemValue'); 296 makeOptionItemFunction(self.options, 'itemText'); 297 makeOptionFunction(self.options, 'tagClass'); 298 299 // Typeahead Bootstrap version 2.3.2 300 if (self.options.typeahead) { 301 var typeahead = self.options.typeahead || {}; 302 303 makeOptionFunction(typeahead, 'source'); 304 305 self.$input.typeahead($.extend({}, typeahead, { 306 source: function (query, process) { 307 function processItems(items) { 308 var texts = []; 309 310 for (var i = 0; i < items.length; i++) { 311 var text = self.options.itemText(items[i]); 312 map[text] = items[i]; 313 texts.push(text); 314 } 315 process(texts); 316 } 317 318 this.map = {}; 319 var map = this.map, 320 data = typeahead.source(query); 321 322 if ($.isFunction(data.success)) { 323 // support for Angular callbacks 324 data.success(processItems); 325 } else if ($.isFunction(data.then)) { 326 // support for Angular promises 327 data.then(processItems); 328 } else { 329 // support for functions and jquery promises 330 $.when(data) 331 .then(processItems); 332 } 333 }, 334 updater: function (text) { 335 self.add(this.map[text]); 336 return this.map[text]; 337 }, 338 matcher: function (text) { 339 return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1); 340 }, 341 sorter: function (texts) { 342 return texts.sort(); 343 }, 344 highlighter: function (text) { 345 var regex = new RegExp( '(' + this.query + ')', 'gi' ); 346 return text.replace( regex, "<strong>$1</strong>" ); 347 } 348 })); 349 } 350 351 // typeahead.js 352 if (self.options.typeaheadjs) { 353 354 // Determine if main configurations were passed or simply a dataset 355 var typeaheadjs = self.options.typeaheadjs; 356 if (!$.isArray(typeaheadjs)) { 357 typeaheadjs = [null, typeaheadjs]; 358 } 359 var valueKey = typeaheadjs[1].valueKey; // We should test typeaheadjs.size >= 1 360 var f_datum = valueKey ? function (datum) { return datum[valueKey]; } 361 : function (datum) { return datum; } 362 $.fn.typeahead.apply(self.$input,typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum) { 363 self.add( f_datum(datum) ); 364 self.$input.typeahead('val', ''); 365 }, self)); 366 367 } 368 369 self.$container.on('click', $.proxy(function(event) { 370 if (! self.$element.attr('disabled')) { 371 self.$input.removeAttr('disabled'); 372 } 373 self.$input.focus(); 374 }, self)); 375 376 if (self.options.addOnBlur && self.options.freeInput) { 377 self.$input.on('focusout', $.proxy(function(event) { 378 // HACK: only process on focusout when no typeahead opened, to 379 // avoid adding the typeahead text as tag 380 if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) { 381 self.add(self.$input.val()); 382 self.$input.val(''); 383 } 384 }, self)); 385 } 386 387 // Toggle the 'focus' css class on the container when it has focus 388 self.$container.on({ 389 focusin: function() { 390 self.$container.addClass(self.options.focusClass); 391 }, 392 focusout: function() { 393 self.$container.removeClass(self.options.focusClass); 394 }, 395 }); 396 397 self.$container.on('keydown', 'input', $.proxy(function(event) { 398 var $input = $(event.target), 399 $inputWrapper = self.findInputWrapper(); 400 401 if (self.$element.attr('disabled')) { 402 self.$input.attr('disabled', 'disabled'); 403 return; 404 } 405 406 switch (event.which) { 407 // BACKSPACE 408 case 8: 409 if (doGetCaretPosition($input[0]) === 0) { 410 var prev = $inputWrapper.prev(); 411 if (prev.length) { 412 self.remove(prev.data('item')); 413 } 414 } 415 break; 416
417 // DELETE 418 case 46: 419 if (doGetCaretPosition($input[0]) === 0) { 420 var next = $inputWrapper.next(); 421 if (next.length) { 422 self.remove(next.data('item')); 423 } 424 } 425 break; 426 427 // LEFT ARROW 428 case 37: 429 // Try to move the input before the previous tag 430 var $prevTag = $inputWrapper.prev(); 431 if ($input.val().length === 0 && $prevTag[0]) { 432 $prevTag.before($inputWrapper); 433 $input.focus(); 434 } 435 break; 436 // RIGHT ARROW 437 case 39: 438 // Try to move the input after the next tag 439 var $nextTag = $inputWrapper.next(); 440 if ($input.val().length === 0 && $nextTag[0]) { 441 $nextTag.after($inputWrapper); 442 $input.focus(); 443 } 444 break; 445 default: 446 // ignore 447 } 448 449 // Reset internal input's size 450 var textLength = $input.val().length,
451 wordSpace = Math.ceil(textLength / 5), 452 size = textLength + wordSpace + 1; 453 $input.attr('size', Math.max(this.inputSize, $input.val().length)); 454 }, self)); 455 456 self.$container.on('keypress', 'input', $.proxy(function(event) { 457 var $input = $(event.target); 458 459 if (self.$element.attr('disabled')) { 460 self.$input.attr('disabled', 'disabled'); 461 return; 462 } 463 464 var text = $input.val(), 465 maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars; 466 if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) { 467 // Only attempt to add a tag if there is data in the field 468 if (text.length !== 0) { 469 self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text); 470 $input.val(''); 471 } 472 473 // If the field is empty, let the event triggered fire as usual 474 if (self.options.cancelConfirmKeysOnEmpty === false) { 475 event.preventDefault(); 476 } 477 } 478 479 // Reset internal input's size 480 var textLength = $input.val().length,
481 wordSpace = Math.ceil(textLength / 5), 482 size = textLength + wordSpace + 1; 483 $input.attr('size', Math.max(this.inputSize, $input.val().length)); 484 }, self)); 485 486 // Remove icon clicked 487 self.$container.on('click', '[data-role=remove]', $.proxy(function(event) { 488 if (self.$element.attr('disabled')) { 489 return; 490 } 491 self.remove($(event.target).closest('.tag').data('item')); 492 }, self)); 493 494 // Only add existing value as tags when using strings as tags 495 if (self.options.itemValue === defaultOptions.itemValue) { 496 if (self.$element[0].tagName === 'INPUT') { 497 self.add(self.$element.val()); 498 } else { 499 $('option', self.$element).each(function() { 500 self.add($(this).attr('value'), true); 501 }); 502 } 503 } 504 }, 505 506 /** 507 * Removes all tagsinput behaviour and unregsiter all event handlers 508 */ 509 destroy: function() { 510 var self = this; 511 512 // Unbind events 513 self.$container.off('keypress', 'input'); 514 self.$container.off('click', '[role=remove]'); 515 516 self.$container.remove(); 517 self.$element.removeData('tagsinput'); 518 self.$element.show(); 519 }, 520 521 /** 522 * Sets focus on the tagsinput 523 */ 524 focus: function() { 525 this.$input.focus(); 526 }, 527 528 /** 529 * Returns the internal input element 530 */ 531 input: function() { 532 return this.$input; 533 }, 534 535 /** 536 * Returns the element which is wrapped around the internal input. This 537 * is normally the $container, but typeahead.js moves the $input element. 538 */ 539 findInputWrapper: function() { 540 var elt = this.$input[0], 541 container = this.$container[0]; 542 while(elt && elt.parentNode !== container) 543 elt = elt.parentNode; 544 545 return $(elt); 546 } 547 }; 548 549 /** 550 * Register JQuery plugin 551 */ 552 $.fn.tagsinput = function(arg1, arg2, arg3) { 553 var results = []; 554 555 this.each(function() { 556 var tagsinput = $(this).data('tagsinput'); 557 // Initialize a new tags input 558 if (!tagsinput) { 559 tagsinput = new TagsInput(this, arg1); 560 $(this).data('tagsinput', tagsinput); 561 results.push(tagsinput); 562 563 if (this.tagName === 'SELECT') { 564 $('option', $(this)).attr('selected', 'selected'); 565 } 566 567 // Init tags from $(this).val() 568 $(this).val($(this).val()); 569 } else if (!arg1 && !arg2) { 570 // tagsinput already exists 571 // no function, trying to init 572 results.push(tagsinput); 573 } else if(tagsinput[arg1] !== undefined) { 574 // Invoke function on existing tags input 575 if(tagsinput[arg1].length === 3 && arg3 !== undefined){ 576 var retVal = tagsinput[arg1](arg2, null, arg3); 577 }else{ 578 var retVal = tagsinput[arg1](arg2); 579 } 580 if (retVal !== undefined) 581 results.push(retVal); 582 } 583 }); 584 585 if ( typeof arg1 == 'string') { 586 // Return the results from the invoked function calls 587 return results.length > 1 ? results : results[0]; 588 } else { 589 return results; 590 } 591 }; 592 593 $.fn.tagsinput.Constructor = TagsInput; 594 595 /** 596 * Most options support both a string or number as well as a function as 597 * option value. This function makes sure that the option with the given 598 * key in the given options is wrapped in a function 599 */ 600 function makeOptionItemFunction(options, key) { 601 if (typeof options[key] !== 'function') { 602 var propertyName = options[key]; 603 options[key] = function(item) { return item[propertyName]; }; 604 } 605 } 606 function makeOptionFunction(options, key) { 607 if (typeof options[key] !== 'function') { 608 var value = options[key]; 609 options[key] = function() { return value; }; 610 } 611 } 612 /** 613 * HtmlEncodes the given value 614 */
615 var htmlEncodeContainer = $('<div />'); 616 function htmlEncode(value) { 617 if (value) { 618 return htmlEncodeContainer.text(value).html(); 619 } else { 620 return ''; 621 } 622 } 623 624 /** 625 * Returns the position of the caret in the given input field 626 * http://flightschool.acylt.com/devnotes/caret-position-woes/ 627 */ 628 function doGetCaretPosition(oField) { 629 var iCaretPos = 0; 630 if (document.selection) { 631 oField.focus (); 632 var oSel = document.selection.createRange(); 633 oSel.moveStart ('character', -oField.value.length); 634 iCaretPos = oSel.text.length; 635 } else if (oField.selectionStart || oField.selectionStart == '0') { 636 iCaretPos = oField.selectionStart; 637 } 638 return (iCaretPos); 639 } 640 641 /** 642 * Returns boolean indicates whether user has pressed an expected key combination. 643 * @param object keyPressEvent: JavaScript event object, refer 644 * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html 645 * @param object lookupList: expected key combinations, as in: 646 * [13, {which: 188, shiftKey: true}] 647 */ 648 function keyCombinationInList(keyPressEvent, lookupList) { 649 var found = false; 650 $.each(lookupList, function (index, keyCombination) { 651 if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) { 652 found = true; 653 return false; 654 } 655 656 if (keyPressEvent.which === keyCombination.which) { 657 var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey, 658 shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey, 659 ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey; 660 if (alt && shift && ctrl) { 661 found = true; 662 return false; 663 } 664 } 665 }); 666 667 return found; 668 } 669 670 /** 671 * Initialize tagsinput behaviour on inputs and selects which have 672 * data-role=tagsinput 673 */ 674 $(function() { 675 $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); 676 }); 677})(window.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.