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