PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/files/bootstrap.tagsinput/0.4.2/bootstrap-tagsinput.js

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