PageSourceSearch

https://fleeped.com/themes/Fleeped/javascript/bootstrap-tagsinput-latest/src/bootstrap-tagsinput.js

js fleeped.com collected 2026-09-24 10:39:02 UTC 21,104 bytes, 646 lines download raw bytes

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.