Options can be passed via data attributes or JavaScript. For data attributes, append the option name to data-, as in data-minLength="".
141 |
142 |
143 |
144 |
145 |
146 |
Name
147 |
type
148 |
default
149 |
description
150 |
151 |
152 |
153 |
154 |
tokens
155 |
string, array
156 |
[]
157 |
Tokens (or tags). Can be a string with comma-separated values ("one,two,three"), an array of strings (["one","two","three"]), or an array of objects ([{ value: "one", label: "Einz" }, { value: "two", label: "Zwei" }])
158 |
159 |
160 |
limit
161 |
int
162 |
0
163 |
Maximum number of tokens allowed. 0 = unlimited.
164 |
165 |
166 |
minLength
167 |
int
168 |
0
169 |
Minimum length required for token value.
170 |
171 |
172 |
minWidth
173 |
int
174 |
60
175 |
Minimum input field width. In pixels.
176 |
177 |
178 |
autocomplete
179 |
object
180 |
{}
181 |
jQuery UI Autocomplete options
182 |
183 |
184 |
showAutocompleteOnFocus
185 |
boolean
186 |
false
187 |
Whether to show autocomplete suggestions menu on focus or not. Works only for jQuery UI Autocomplete, as Typeahead has no support for this kind of behavior.
188 |
189 |
190 |
typeahead
191 |
array
192 |
[]
193 |
Arguments for Twitter Typeahead. The first argument should be an options hash (or null if you want to use the defaults). The second argument should be a dataset. You can add multiple datasets: typeahead: [options, dataset1, dataset2]
194 |
195 |
196 |
createTokensOnBlur
197 |
boolean
198 |
false
199 |
Whether to turn input into tokens when tokenfield loses focus or not.
200 |
201 |
202 |
delimiter
203 |
string, array
204 |
','
205 |
A character or an array of characters that will trigger token creation on keypress event. Defaults to ',' (comma). Note - this does not affect Enter or Tab keys, as they are handled in the keydown event. The first delimiter will be used as a separator when getting the list of tokens or copy-pasting tokens.
206 |
207 |
208 |
beautify
209 |
boolean
210 |
true
211 |
Whether to insert spaces after each token when getting a comma-separated list of tokens. This affects both value returned by getTokensList() and the value of the original input field.
212 |
213 |
214 |
inputType
215 |
string
216 |
'text'
217 |
HTML type attribute for the token input. This is useful for specifying an HTML5 input type like 'email', 'url' or 'tel' which allows mobile browsers to show a specialized virtual keyboard optimized for different types of input. This only sets the type of the visible token input but does not touch the original input field. So you may set the original input to have type="text" but set this inputType option to 'email' if you only want to take advantage of the email style keyboard on mobile, but don't want to enable HTML5 native email validation on the original hidden input.
218 |
219 |
220 |
221 |
222 |
223 |
224 |
Data attributes for individual tokenfields
225 |
Options for individual tokenfields can alternatively be specified through the use of data attributes, as explained above.
226 |
227 |
228 |
229 |
Styling Twitter Typeahead
230 |
Twitter Typeahead comes with no default styling. Make sure to include tokenfield-typeahead.css on your page.
Get a comma-separated list of the tokens from the input. You can use an alternative separator by supplying also a delimiter argument. Setting beautify to false will prevent adding a space after each token. Set active to true to return only selected tokens.
Though not recommended, you can access the original input field like so: $('#tokenfield').data('bs.tokenfield').$input
301 |
You can also set new options for the autocomplete or typehead objects from the original input above like so: $('#tokenfield').data('bs.tokenfield').$input.autocomplete({source: new_array})
302 |
303 |
304 |
Events
305 |
Tokenfield exposes a few events for hooking into it's functionality.
306 |
307 |
308 |
309 |
310 |
311 |
Event
312 |
Description
313 |
314 |
315 |
316 |
317 |
tokenfield:initialize
318 |
Fires after Tokenfield has been initialized.
319 |
320 |
321 |
tokenfield:createtoken
322 |
This event fires when a token is all set up to be created, but before it is inserted into the DOM and event listeners are attached. You can use this event to manipulate token value and label by changing the appropriate values of attrs property of the event. See below for an example. Calling event.preventDefault() or doing return false in the event handler will prevent the token from being created.
323 |
324 |
325 |
tokenfield:createdtoken
326 |
This event is fired after the token has been created. Here, attrs property of the event is also available, but is basically read-only. You can also get a direct reference to the token DOM object via e.relatedTarget. The example below uses this to set an 'invalid' class on the newly created token if it does not pass validation.
327 |
328 |
329 |
tokenfield:edittoken
330 |
This event is fired just before a token is about to be edited. This allows you to manipluate the input field value before it is created. Again, to do this, manipluate the attrs property of the event. Here you can also access the token DOM object with e.relatedTarget. Calling event.preventDefault() or doing return false in the event handler will prevent the token from being edited.
331 |
332 |
333 |
tokenfield:editedtoken
334 |
This event is fired when a token is ready for being edited. It means that the token has been replaced by an input field.
335 |
336 |
337 |
tokenfield:removetoken
338 |
This event is fired right before a token is removed. Here you can also access the token DOM object with e.relatedTarget. Calling event.preventDefault() or doing return false in the event handler will prevent the token from being removed. You can access token label and value by checking the attrs property of the event. Also, e.relatedTarget is a reference to the token DOM object.
339 |
340 |
341 |
tokenfield:removedtoken
342 |
This event is fired right after a token is removed from the DOM. You can access token label and value by checking the attrs property of the event.
343 |
344 |
345 |
346 |
347 |
348 |
The example below is pretty comprehensive. Here, we split user input into two parts: name and email. Then, we validate the email and if it is not valid, we add an invalid class to the token.
349 |
When the user starts to edit the token, we merge token value and label together again.
Tokenfield includes support for manipulating tokens via keyboard
389 |
left, right arrow keys
390 |
Arrow keys will move between active tokens. Try it out: click on one of the tokens and press left and right arrow keys
391 |
Backspace and delete
392 |
You can delete a selected token with backspace or delete keys. Try it out now:
393 |
Ctrl + A / Cmd + A, Ctrl + C / Cmd + C, Ctrl + V, Cmd + V
394 |
If You have one token selected, you can select all tokens with the keyboard. Then, you can copy the tokens using keyboard. You can also paste tokens to another field.
395 |
396 |
397 |
398 |
399 |
400 |
Copy & paste support
401 |
You can copy tokens from a tokenfield and paste them to any other field as comma-separated values. When you paste to another tokenfield, they will become tokens there, aswell!
402 |
Try it out, copy the following to the field below: violet,yellow,brown
403 |
404 |
405 |
406 |
407 |
408 |
Validation states
409 |
Tokenfield also supports all the default validation states from Bootstrap
410 |
411 |
429 |
430 |
Various examples of using tokenfield
431 |
Using tokenfield with input groups
432 |
433 |
450 |
451 |
Using tokenfield with input group checkboxes and radio buttons
452 |
453 |
473 |
474 |
Using tokenfield with buttons in input groups
475 |
476 |
501 |
502 |
Using tokenfield with different sizes
503 |
504 |
522 |
523 |
Disabled tokenfield
524 |
525 |
528 |
529 |
Disabled fieldset with tokenfield
530 |
531 |
543 |
544 |
Tokenfield in inline form
545 |
546 |
553 |
554 |
Tokenfield in horizontal form
555 |
556 |
581 |
582 |
Tokenfield with fluid and fixed widths (50%, 300px, etc...)
583 |
584 |
598 |
599 |
Tokenfield with RTL direction
600 |
601 |
604 |
605 |
606 |
607 |
608 |
609 |
610 |
611 |
641 |
642 |
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
659 |
660 |
661 |
662 |
--------------------------------------------------------------------------------
/dist/bootstrap-tokenfield.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * bootstrap-tokenfield
3 | * https://github.com/sliptree/bootstrap-tokenfield
4 | * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
5 | */
6 |
7 | (function (factory) {
8 | if (typeof define === 'function' && define.amd) {
9 | // AMD. Register as an anonymous module.
10 | define(['jquery'], factory);
11 | } else if (typeof exports === 'object') {
12 | // For CommonJS and CommonJS-like environments where a window with jQuery
13 | // is present, execute the factory with the jQuery instance from the window object
14 | // For environments that do not inherently posses a window with a document
15 | // (such as Node.js), expose a Tokenfield-making factory as module.exports
16 | // This accentuates the need for the creation of a real window or passing in a jQuery instance
17 | // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
18 | module.exports = global.window && global.window.$ ?
19 | factory( global.window.$ ) :
20 | function( input ) {
21 | if ( !input.$ && !input.fn ) {
22 | throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
23 | }
24 | return factory( input.$ || input );
25 | };
26 | } else {
27 | // Browser globals
28 | factory(jQuery, window);
29 | }
30 | }(function ($, window) {
31 |
32 | "use strict"; // jshint ;_;
33 |
34 | /* TOKENFIELD PUBLIC CLASS DEFINITION
35 | * ============================== */
36 |
37 | var Tokenfield = function (element, options) {
38 | var _self = this
39 |
40 | this.$element = $(element)
41 | this.textDirection = this.$element.css('direction');
42 |
43 | // Extend options
44 | this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
45 |
46 | // Setup delimiters and trigger keys
47 | this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
48 | this._triggerKeys = $.map(this._delimiters, function (delimiter) {
49 | return delimiter.charCodeAt(0);
50 | });
51 | this._firstDelimiter = this._delimiters[0];
52 |
53 | // Check for whitespace, dash and special characters
54 | var whitespace = $.inArray(' ', this._delimiters)
55 | , dash = $.inArray('-', this._delimiters)
56 |
57 | if (whitespace >= 0)
58 | this._delimiters[whitespace] = '\\s'
59 |
60 | if (dash >= 0) {
61 | delete this._delimiters[dash]
62 | this._delimiters.unshift('-')
63 | }
64 |
65 | var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
66 | $.each(this._delimiters, function (index, character) {
67 | var pos = $.inArray(character, specialCharacters)
68 | if (pos >= 0) _self._delimiters[index] = '\\' + character;
69 | });
70 |
71 | // Store original input width
72 | var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
73 | , elStyleWidth = element.style.width
74 | , elCSSWidth
75 | , elWidth = this.$element.width()
76 |
77 | if (elRules) {
78 | $.each( elRules, function (i, rule) {
79 | if (rule.style.width) {
80 | elCSSWidth = rule.style.width;
81 | }
82 | });
83 | }
84 |
85 | // Move original input out of the way
86 | var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
87 | originalStyles = { position: this.$element.css('position') };
88 | originalStyles[hidingPosition] = this.$element.css(hidingPosition);
89 |
90 | this.$element
91 | .data('original-styles', originalStyles)
92 | .data('original-tabindex', this.$element.prop('tabindex'))
93 | .css('position', 'absolute')
94 | .css(hidingPosition, '-10000px')
95 | .prop('tabindex', -1)
96 |
97 | // Create a wrapper
98 | this.$wrapper = $('')
99 | if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
100 | if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
101 | if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
102 |
103 | // Create a new input
104 | var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
105 | this.$input = $('')
106 | .appendTo( this.$wrapper )
107 | .prop( 'placeholder', this.$element.prop('placeholder') )
108 | .prop( 'id', id + '-tokenfield' )
109 | .prop( 'tabindex', this.$element.data('original-tabindex') )
110 |
111 | // Re-route original input label to new input
112 | var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
113 | if ( $label.length ) {
114 | $label.prop( 'for', this.$input.prop('id') )
115 | }
116 |
117 | // Set up a copy helper to handle copy & paste
118 | this.$copyHelper = $('').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
119 |
120 | // Set wrapper width
121 | if (elStyleWidth) {
122 | this.$wrapper.css('width', elStyleWidth);
123 | }
124 | else if (elCSSWidth) {
125 | this.$wrapper.css('width', elCSSWidth);
126 | }
127 | // If input is inside inline-form with no width set, set fixed width
128 | else if (this.$element.parents('.form-inline').length) {
129 | this.$wrapper.width( elWidth )
130 | }
131 |
132 | // Set tokenfield disabled, if original or fieldset input is disabled
133 | if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
134 | this.disable();
135 | }
136 |
137 | // Set tokenfield readonly, if original input is readonly
138 | if (this.$element.prop('readonly')) {
139 | this.readonly();
140 | }
141 |
142 | // Set up mirror for input auto-sizing
143 | this.$mirror = $('');
144 | this.$input.css('min-width', this.options.minWidth + 'px')
145 | $.each([
146 | 'fontFamily',
147 | 'fontSize',
148 | 'fontWeight',
149 | 'fontStyle',
150 | 'letterSpacing',
151 | 'textTransform',
152 | 'wordSpacing',
153 | 'textIndent'
154 | ], function (i, val) {
155 | _self.$mirror[0].style[val] = _self.$input.css(val);
156 | });
157 | this.$mirror.appendTo( 'body' )
158 |
159 | // Insert tokenfield to HTML
160 | this.$wrapper.insertBefore( this.$element )
161 | this.$element.prependTo( this.$wrapper )
162 |
163 | // Calculate inner input width
164 | this.update()
165 |
166 | // Create initial tokens, if any
167 | this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
168 |
169 | // Start listening to events
170 | this.listen()
171 |
172 | // Initialize autocomplete, if necessary
173 | if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
174 | var side = this.textDirection === 'rtl' ? 'right' : 'left'
175 | , autocompleteOptions = $.extend({
176 | minLength: this.options.showAutocompleteOnFocus ? 0 : null,
177 | position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
178 | }, this.options.autocomplete )
179 |
180 | this.$input.autocomplete( autocompleteOptions )
181 | }
182 |
183 | // Initialize typeahead, if necessary
184 | if ( ! $.isEmptyObject( this.options.typeahead ) ) {
185 |
186 | var typeaheadOptions = this.options.typeahead
187 | , defaults = {
188 | minLength: this.options.showAutocompleteOnFocus ? 0 : null
189 | }
190 | , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
191 |
192 | args[0] = $.extend( {}, defaults, args[0] )
193 |
194 | this.$input.typeahead.apply( this.$input, args )
195 | this.$hint = this.$input.prev('.tt-hint')
196 | this.typeahead = true
197 | }
198 | }
199 |
200 | Tokenfield.prototype = {
201 |
202 | constructor: Tokenfield
203 |
204 | , createToken: function (attrs, triggerChange) {
205 | var _self = this
206 |
207 | if (typeof attrs === 'string') {
208 | attrs = { value: attrs, label: attrs }
209 | } else {
210 | // Copy objects to prevent contamination of data sources.
211 | attrs = $.extend( {}, attrs )
212 | }
213 |
214 | if (typeof triggerChange === 'undefined') {
215 | triggerChange = true
216 | }
217 |
218 | // Normalize label and value
219 | attrs.value = $.trim(attrs.value.toString());
220 | attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
221 |
222 | // Bail out if has no value or label, or label is too short
223 | if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
224 |
225 | // Bail out if maximum number of tokens is reached
226 | if (this.options.limit && this.getTokens().length >= this.options.limit) return
227 |
228 | // Allow changing token data before creating it
229 | var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
230 | this.$element.trigger(createEvent)
231 |
232 | // Bail out if there if attributes are empty or event was defaultPrevented
233 | if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
234 |
235 | var $token = $('')
236 | .append('')
237 | .append('×')
238 | .data('attrs', attrs)
239 |
240 | // Insert token into HTML
241 | if (this.$input.hasClass('tt-input')) {
242 | // If the input has typeahead enabled, insert token before it's parent
243 | this.$input.parent().before( $token )
244 | } else {
245 | this.$input.before( $token )
246 | }
247 |
248 | // Temporarily set input width to minimum
249 | this.$input.css('width', this.options.minWidth + 'px')
250 |
251 | var $tokenLabel = $token.find('.token-label')
252 | , $closeButton = $token.find('.close')
253 |
254 | // Determine maximum possible token label width
255 | if (!this.maxTokenWidth) {
256 | this.maxTokenWidth =
257 | this.$wrapper.width() - $closeButton.outerWidth() -
258 | parseInt($closeButton.css('margin-left'), 10) -
259 | parseInt($closeButton.css('margin-right'), 10) -
260 | parseInt($token.css('border-left-width'), 10) -
261 | parseInt($token.css('border-right-width'), 10) -
262 | parseInt($token.css('padding-left'), 10) -
263 | parseInt($token.css('padding-right'), 10)
264 | parseInt($tokenLabel.css('border-left-width'), 10) -
265 | parseInt($tokenLabel.css('border-right-width'), 10) -
266 | parseInt($tokenLabel.css('padding-left'), 10) -
267 | parseInt($tokenLabel.css('padding-right'), 10)
268 | parseInt($tokenLabel.css('margin-left'), 10) -
269 | parseInt($tokenLabel.css('margin-right'), 10)
270 | }
271 |
272 | $tokenLabel
273 | .text(attrs.label)
274 | .css('max-width', this.maxTokenWidth)
275 |
276 | // Listen to events on token
277 | $token
278 | .on('mousedown', function (e) {
279 | if (_self._disabled || _self._readonly) return false
280 | _self.preventDeactivation = true
281 | })
282 | .on('click', function (e) {
283 | if (_self._disabled || _self._readonly) return false
284 | _self.preventDeactivation = false
285 |
286 | if (e.ctrlKey || e.metaKey) {
287 | e.preventDefault()
288 | return _self.toggle( $token )
289 | }
290 |
291 | _self.activate( $token, e.shiftKey, e.shiftKey )
292 | })
293 | .on('dblclick', function (e) {
294 | if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
295 | _self.edit( $token )
296 | })
297 |
298 | $closeButton
299 | .on('click', $.proxy(this.remove, this))
300 |
301 | // Trigger createdtoken event on the original field
302 | // indicating that the token is now in the DOM
303 | this.$element.trigger($.Event('tokenfield:createdtoken', {
304 | attrs: attrs,
305 | relatedTarget: $token.get(0)
306 | }))
307 |
308 | // Trigger change event on the original field
309 | if (triggerChange) {
310 | this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
311 | }
312 |
313 | // Update tokenfield dimensions
314 | this.update()
315 |
316 | // Return original element
317 | return this.$element.get(0)
318 | }
319 |
320 | , setTokens: function (tokens, add, triggerChange) {
321 | if (!tokens) return
322 |
323 | if (!add) this.$wrapper.find('.token').remove()
324 |
325 | if (typeof triggerChange === 'undefined') {
326 | triggerChange = true
327 | }
328 |
329 | if (typeof tokens === 'string') {
330 | if (this._delimiters.length) {
331 | // Split based on delimiters
332 | tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
333 | } else {
334 | tokens = [tokens];
335 | }
336 | }
337 |
338 | var _self = this
339 | $.each(tokens, function (i, attrs) {
340 | _self.createToken(attrs, triggerChange)
341 | })
342 |
343 | return this.$element.get(0)
344 | }
345 |
346 | , getTokenData: function($token) {
347 | var data = $token.map(function() {
348 | var $token = $(this);
349 | return $token.data('attrs')
350 | }).get();
351 |
352 | if (data.length == 1) {
353 | data = data[0];
354 | }
355 |
356 | return data;
357 | }
358 |
359 | , getTokens: function(active) {
360 | var self = this
361 | , tokens = []
362 | , activeClass = active ? '.active' : '' // get active tokens only
363 | this.$wrapper.find( '.token' + activeClass ).each( function() {
364 | tokens.push( self.getTokenData( $(this) ) )
365 | })
366 | return tokens
367 | }
368 |
369 | , getTokensList: function(delimiter, beautify, active) {
370 | delimiter = delimiter || this._firstDelimiter
371 | beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
372 |
373 | var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
374 | return $.map( this.getTokens(active), function (token) {
375 | return token.value
376 | }).join(separator)
377 | }
378 |
379 | , getInput: function() {
380 | return this.$input.val()
381 | }
382 |
383 | , listen: function () {
384 | var _self = this
385 |
386 | this.$element
387 | .on('change', $.proxy(this.change, this))
388 |
389 | this.$wrapper
390 | .on('mousedown',$.proxy(this.focusInput, this))
391 |
392 | this.$input
393 | .on('focus', $.proxy(this.focus, this))
394 | .on('blur', $.proxy(this.blur, this))
395 | .on('paste', $.proxy(this.paste, this))
396 | .on('keydown', $.proxy(this.keydown, this))
397 | .on('keypress', $.proxy(this.keypress, this))
398 | .on('keyup', $.proxy(this.keyup, this))
399 |
400 | this.$copyHelper
401 | .on('focus', $.proxy(this.focus, this))
402 | .on('blur', $.proxy(this.blur, this))
403 | .on('keydown', $.proxy(this.keydown, this))
404 | .on('keyup', $.proxy(this.keyup, this))
405 |
406 | // Secondary listeners for input width calculation
407 | this.$input
408 | .on('keypress', $.proxy(this.update, this))
409 | .on('keyup', $.proxy(this.update, this))
410 |
411 | this.$input
412 | .on('autocompletecreate', function() {
413 | // Set minimum autocomplete menu width
414 | var $_menuElement = $(this).data('ui-autocomplete').menu.element
415 |
416 | var minWidth = _self.$wrapper.outerWidth() -
417 | parseInt( $_menuElement.css('border-left-width'), 10 ) -
418 | parseInt( $_menuElement.css('border-right-width'), 10 )
419 |
420 | $_menuElement.css( 'min-width', minWidth + 'px' )
421 | })
422 | .on('autocompleteselect', function (e, ui) {
423 | if (_self.createToken( ui.item )) {
424 | _self.$input.val('')
425 | if (_self.$input.data( 'edit' )) {
426 | _self.unedit(true)
427 | }
428 | }
429 | return false
430 | })
431 | .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
432 | // Create token
433 | if (_self.createToken( datum )) {
434 | _self.$input.typeahead('val', '')
435 | if (_self.$input.data( 'edit' )) {
436 | _self.unedit(true)
437 | }
438 | }
439 | })
440 |
441 | // Listen to window resize
442 | $(window).on('resize', $.proxy(this.update, this ))
443 |
444 | }
445 |
446 | , keydown: function (e) {
447 |
448 | if (!this.focused) return
449 |
450 | var _self = this
451 |
452 | switch(e.keyCode) {
453 | case 8: // backspace
454 | if (!this.$input.is(document.activeElement)) break
455 | this.lastInputValue = this.$input.val()
456 | break
457 |
458 | case 37: // left arrow
459 | leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
460 | break
461 |
462 | case 38: // up arrow
463 | upDown('prev')
464 | break
465 |
466 | case 39: // right arrow
467 | leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
468 | break
469 |
470 | case 40: // down arrow
471 | upDown('next')
472 | break
473 |
474 | case 65: // a (to handle ctrl + a)
475 | if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
476 | this.activateAll()
477 | e.preventDefault()
478 | break
479 |
480 | case 9: // tab
481 | case 13: // enter
482 |
483 | // We will handle creating tokens from autocomplete in autocomplete events
484 | if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break
485 |
486 | // We will handle creating tokens from typeahead in typeahead events
487 | if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
488 | if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
489 |
490 | // Create token
491 | if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
492 | return this.createTokensFromInput(e, this.$input.data('edit'));
493 | }
494 |
495 | // Edit token
496 | if (e.keyCode === 13) {
497 | if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
498 | if (!_self.options.allowEditing) break
499 | this.edit( this.$wrapper.find('.token.active') )
500 | }
501 | }
502 |
503 | function leftRight(direction) {
504 | if (_self.$input.is(document.activeElement)) {
505 | if (_self.$input.val().length > 0) return
506 |
507 | direction += 'All'
508 | var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
509 | if (!$token.length) return
510 |
511 | _self.preventInputFocus = true
512 | _self.preventDeactivation = true
513 |
514 | _self.activate( $token )
515 | e.preventDefault()
516 |
517 | } else {
518 | _self[direction]( e.shiftKey )
519 | e.preventDefault()
520 | }
521 | }
522 |
523 | function upDown(direction) {
524 | if (!e.shiftKey) return
525 |
526 | if (_self.$input.is(document.activeElement)) {
527 | if (_self.$input.val().length > 0) return
528 |
529 | var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
530 | if (!$token.length) return
531 |
532 | _self.activate( $token )
533 | }
534 |
535 | var opposite = direction === 'prev' ? 'next' : 'prev'
536 | , position = direction === 'prev' ? 'first' : 'last'
537 |
538 | _self.$firstActiveToken[opposite + 'All']('.token').each(function() {
539 | _self.deactivate( $(this) )
540 | })
541 |
542 | _self.activate( _self.$wrapper.find('.token:' + position), true, true )
543 | e.preventDefault()
544 | }
545 |
546 | this.lastKeyDown = e.keyCode
547 | }
548 |
549 | , keypress: function(e) {
550 |
551 | // Comma
552 | if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
553 | if (this.$input.val()) {
554 | this.createTokensFromInput(e)
555 | }
556 | return false;
557 | }
558 | }
559 |
560 | , keyup: function (e) {
561 | this.preventInputFocus = false
562 |
563 | if (!this.focused) return
564 |
565 | switch(e.keyCode) {
566 | case 8: // backspace
567 | if (this.$input.is(document.activeElement)) {
568 | if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
569 |
570 | this.preventDeactivation = true
571 | var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
572 |
573 | if (!$prevToken.length) break
574 |
575 | this.activate( $prevToken )
576 | } else {
577 | this.remove(e)
578 | }
579 | break
580 |
581 | case 46: // delete
582 | this.remove(e, 'next')
583 | break
584 | }
585 | this.lastKeyUp = e.keyCode
586 | }
587 |
588 | , focus: function (e) {
589 | this.focused = true
590 | this.$wrapper.addClass('focus')
591 |
592 | if (this.$input.is(document.activeElement)) {
593 | this.$wrapper.find('.active').removeClass('active')
594 | this.$firstActiveToken = null
595 |
596 | if (this.options.showAutocompleteOnFocus) {
597 | this.search()
598 | }
599 | }
600 | }
601 |
602 | , blur: function (e) {
603 |
604 | this.focused = false
605 | this.$wrapper.removeClass('focus')
606 |
607 | if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
608 | this.$wrapper.find('.active').removeClass('active')
609 | this.$firstActiveToken = null
610 | }
611 |
612 | if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
613 | this.createTokensFromInput(e)
614 | }
615 |
616 | this.preventDeactivation = false
617 | this.preventCreateTokens = false
618 | }
619 |
620 | , paste: function (e) {
621 | var _self = this
622 |
623 | // Add tokens to existing ones
624 | if (_self.options.allowPasting) {
625 | setTimeout(function () {
626 | _self.createTokensFromInput(e)
627 | }, 1)
628 | }
629 | }
630 |
631 | , change: function (e) {
632 | if ( e.initiator === 'tokenfield' ) return // Prevent loops
633 |
634 | this.setTokens( this.$element.val() )
635 | }
636 |
637 | , createTokensFromInput: function (e, focus) {
638 | if (this.$input.val().length < this.options.minLength)
639 | return // No input, simply return
640 |
641 | var tokensBefore = this.getTokensList()
642 | this.setTokens( this.$input.val(), true )
643 |
644 | if (tokensBefore == this.getTokensList() && this.$input.val().length)
645 | return false // No tokens were added, do nothing (prevent form submit)
646 |
647 | if (this.$input.hasClass('tt-input')) {
648 | // Typeahead acts weird when simply setting input value to empty,
649 | // so we set the query to empty instead
650 | this.$input.typeahead('val', '')
651 | } else {
652 | this.$input.val('')
653 | }
654 |
655 | if (this.$input.data( 'edit' )) {
656 | this.unedit(focus)
657 | }
658 |
659 | return false // Prevent form being submitted
660 | }
661 |
662 | , next: function (add) {
663 | if (add) {
664 | var $firstActiveToken = this.$wrapper.find('.active:first')
665 | , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
666 |
667 | if (deactivate) return this.deactivate( $firstActiveToken )
668 | }
669 |
670 | var $lastActiveToken = this.$wrapper.find('.active:last')
671 | , $nextToken = $lastActiveToken.nextAll('.token:first')
672 |
673 | if (!$nextToken.length) {
674 | this.$input.focus()
675 | return
676 | }
677 |
678 | this.activate($nextToken, add)
679 | }
680 |
681 | , prev: function (add) {
682 |
683 | if (add) {
684 | var $lastActiveToken = this.$wrapper.find('.active:last')
685 | , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
686 |
687 | if (deactivate) return this.deactivate( $lastActiveToken )
688 | }
689 |
690 | var $firstActiveToken = this.$wrapper.find('.active:first')
691 | , $prevToken = $firstActiveToken.prevAll('.token:first')
692 |
693 | if (!$prevToken.length) {
694 | $prevToken = this.$wrapper.find('.token:first')
695 | }
696 |
697 | if (!$prevToken.length && !add) {
698 | this.$input.focus()
699 | return
700 | }
701 |
702 | this.activate( $prevToken, add )
703 | }
704 |
705 | , activate: function ($token, add, multi, remember) {
706 |
707 | if (!$token) return
708 |
709 | if (typeof remember === 'undefined') var remember = true
710 |
711 | if (multi) var add = true
712 |
713 | this.$copyHelper.focus()
714 |
715 | if (!add) {
716 | this.$wrapper.find('.active').removeClass('active')
717 | if (remember) {
718 | this.$firstActiveToken = $token
719 | } else {
720 | delete this.$firstActiveToken
721 | }
722 | }
723 |
724 | if (multi && this.$firstActiveToken) {
725 | // Determine first active token and the current tokens indicies
726 | // Account for the 1 hidden textarea by subtracting 1 from both
727 | var i = this.$firstActiveToken.index() - 2
728 | , a = $token.index() - 2
729 | , _self = this
730 |
731 | this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
732 | _self.activate( $(this), true )
733 | })
734 | }
735 |
736 | $token.addClass('active')
737 | this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
738 | }
739 |
740 | , activateAll: function() {
741 | var _self = this
742 |
743 | this.$wrapper.find('.token').each( function (i) {
744 | _self.activate($(this), i !== 0, false, false)
745 | })
746 | }
747 |
748 | , deactivate: function($token) {
749 | if (!$token) return
750 |
751 | $token.removeClass('active')
752 | this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
753 | }
754 |
755 | , toggle: function($token) {
756 | if (!$token) return
757 |
758 | $token.toggleClass('active')
759 | this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
760 | }
761 |
762 | , edit: function ($token) {
763 | if (!$token) return
764 |
765 | var attrs = $token.data('attrs')
766 |
767 | // Allow changing input value before editing
768 | var options = { attrs: attrs, relatedTarget: $token.get(0) }
769 | var editEvent = $.Event('tokenfield:edittoken', options)
770 | this.$element.trigger( editEvent )
771 |
772 | // Edit event can be cancelled if default is prevented
773 | if (editEvent.isDefaultPrevented()) return
774 |
775 | $token.find('.token-label').text(attrs.value)
776 | var tokenWidth = $token.outerWidth()
777 |
778 | var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
779 |
780 | $token.replaceWith( $_input )
781 |
782 | this.preventCreateTokens = true
783 |
784 | this.$input.val( attrs.value )
785 | .select()
786 | .data( 'edit', true )
787 | .width( tokenWidth )
788 |
789 | this.update();
790 |
791 | // Indicate that token is now being edited, and is replaced with an input field in the DOM
792 | this.$element.trigger($.Event('tokenfield:editedtoken', options ))
793 | }
794 |
795 | , unedit: function (focus) {
796 | var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
797 | $_input.appendTo( this.$wrapper )
798 |
799 | this.$input.data('edit', false)
800 | this.$mirror.text('')
801 |
802 | this.update()
803 |
804 | // Because moving the input element around in DOM
805 | // will cause it to lose focus, we provide an option
806 | // to re-focus the input after appending it to the wrapper
807 | if (focus) {
808 | var _self = this
809 | setTimeout(function () {
810 | _self.$input.focus()
811 | }, 1)
812 | }
813 | }
814 |
815 | , remove: function (e, direction) {
816 | if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
817 |
818 | var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
819 |
820 | if (e.type !== 'click') {
821 | if (!direction) var direction = 'prev'
822 | this[direction]()
823 |
824 | // Was it the first token?
825 | if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
826 | }
827 |
828 | // Prepare events and their options
829 | var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
830 | , removeEvent = $.Event('tokenfield:removetoken', options)
831 |
832 | this.$element.trigger(removeEvent);
833 |
834 | // Remove event can be intercepted and cancelled
835 | if (removeEvent.isDefaultPrevented()) return
836 |
837 | var removedEvent = $.Event('tokenfield:removedtoken', options)
838 | , changeEvent = $.Event('change', { initiator: 'tokenfield' })
839 |
840 | // Remove token from DOM
841 | $token.remove()
842 |
843 | // Trigger events
844 | this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
845 |
846 | // Focus, when necessary:
847 | // When there are no more tokens, or if this was the first token
848 | // and it was removed with backspace or it was clicked on
849 | if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
850 |
851 | // Adjust input width
852 | this.$input.css('width', this.options.minWidth + 'px')
853 | this.update()
854 |
855 | // Cancel original event handlers
856 | e.preventDefault()
857 | e.stopPropagation()
858 | }
859 |
860 | /**
861 | * Update tokenfield dimensions
862 | */
863 | , update: function (e) {
864 | var value = this.$input.val()
865 | , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
866 | , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
867 | , inputPadding = inputPaddingLeft + inputPaddingRight
868 |
869 | if (this.$input.data('edit')) {
870 |
871 | if (!value) {
872 | value = this.$input.prop("placeholder")
873 | }
874 | if (value === this.$mirror.text()) return
875 |
876 | this.$mirror.text(value)
877 |
878 | var mirrorWidth = this.$mirror.width() + 10;
879 | if ( mirrorWidth > this.$wrapper.width() ) {
880 | return this.$input.width( this.$wrapper.width() )
881 | }
882 |
883 | this.$input.width( mirrorWidth )
884 |
885 | if (this.$hint) {
886 | this.$hint.width( mirrorWidth )
887 | }
888 | }
889 | else {
890 | var w = (this.textDirection === 'rtl')
891 | ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
892 | : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
893 | //
894 | // some usecases pre-render widget before attaching to DOM,
895 | // dimensions returned by jquery will be NaN -> we default to 100%
896 | // so placeholder won't be cut off.
897 | isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
898 |
899 | if (this.$hint) {
900 | isNaN(w) ? this.$hint.width('100%') : this.$hint.width(w);
901 | }
902 | }
903 | }
904 |
905 | , focusInput: function (e) {
906 | if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
907 | // Focus only after the current call stack has cleared,
908 | // otherwise has no effect.
909 | // Reason: mousedown is too early - input will lose focus
910 | // after mousedown. However, since the input may be moved
911 | // in DOM, there may be no click or mouseup event triggered.
912 | var _self = this
913 | setTimeout(function() {
914 | _self.$input.focus()
915 | }, 0)
916 | }
917 |
918 | , search: function () {
919 | if ( this.$input.data('ui-autocomplete') ) {
920 | this.$input.autocomplete('search')
921 | }
922 | }
923 |
924 | , disable: function () {
925 | this.setProperty('disabled', true);
926 | }
927 |
928 | , enable: function () {
929 | this.setProperty('disabled', false);
930 | }
931 |
932 | , readonly: function () {
933 | this.setProperty('readonly', true);
934 | }
935 |
936 | , writeable: function () {
937 | this.setProperty('readonly', false);
938 | }
939 |
940 | , setProperty: function(property, value) {
941 | this['_' + property] = value;
942 | this.$input.prop(property, value);
943 | this.$element.prop(property, value);
944 | this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
945 | }
946 |
947 | , destroy: function() {
948 | // Set field value
949 | this.$element.val( this.getTokensList() );
950 | // Restore styles and properties
951 | this.$element.css( this.$element.data('original-styles') );
952 | this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
953 |
954 | // Re-route tokenfield label to original input
955 | var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
956 | if ( $label.length ) {
957 | $label.prop( 'for', this.$element.prop('id') )
958 | }
959 |
960 | // Move original element outside of tokenfield wrapper
961 | this.$element.insertBefore( this.$wrapper );
962 |
963 | // Remove tokenfield-related data
964 | this.$element.removeData('original-styles')
965 | .removeData('original-tabindex')
966 | .removeData('bs.tokenfield');
967 |
968 | // Remove tokenfield from DOM
969 | this.$wrapper.remove();
970 | this.$mirror.remove();
971 |
972 | var $_element = this.$element;
973 |
974 | return $_element;
975 | }
976 |
977 | }
978 |
979 |
980 | /* TOKENFIELD PLUGIN DEFINITION
981 | * ======================== */
982 |
983 | var old = $.fn.tokenfield
984 |
985 | $.fn.tokenfield = function (option, param) {
986 | var value
987 | , args = []
988 |
989 | Array.prototype.push.apply( args, arguments );
990 |
991 | var elements = this.each(function () {
992 | var $this = $(this)
993 | , data = $this.data('bs.tokenfield')
994 | , options = typeof option == 'object' && option
995 |
996 | if (typeof option === 'string' && data && data[option]) {
997 | args.shift()
998 | value = data[option].apply(data, args)
999 | } else {
1000 | if (!data && typeof option !== 'string' && !param) {
1001 | $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
1002 | $this.trigger('tokenfield:initialize')
1003 | }
1004 | }
1005 | })
1006 |
1007 | return typeof value !== 'undefined' ? value : elements;
1008 | }
1009 |
1010 | $.fn.tokenfield.defaults = {
1011 | minWidth: 60,
1012 | minLength: 0,
1013 | allowEditing: true,
1014 | allowPasting: true,
1015 | limit: 0,
1016 | autocomplete: {},
1017 | typeahead: {},
1018 | showAutocompleteOnFocus: false,
1019 | createTokensOnBlur: false,
1020 | delimiter: ',',
1021 | beautify: true,
1022 | inputType: 'text'
1023 | }
1024 |
1025 | $.fn.tokenfield.Constructor = Tokenfield
1026 |
1027 |
1028 | /* TOKENFIELD NO CONFLICT
1029 | * ================== */
1030 |
1031 | $.fn.tokenfield.noConflict = function () {
1032 | $.fn.tokenfield = old
1033 | return this
1034 | }
1035 |
1036 | return Tokenfield;
1037 |
1038 | }));
1039 |
--------------------------------------------------------------------------------