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