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