bootstrap-tokenfield.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. /*!
  2. * bootstrap-tokenfield
  3. * https://github.com/sliptree/bootstrap-tokenfield
  4. * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
  5. */
  6. (function (factory) {
  7. if (typeof define === 'function' && define.amd) {
  8. // AMD. Register as an anonymous module.
  9. define(['jquery'], factory);
  10. } else if (typeof exports === 'object') {
  11. // For CommonJS and CommonJS-like environments where a window with jQuery
  12. // is present, execute the factory with the jQuery instance from the window object
  13. // For environments that do not inherently posses a window with a document
  14. // (such as Node.js), expose a Tokenfield-making factory as module.exports
  15. // This accentuates the need for the creation of a real window or passing in a jQuery instance
  16. // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
  17. module.exports = global.window && global.window.$ ?
  18. factory( global.window.$ ) :
  19. function( input ) {
  20. if ( !input.$ && !input.fn ) {
  21. throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
  22. }
  23. return factory( input.$ || input );
  24. };
  25. } else {
  26. // Browser globals
  27. factory(jQuery, window);
  28. }
  29. }(function ($, window) {
  30. "use strict"; // jshint ;_;
  31. /* TOKENFIELD PUBLIC CLASS DEFINITION
  32. * ============================== */
  33. var Tokenfield = function (element, options) {
  34. var _self = this
  35. this.$element = $(element)
  36. this.textDirection = this.$element.css('direction');
  37. // Extend options
  38. this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
  39. // Setup delimiters and trigger keys
  40. this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
  41. this._triggerKeys = $.map(this._delimiters, function (delimiter) {
  42. return delimiter.charCodeAt(0);
  43. });
  44. this._firstDelimiter = this._delimiters[0];
  45. // Check for whitespace, dash and special characters
  46. var whitespace = $.inArray(' ', this._delimiters)
  47. , dash = $.inArray('-', this._delimiters)
  48. if (whitespace >= 0)
  49. this._delimiters[whitespace] = '\\s'
  50. if (dash >= 0) {
  51. delete this._delimiters[dash]
  52. this._delimiters.unshift('-')
  53. }
  54. var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
  55. $.each(this._delimiters, function (index, character) {
  56. var pos = $.inArray(character, specialCharacters)
  57. if (pos >= 0) _self._delimiters[index] = '\\' + character;
  58. });
  59. // Store original input width
  60. var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
  61. , elStyleWidth = element.style.width
  62. , elCSSWidth
  63. , elWidth = this.$element.width()
  64. if (elRules) {
  65. $.each( elRules, function (i, rule) {
  66. if (rule.style.width) {
  67. elCSSWidth = rule.style.width;
  68. }
  69. });
  70. }
  71. // Move original input out of the way
  72. var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
  73. originalStyles = { position: this.$element.css('position') };
  74. originalStyles[hidingPosition] = this.$element.css(hidingPosition);
  75. this.$element
  76. .data('original-styles', originalStyles)
  77. .data('original-tabindex', this.$element.prop('tabindex'))
  78. .css('position', 'absolute')
  79. .css(hidingPosition, '-10000px')
  80. .prop('tabindex', -1)
  81. // Create a wrapper
  82. this.$wrapper = $('<div class="tokenfield form-control" />')
  83. if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
  84. if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
  85. if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
  86. // Create a new input
  87. var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
  88. this.$input = $('<input type="text" class="token-input" autocomplete="off" />')
  89. .appendTo( this.$wrapper )
  90. .prop( 'placeholder', this.$element.prop('placeholder') )
  91. .prop( 'id', id + '-tokenfield' )
  92. .prop( 'tabindex', this.$element.data('original-tabindex') )
  93. // Re-route original input label to new input
  94. var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
  95. if ( $label.length ) {
  96. $label.prop( 'for', this.$input.prop('id') )
  97. }
  98. // Set up a copy helper to handle copy & paste
  99. this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
  100. // Set wrapper width
  101. if (elStyleWidth) {
  102. this.$wrapper.css('width', elStyleWidth);
  103. }
  104. else if (elCSSWidth) {
  105. this.$wrapper.css('width', elCSSWidth);
  106. }
  107. // If input is inside inline-form with no width set, set fixed width
  108. else if (this.$element.parents('.form-inline').length) {
  109. this.$wrapper.width( elWidth )
  110. }
  111. // Set tokenfield disabled, if original or fieldset input is disabled
  112. if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
  113. this.disable();
  114. }
  115. // Set tokenfield readonly, if original input is readonly
  116. if (this.$element.prop('readonly')) {
  117. this.readonly();
  118. }
  119. // Set up mirror for input auto-sizing
  120. this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
  121. this.$input.css('min-width', this.options.minWidth + 'px')
  122. $.each([
  123. 'fontFamily',
  124. 'fontSize',
  125. 'fontWeight',
  126. 'fontStyle',
  127. 'letterSpacing',
  128. 'textTransform',
  129. 'wordSpacing',
  130. 'textIndent'
  131. ], function (i, val) {
  132. _self.$mirror[0].style[val] = _self.$input.css(val);
  133. });
  134. this.$mirror.appendTo( 'body' )
  135. // Insert tokenfield to HTML
  136. this.$wrapper.insertBefore( this.$element )
  137. this.$element.prependTo( this.$wrapper )
  138. // Calculate inner input width
  139. this.update()
  140. // Create initial tokens, if any
  141. this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
  142. // Start listening to events
  143. this.listen()
  144. // Initialize autocomplete, if necessary
  145. if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
  146. var side = this.textDirection === 'rtl' ? 'right' : 'left'
  147. , autocompleteOptions = $.extend({
  148. minLength: this.options.showAutocompleteOnFocus ? 0 : null,
  149. position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
  150. }, this.options.autocomplete )
  151. this.$input.autocomplete( autocompleteOptions )
  152. }
  153. // Initialize typeahead, if necessary
  154. if ( ! $.isEmptyObject( this.options.typeahead ) ) {
  155. var typeaheadOptions = this.options.typeahead
  156. , defaults = {
  157. minLength: this.options.showAutocompleteOnFocus ? 0 : null
  158. }
  159. , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
  160. args[0] = $.extend( {}, defaults, args[0] )
  161. this.$input.typeahead.apply( this.$input, args )
  162. this.typeahead = true
  163. }
  164. }
  165. Tokenfield.prototype = {
  166. constructor: Tokenfield
  167. , createToken: function (attrs, triggerChange) {
  168. var _self = this
  169. if (typeof attrs === 'string') {
  170. attrs = { value: attrs, label: attrs }
  171. } else {
  172. // Copy objects to prevent contamination of data sources.
  173. attrs = $.extend( {}, attrs )
  174. }
  175. if (typeof triggerChange === 'undefined') {
  176. triggerChange = true
  177. }
  178. // Normalize label and value
  179. attrs.value = $.trim(attrs.value);
  180. attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
  181. // Bail out if has no value or label, or label is too short
  182. if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
  183. // Bail out if maximum number of tokens is reached
  184. if (this.options.limit && this.getTokens().length >= this.options.limit) return
  185. // Allow changing token data before creating it
  186. var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
  187. this.$element.trigger(createEvent)
  188. // Bail out if there if attributes are empty or event was defaultPrevented
  189. if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
  190. var $token = $('<div class="token" />')
  191. .append('<span class="token-label" />')
  192. .append('<a href="#" class="close" tabindex="-1">&times;</a>')
  193. .data('attrs', attrs)
  194. // Insert token into HTML
  195. if (this.$input.hasClass('tt-input')) {
  196. // If the input has typeahead enabled, insert token before it's parent
  197. this.$input.parent().before( $token )
  198. } else {
  199. this.$input.before( $token )
  200. }
  201. // Temporarily set input width to minimum
  202. this.$input.css('width', this.options.minWidth + 'px')
  203. var $tokenLabel = $token.find('.token-label')
  204. , $closeButton = $token.find('.close')
  205. // Determine maximum possible token label width
  206. if (!this.maxTokenWidth) {
  207. this.maxTokenWidth =
  208. this.$wrapper.width() - $closeButton.outerWidth() -
  209. parseInt($closeButton.css('margin-left'), 10) -
  210. parseInt($closeButton.css('margin-right'), 10) -
  211. parseInt($token.css('border-left-width'), 10) -
  212. parseInt($token.css('border-right-width'), 10) -
  213. parseInt($token.css('padding-left'), 10) -
  214. parseInt($token.css('padding-right'), 10)
  215. parseInt($tokenLabel.css('border-left-width'), 10) -
  216. parseInt($tokenLabel.css('border-right-width'), 10) -
  217. parseInt($tokenLabel.css('padding-left'), 10) -
  218. parseInt($tokenLabel.css('padding-right'), 10)
  219. parseInt($tokenLabel.css('margin-left'), 10) -
  220. parseInt($tokenLabel.css('margin-right'), 10)
  221. }
  222. $tokenLabel
  223. .text(attrs.label)
  224. .css('max-width', this.maxTokenWidth)
  225. // Listen to events on token
  226. $token
  227. .on('mousedown', function (e) {
  228. if (_self._disabled || _self._readonly) return false
  229. _self.preventDeactivation = true
  230. })
  231. .on('click', function (e) {
  232. if (_self._disabled || _self._readonly) return false
  233. _self.preventDeactivation = false
  234. if (e.ctrlKey || e.metaKey) {
  235. e.preventDefault()
  236. return _self.toggle( $token )
  237. }
  238. _self.activate( $token, e.shiftKey, e.shiftKey )
  239. })
  240. .on('dblclick', function (e) {
  241. if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
  242. _self.edit( $token )
  243. })
  244. $closeButton
  245. .on('click', $.proxy(this.remove, this))
  246. // Trigger createdtoken event on the original field
  247. // indicating that the token is now in the DOM
  248. this.$element.trigger($.Event('tokenfield:createdtoken', {
  249. attrs: attrs,
  250. relatedTarget: $token.get(0)
  251. }))
  252. // Trigger change event on the original field
  253. if (triggerChange) {
  254. this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
  255. }
  256. // Update tokenfield dimensions
  257. this.update()
  258. // Return original element
  259. return this.$element.get(0)
  260. }
  261. , setTokens: function (tokens, add, triggerChange) {
  262. if (!tokens) return
  263. if (!add) this.$wrapper.find('.token').remove()
  264. if (typeof triggerChange === 'undefined') {
  265. triggerChange = true
  266. }
  267. if (typeof tokens === 'string') {
  268. if (this._delimiters.length) {
  269. // Split based on delimiters
  270. tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
  271. } else {
  272. tokens = [tokens];
  273. }
  274. }
  275. var _self = this
  276. $.each(tokens, function (i, attrs) {
  277. _self.createToken(attrs, triggerChange)
  278. })
  279. return this.$element.get(0)
  280. }
  281. , getTokenData: function($token) {
  282. var data = $token.map(function() {
  283. var $token = $(this);
  284. return $token.data('attrs')
  285. }).get();
  286. if (data.length == 1) {
  287. data = data[0];
  288. }
  289. return data;
  290. }
  291. , getTokens: function(active) {
  292. var self = this
  293. , tokens = []
  294. , activeClass = active ? '.active' : '' // get active tokens only
  295. this.$wrapper.find( '.token' + activeClass ).each( function() {
  296. tokens.push( self.getTokenData( $(this) ) )
  297. })
  298. return tokens
  299. }
  300. , getTokensList: function(delimiter, beautify, active) {
  301. delimiter = delimiter || this._firstDelimiter
  302. beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
  303. var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
  304. return $.map( this.getTokens(active), function (token) {
  305. return token.value
  306. }).join(separator)
  307. }
  308. , getInput: function() {
  309. return this.$input.val()
  310. }
  311. , listen: function () {
  312. var _self = this
  313. this.$element
  314. .on('change', $.proxy(this.change, this))
  315. this.$wrapper
  316. .on('mousedown',$.proxy(this.focusInput, this))
  317. this.$input
  318. .on('focus', $.proxy(this.focus, this))
  319. .on('blur', $.proxy(this.blur, this))
  320. .on('paste', $.proxy(this.paste, this))
  321. .on('keydown', $.proxy(this.keydown, this))
  322. .on('keypress', $.proxy(this.keypress, this))
  323. .on('keyup', $.proxy(this.keyup, this))
  324. this.$copyHelper
  325. .on('focus', $.proxy(this.focus, this))
  326. .on('blur', $.proxy(this.blur, this))
  327. .on('keydown', $.proxy(this.keydown, this))
  328. .on('keyup', $.proxy(this.keyup, this))
  329. // Secondary listeners for input width calculation
  330. this.$input
  331. .on('keypress', $.proxy(this.update, this))
  332. .on('keyup', $.proxy(this.update, this))
  333. this.$input
  334. .on('autocompletecreate', function() {
  335. // Set minimum autocomplete menu width
  336. var $_menuElement = $(this).data('ui-autocomplete').menu.element
  337. var minWidth = _self.$wrapper.outerWidth() -
  338. parseInt( $_menuElement.css('border-left-width'), 10 ) -
  339. parseInt( $_menuElement.css('border-right-width'), 10 )
  340. $_menuElement.css( 'min-width', minWidth + 'px' )
  341. })
  342. .on('autocompleteselect', function (e, ui) {
  343. if (_self.createToken( ui.item )) {
  344. _self.$input.val('')
  345. if (_self.$input.data( 'edit' )) {
  346. _self.unedit(true)
  347. }
  348. }
  349. return false
  350. })
  351. .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
  352. // Create token
  353. if (_self.createToken( datum )) {
  354. _self.$input.typeahead('val', '')
  355. if (_self.$input.data( 'edit' )) {
  356. _self.unedit(true)
  357. }
  358. }
  359. })
  360. // Listen to window resize
  361. $(window).on('resize', $.proxy(this.update, this ))
  362. }
  363. , keydown: function (e) {
  364. if (!this.focused) return
  365. var _self = this
  366. switch(e.keyCode) {
  367. case 8: // backspace
  368. if (!this.$input.is(document.activeElement)) break
  369. this.lastInputValue = this.$input.val()
  370. break
  371. case 37: // left arrow
  372. leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
  373. break
  374. case 38: // up arrow
  375. upDown('prev')
  376. break
  377. case 39: // right arrow
  378. leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
  379. break
  380. case 40: // down arrow
  381. upDown('next')
  382. break
  383. case 65: // a (to handle ctrl + a)
  384. if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
  385. this.activateAll()
  386. e.preventDefault()
  387. break
  388. case 9: // tab
  389. case 13: // enter
  390. // We will handle creating tokens from autocomplete in autocomplete events
  391. if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus)").length) break
  392. // We will handle creating tokens from typeahead in typeahead events
  393. if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
  394. if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val().length) break
  395. // Create token
  396. if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
  397. return this.createTokensFromInput(e, this.$input.data('edit'));
  398. }
  399. // Edit token
  400. if (e.keyCode === 13) {
  401. if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
  402. if (!_self.options.allowEditing) break
  403. this.edit( this.$wrapper.find('.token.active') )
  404. }
  405. }
  406. function leftRight(direction) {
  407. if (_self.$input.is(document.activeElement)) {
  408. if (_self.$input.val().length > 0) return
  409. direction += 'All'
  410. var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
  411. if (!$token.length) return
  412. _self.preventInputFocus = true
  413. _self.preventDeactivation = true
  414. _self.activate( $token )
  415. e.preventDefault()
  416. } else {
  417. _self[direction]( e.shiftKey )
  418. e.preventDefault()
  419. }
  420. }
  421. function upDown(direction) {
  422. if (!e.shiftKey) return
  423. if (_self.$input.is(document.activeElement)) {
  424. if (_self.$input.val().length > 0) return
  425. var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
  426. if (!$token.length) return
  427. _self.activate( $token )
  428. }
  429. var opposite = direction === 'prev' ? 'next' : 'prev'
  430. , position = direction === 'prev' ? 'first' : 'last'
  431. _self.firstActiveToken[opposite + 'All']('.token').each(function() {
  432. _self.deactivate( $(this) )
  433. })
  434. _self.activate( _self.$wrapper.find('.token:' + position), true, true )
  435. e.preventDefault()
  436. }
  437. this.lastKeyDown = e.keyCode
  438. }
  439. , keypress: function(e) {
  440. this.lastKeyPressCode = e.keyCode
  441. this.lastKeyPressCharCode = e.charCode
  442. // Comma
  443. if ($.inArray( e.charCode, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
  444. if (this.$input.val()) {
  445. this.createTokensFromInput(e)
  446. }
  447. return false;
  448. }
  449. }
  450. , keyup: function (e) {
  451. this.preventInputFocus = false
  452. if (!this.focused) return
  453. switch(e.keyCode) {
  454. case 8: // backspace
  455. if (this.$input.is(document.activeElement)) {
  456. if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
  457. this.preventDeactivation = true
  458. var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
  459. if (!$prevToken.length) break
  460. this.activate( $prevToken )
  461. } else {
  462. this.remove(e)
  463. }
  464. break
  465. case 46: // delete
  466. this.remove(e, 'next')
  467. break
  468. }
  469. this.lastKeyUp = e.keyCode
  470. }
  471. , focus: function (e) {
  472. this.focused = true
  473. this.$wrapper.addClass('focus')
  474. if (this.$input.is(document.activeElement)) {
  475. this.$wrapper.find('.active').removeClass('active')
  476. this.$firstActiveToken = null
  477. if (this.options.showAutocompleteOnFocus) {
  478. this.search()
  479. }
  480. }
  481. }
  482. , blur: function (e) {
  483. this.focused = false
  484. this.$wrapper.removeClass('focus')
  485. if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
  486. this.$wrapper.find('.active').removeClass('active')
  487. this.$firstActiveToken = null
  488. }
  489. if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
  490. this.createTokensFromInput(e)
  491. }
  492. this.preventDeactivation = false
  493. this.preventCreateTokens = false
  494. }
  495. , paste: function (e) {
  496. var _self = this
  497. // Add tokens to existing ones
  498. if (_self.options.allowPasting) {
  499. setTimeout(function () {
  500. _self.createTokensFromInput(e)
  501. }, 1)
  502. }
  503. }
  504. , change: function (e) {
  505. if ( e.initiator === 'tokenfield' ) return // Prevent loops
  506. this.setTokens( this.$element.val() )
  507. }
  508. , createTokensFromInput: function (e, focus) {
  509. if (this.$input.val().length < this.options.minLength)
  510. return // No input, simply return
  511. var tokensBefore = this.getTokensList()
  512. this.setTokens( this.$input.val(), true )
  513. if (tokensBefore == this.getTokensList() && this.$input.val().length)
  514. return false // No tokens were added, do nothing (prevent form submit)
  515. if (this.$input.hasClass('tt-input')) {
  516. // Typeahead acts weird when simply setting input value to empty,
  517. // so we set the query to empty instead
  518. this.$input.typeahead('val', '')
  519. } else {
  520. this.$input.val('')
  521. }
  522. if (this.$input.data( 'edit' )) {
  523. this.unedit(focus)
  524. }
  525. return false // Prevent form being submitted
  526. }
  527. , next: function (add) {
  528. if (add) {
  529. var $firstActiveToken = this.$wrapper.find('.active:first')
  530. , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
  531. if (deactivate) return this.deactivate( $firstActiveToken )
  532. }
  533. var $lastActiveToken = this.$wrapper.find('.active:last')
  534. , $nextToken = $lastActiveToken.nextAll('.token:first')
  535. if (!$nextToken.length) {
  536. this.$input.focus()
  537. return
  538. }
  539. this.activate($nextToken, add)
  540. }
  541. , prev: function (add) {
  542. if (add) {
  543. var $lastActiveToken = this.$wrapper.find('.active:last')
  544. , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
  545. if (deactivate) return this.deactivate( $lastActiveToken )
  546. }
  547. var $firstActiveToken = this.$wrapper.find('.active:first')
  548. , $prevToken = $firstActiveToken.prevAll('.token:first')
  549. if (!$prevToken.length) {
  550. $prevToken = this.$wrapper.find('.token:first')
  551. }
  552. if (!$prevToken.length && !add) {
  553. this.$input.focus()
  554. return
  555. }
  556. this.activate( $prevToken, add )
  557. }
  558. , activate: function ($token, add, multi, remember) {
  559. if (!$token) return
  560. if (typeof remember === 'undefined') var remember = true
  561. if (multi) var add = true
  562. this.$copyHelper.focus()
  563. if (!add) {
  564. this.$wrapper.find('.active').removeClass('active')
  565. if (remember) {
  566. this.$firstActiveToken = $token
  567. } else {
  568. delete this.$firstActiveToken
  569. }
  570. }
  571. if (multi && this.$firstActiveToken) {
  572. // Determine first active token and the current tokens indicies
  573. // Account for the 1 hidden textarea by subtracting 1 from both
  574. var i = this.$firstActiveToken.index() - 2
  575. , a = $token.index() - 2
  576. , _self = this
  577. this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
  578. _self.activate( $(this), true )
  579. })
  580. }
  581. $token.addClass('active')
  582. this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  583. }
  584. , activateAll: function() {
  585. var _self = this
  586. this.$wrapper.find('.token').each( function (i) {
  587. _self.activate($(this), i !== 0, false, false)
  588. })
  589. }
  590. , deactivate: function($token) {
  591. if (!$token) return
  592. $token.removeClass('active')
  593. this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  594. }
  595. , toggle: function($token) {
  596. if (!$token) return
  597. $token.toggleClass('active')
  598. this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
  599. }
  600. , edit: function ($token) {
  601. if (!$token) return
  602. var attrs = $token.data('attrs')
  603. // Allow changing input value before editing
  604. var options = { attrs: attrs, relatedTarget: $token.get(0) }
  605. var editEvent = $.Event('tokenfield:edittoken', options)
  606. this.$element.trigger( editEvent )
  607. // Edit event can be cancelled if default is prevented
  608. if (editEvent.isDefaultPrevented()) return
  609. $token.find('.token-label').text(attrs.value)
  610. var tokenWidth = $token.outerWidth()
  611. var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
  612. $token.replaceWith( $_input )
  613. this.preventCreateTokens = true
  614. this.$input.val( attrs.value )
  615. .select()
  616. .data( 'edit', true )
  617. .width( tokenWidth )
  618. this.update();
  619. // Indicate that token is now being edited, and is replaced with an input field in the DOM
  620. this.$element.trigger($.Event('tokenfield:editedtoken', options ))
  621. }
  622. , unedit: function (focus) {
  623. var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
  624. $_input.appendTo( this.$wrapper )
  625. this.$input.data('edit', false)
  626. this.$mirror.text('')
  627. this.update()
  628. // Because moving the input element around in DOM
  629. // will cause it to lose focus, we provide an option
  630. // to re-focus the input after appending it to the wrapper
  631. if (focus) {
  632. var _self = this
  633. setTimeout(function () {
  634. _self.$input.focus()
  635. }, 1)
  636. }
  637. }
  638. , remove: function (e, direction) {
  639. if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
  640. var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
  641. if (e.type !== 'click') {
  642. if (!direction) var direction = 'prev'
  643. this[direction]()
  644. // Was it the first token?
  645. if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
  646. }
  647. // Prepare events and their options
  648. var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
  649. , removeEvent = $.Event('tokenfield:removetoken', options)
  650. this.$element.trigger(removeEvent);
  651. // Remove event can be intercepted and cancelled
  652. if (removeEvent.isDefaultPrevented()) return
  653. var removedEvent = $.Event('tokenfield:removedtoken', options)
  654. , changeEvent = $.Event('change', { initiator: 'tokenfield' })
  655. // Remove token from DOM
  656. $token.remove()
  657. // Trigger events
  658. this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
  659. // Focus, when necessary:
  660. // When there are no more tokens, or if this was the first token
  661. // and it was removed with backspace or it was clicked on
  662. if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
  663. // Adjust input width
  664. this.$input.css('width', this.options.minWidth + 'px')
  665. this.update()
  666. // Cancel original event handlers
  667. e.preventDefault()
  668. e.stopPropagation()
  669. }
  670. /**
  671. * Update tokenfield dimensions
  672. */
  673. , update: function (e) {
  674. var value = this.$input.val()
  675. , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
  676. , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
  677. , inputPadding = inputPaddingLeft + inputPaddingRight
  678. if (this.$input.data('edit')) {
  679. if (!value) {
  680. value = this.$input.prop("placeholder")
  681. }
  682. if (value === this.$mirror.text()) return
  683. this.$mirror.text(value)
  684. var mirrorWidth = this.$mirror.width() + 10;
  685. if ( mirrorWidth > this.$wrapper.width() ) {
  686. return this.$input.width( this.$wrapper.width() )
  687. }
  688. this.$input.width( mirrorWidth )
  689. }
  690. else {
  691. var w = (this.textDirection === 'rtl')
  692. ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
  693. : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
  694. //
  695. // some usecases pre-render widget before attaching to DOM,
  696. // dimensions returned by jquery will be NaN -> we default to 100%
  697. // so placeholder won't be cut off.
  698. isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
  699. }
  700. }
  701. , focusInput: function (e) {
  702. if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
  703. // Focus only after the current call stack has cleared,
  704. // otherwise has no effect.
  705. // Reason: mousedown is too early - input will lose focus
  706. // after mousedown. However, since the input may be moved
  707. // in DOM, there may be no click or mouseup event triggered.
  708. var _self = this
  709. setTimeout(function() {
  710. _self.$input.focus()
  711. }, 0)
  712. }
  713. , search: function () {
  714. if ( this.$input.data('ui-autocomplete') ) {
  715. this.$input.autocomplete('search')
  716. }
  717. }
  718. , disable: function () {
  719. this.setProperty('disabled', true);
  720. }
  721. , enable: function () {
  722. this.setProperty('disabled', false);
  723. }
  724. , readonly: function () {
  725. this.setProperty('readonly', true);
  726. }
  727. , writeable: function () {
  728. this.setProperty('readonly', false);
  729. }
  730. , setProperty: function(property, value) {
  731. this['_' + property] = value;
  732. this.$input.prop(property, value);
  733. this.$element.prop(property, value);
  734. this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
  735. }
  736. , destroy: function() {
  737. // Set field value
  738. this.$element.val( this.getTokensList() );
  739. // Restore styles and properties
  740. this.$element.css( this.$element.data('original-styles') );
  741. this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
  742. // Re-route tokenfield label to original input
  743. var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
  744. if ( $label.length ) {
  745. $label.prop( 'for', this.$element.prop('id') )
  746. }
  747. // Move original element outside of tokenfield wrapper
  748. this.$element.insertBefore( this.$wrapper );
  749. // Remove tokenfield-related data
  750. this.$element.removeData('original-styles')
  751. .removeData('original-tabindex')
  752. .removeData('bs.tokenfield');
  753. // Remove tokenfield from DOM
  754. this.$wrapper.remove();
  755. this.$mirror.remove();
  756. var $_element = this.$element;
  757. delete this;
  758. return $_element;
  759. }
  760. }
  761. /* TOKENFIELD PLUGIN DEFINITION
  762. * ======================== */
  763. var old = $.fn.tokenfield
  764. $.fn.tokenfield = function (option, param) {
  765. var value
  766. , args = []
  767. Array.prototype.push.apply( args, arguments );
  768. var elements = this.each(function () {
  769. var $this = $(this)
  770. , data = $this.data('bs.tokenfield')
  771. , options = typeof option == 'object' && option
  772. if (typeof option === 'string' && data && data[option]) {
  773. args.shift()
  774. value = data[option].apply(data, args)
  775. } else {
  776. if (!data && typeof option !== 'string' && !param) {
  777. $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
  778. $this.trigger('tokenfield:initialize')
  779. }
  780. }
  781. })
  782. return typeof value !== 'undefined' ? value : elements;
  783. }
  784. $.fn.tokenfield.defaults = {
  785. minWidth: 60,
  786. minLength: 0,
  787. allowEditing: true,
  788. allowPasting: true,
  789. limit: 0,
  790. autocomplete: {},
  791. typeahead: {},
  792. showAutocompleteOnFocus: false,
  793. createTokensOnBlur: false,
  794. delimiter: ',',
  795. beautify: true
  796. }
  797. $.fn.tokenfield.Constructor = Tokenfield
  798. /* TOKENFIELD NO CONFLICT
  799. * ================== */
  800. $.fn.tokenfield.noConflict = function () {
  801. $.fn.tokenfield = old
  802. return this
  803. }
  804. return Tokenfield;
  805. }));