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