scratch – Blame information for rev 134

Subversion Repositories:
Rev:
Rev Author Line No. Line
134 office 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 = $('<div class="tokenfield form-control" />')
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 = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />')
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 = $('<input type="text" />').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 = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
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.typeahead = true
196 }
197 }
198  
199 Tokenfield.prototype = {
200  
201 constructor: Tokenfield
202  
203 , createToken: function (attrs, triggerChange) {
204 var _self = this
205  
206 if (typeof attrs === 'string') {
207 attrs = { value: attrs, label: attrs }
208 } else {
209 // Copy objects to prevent contamination of data sources.
210 attrs = $.extend( {}, attrs )
211 }
212  
213 if (typeof triggerChange === 'undefined') {
214 triggerChange = true
215 }
216  
217 // Normalize label and value
218 attrs.value = $.trim(attrs.value.toString());
219 attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
220  
221 // Bail out if has no value or label, or label is too short
222 if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
223  
224 // Bail out if maximum number of tokens is reached
225 if (this.options.limit && this.getTokens().length >= this.options.limit) return
226  
227 // Allow changing token data before creating it
228 var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
229 this.$element.trigger(createEvent)
230  
231 // Bail out if there if attributes are empty or event was defaultPrevented
232 if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
233  
234 var $token = $('<div class="token" />')
235 .append('<span class="token-label" />')
236 .append('<a href="#" class="close" tabindex="-1">&times;</a>')
237 .data('attrs', attrs)
238  
239 // Insert token into HTML
240 if (this.$input.hasClass('tt-input')) {
241 // If the input has typeahead enabled, insert token before it's parent
242 this.$input.parent().before( $token )
243 } else {
244 this.$input.before( $token )
245 }
246  
247 // Temporarily set input width to minimum
248 this.$input.css('width', this.options.minWidth + 'px')
249  
250 var $tokenLabel = $token.find('.token-label')
251 , $closeButton = $token.find('.close')
252  
253 // Determine maximum possible token label width
254 if (!this.maxTokenWidth) {
255 this.maxTokenWidth =
256 this.$wrapper.width() - $closeButton.outerWidth() -
257 parseInt($closeButton.css('margin-left'), 10) -
258 parseInt($closeButton.css('margin-right'), 10) -
259 parseInt($token.css('border-left-width'), 10) -
260 parseInt($token.css('border-right-width'), 10) -
261 parseInt($token.css('padding-left'), 10) -
262 parseInt($token.css('padding-right'), 10)
263 parseInt($tokenLabel.css('border-left-width'), 10) -
264 parseInt($tokenLabel.css('border-right-width'), 10) -
265 parseInt($tokenLabel.css('padding-left'), 10) -
266 parseInt($tokenLabel.css('padding-right'), 10)
267 parseInt($tokenLabel.css('margin-left'), 10) -
268 parseInt($tokenLabel.css('margin-right'), 10)
269 }
270  
271 $tokenLabel
272 .text(attrs.label)
273 .css('max-width', this.maxTokenWidth)
274  
275 // Listen to events on token
276 $token
277 .on('mousedown', function (e) {
278 if (_self._disabled || _self._readonly) return false
279 _self.preventDeactivation = true
280 })
281 .on('click', function (e) {
282 if (_self._disabled || _self._readonly) return false
283 _self.preventDeactivation = false
284  
285 if (e.ctrlKey || e.metaKey) {
286 e.preventDefault()
287 return _self.toggle( $token )
288 }
289  
290 _self.activate( $token, e.shiftKey, e.shiftKey )
291 })
292 .on('dblclick', function (e) {
293 if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
294 _self.edit( $token )
295 })
296  
297 $closeButton
298 .on('click', $.proxy(this.remove, this))
299  
300 // Trigger createdtoken event on the original field
301 // indicating that the token is now in the DOM
302 this.$element.trigger($.Event('tokenfield:createdtoken', {
303 attrs: attrs,
304 relatedTarget: $token.get(0)
305 }))
306  
307 // Trigger change event on the original field
308 if (triggerChange) {
309 this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
310 }
311  
312 // Update tokenfield dimensions
313 this.update()
314  
315 // Return original element
316 return this.$element.get(0)
317 }
318  
319 , setTokens: function (tokens, add, triggerChange) {
320 if (!tokens) return
321  
322 if (!add) this.$wrapper.find('.token').remove()
323  
324 if (typeof triggerChange === 'undefined') {
325 triggerChange = true
326 }
327  
328 if (typeof tokens === 'string') {
329 if (this._delimiters.length) {
330 // Split based on delimiters
331 tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
332 } else {
333 tokens = [tokens];
334 }
335 }
336  
337 var _self = this
338 $.each(tokens, function (i, attrs) {
339 _self.createToken(attrs, triggerChange)
340 })
341  
342 return this.$element.get(0)
343 }
344  
345 , getTokenData: function($token) {
346 var data = $token.map(function() {
347 var $token = $(this);
348 return $token.data('attrs')
349 }).get();
350  
351 if (data.length == 1) {
352 data = data[0];
353 }
354  
355 return data;
356 }
357  
358 , getTokens: function(active) {
359 var self = this
360 , tokens = []
361 , activeClass = active ? '.active' : '' // get active tokens only
362 this.$wrapper.find( '.token' + activeClass ).each( function() {
363 tokens.push( self.getTokenData( $(this) ) )
364 })
365 return tokens
366 }
367  
368 , getTokensList: function(delimiter, beautify, active) {
369 delimiter = delimiter || this._firstDelimiter
370 beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
371  
372 var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
373 return $.map( this.getTokens(active), function (token) {
374 return token.value
375 }).join(separator)
376 }
377  
378 , getInput: function() {
379 return this.$input.val()
380 }
381  
382 , listen: function () {
383 var _self = this
384  
385 this.$element
386 .on('change', $.proxy(this.change, this))
387  
388 this.$wrapper
389 .on('mousedown',$.proxy(this.focusInput, this))
390  
391 this.$input
392 .on('focus', $.proxy(this.focus, this))
393 .on('blur', $.proxy(this.blur, this))
394 .on('paste', $.proxy(this.paste, this))
395 .on('keydown', $.proxy(this.keydown, this))
396 .on('keypress', $.proxy(this.keypress, this))
397 .on('keyup', $.proxy(this.keyup, this))
398  
399 this.$copyHelper
400 .on('focus', $.proxy(this.focus, this))
401 .on('blur', $.proxy(this.blur, this))
402 .on('keydown', $.proxy(this.keydown, this))
403 .on('keyup', $.proxy(this.keyup, this))
404  
405 // Secondary listeners for input width calculation
406 this.$input
407 .on('keypress', $.proxy(this.update, this))
408 .on('keyup', $.proxy(this.update, this))
409  
410 this.$input
411 .on('autocompletecreate', function() {
412 // Set minimum autocomplete menu width
413 var $_menuElement = $(this).data('ui-autocomplete').menu.element
414  
415 var minWidth = _self.$wrapper.outerWidth() -
416 parseInt( $_menuElement.css('border-left-width'), 10 ) -
417 parseInt( $_menuElement.css('border-right-width'), 10 )
418  
419 $_menuElement.css( 'min-width', minWidth + 'px' )
420 })
421 .on('autocompleteselect', function (e, ui) {
422 if (_self.createToken( ui.item )) {
423 _self.$input.val('')
424 if (_self.$input.data( 'edit' )) {
425 _self.unedit(true)
426 }
427 }
428 return false
429 })
430 .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
431 // Create token
432 if (_self.createToken( datum )) {
433 _self.$input.typeahead('val', '')
434 if (_self.$input.data( 'edit' )) {
435 _self.unedit(true)
436 }
437 }
438 })
439  
440 // Listen to window resize
441 $(window).on('resize', $.proxy(this.update, this ))
442  
443 }
444  
445 , keydown: function (e) {
446  
447 if (!this.focused) return
448  
449 var _self = this
450  
451 switch(e.keyCode) {
452 case 8: // backspace
453 if (!this.$input.is(document.activeElement)) break
454 this.lastInputValue = this.$input.val()
455 break
456  
457 case 37: // left arrow
458 leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
459 break
460  
461 case 38: // up arrow
462 upDown('prev')
463 break
464  
465 case 39: // right arrow
466 leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
467 break
468  
469 case 40: // down arrow
470 upDown('next')
471 break
472  
473 case 65: // a (to handle ctrl + a)
474 if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
475 this.activateAll()
476 e.preventDefault()
477 break
478  
479 case 9: // tab
480 case 13: // enter
481  
482 // We will handle creating tokens from autocomplete in autocomplete events
483 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
484  
485 // We will handle creating tokens from typeahead in typeahead events
486 if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
487 if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
488  
489 // Create token
490 if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
491 return this.createTokensFromInput(e, this.$input.data('edit'));
492 }
493  
494 // Edit token
495 if (e.keyCode === 13) {
496 if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
497 if (!_self.options.allowEditing) break
498 this.edit( this.$wrapper.find('.token.active') )
499 }
500 }
501  
502 function leftRight(direction) {
503 if (_self.$input.is(document.activeElement)) {
504 if (_self.$input.val().length > 0) return
505  
506 direction += 'All'
507 var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
508 if (!$token.length) return
509  
510 _self.preventInputFocus = true
511 _self.preventDeactivation = true
512  
513 _self.activate( $token )
514 e.preventDefault()
515  
516 } else {
517 _self[direction]( e.shiftKey )
518 e.preventDefault()
519 }
520 }
521  
522 function upDown(direction) {
523 if (!e.shiftKey) return
524  
525 if (_self.$input.is(document.activeElement)) {
526 if (_self.$input.val().length > 0) return
527  
528 var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
529 if (!$token.length) return
530  
531 _self.activate( $token )
532 }
533  
534 var opposite = direction === 'prev' ? 'next' : 'prev'
535 , position = direction === 'prev' ? 'first' : 'last'
536  
537 _self.$firstActiveToken[opposite + 'All']('.token').each(function() {
538 _self.deactivate( $(this) )
539 })
540  
541 _self.activate( _self.$wrapper.find('.token:' + position), true, true )
542 e.preventDefault()
543 }
544  
545 this.lastKeyDown = e.keyCode
546 }
547  
548 , keypress: function(e) {
549  
550 // Comma
551 if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
552 if (this.$input.val()) {
553 this.createTokensFromInput(e)
554 }
555 return false;
556 }
557 }
558  
559 , keyup: function (e) {
560 this.preventInputFocus = false
561  
562 if (!this.focused) return
563  
564 switch(e.keyCode) {
565 case 8: // backspace
566 if (this.$input.is(document.activeElement)) {
567 if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
568  
569 this.preventDeactivation = true
570 var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
571  
572 if (!$prevToken.length) break
573  
574 this.activate( $prevToken )
575 } else {
576 this.remove(e)
577 }
578 break
579  
580 case 46: // delete
581 this.remove(e, 'next')
582 break
583 }
584 this.lastKeyUp = e.keyCode
585 }
586  
587 , focus: function (e) {
588 this.focused = true
589 this.$wrapper.addClass('focus')
590  
591 if (this.$input.is(document.activeElement)) {
592 this.$wrapper.find('.active').removeClass('active')
593 this.$firstActiveToken = null
594  
595 if (this.options.showAutocompleteOnFocus) {
596 this.search()
597 }
598 }
599 }
600  
601 , blur: function (e) {
602  
603 this.focused = false
604 this.$wrapper.removeClass('focus')
605  
606 if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
607 this.$wrapper.find('.active').removeClass('active')
608 this.$firstActiveToken = null
609 }
610  
611 if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
612 this.createTokensFromInput(e)
613 }
614  
615 this.preventDeactivation = false
616 this.preventCreateTokens = false
617 }
618  
619 , paste: function (e) {
620 var _self = this
621  
622 // Add tokens to existing ones
623 if (_self.options.allowPasting) {
624 setTimeout(function () {
625 _self.createTokensFromInput(e)
626 }, 1)
627 }
628 }
629  
630 , change: function (e) {
631 if ( e.initiator === 'tokenfield' ) return // Prevent loops
632  
633 this.setTokens( this.$element.val() )
634 }
635  
636 , createTokensFromInput: function (e, focus) {
637 if (this.$input.val().length < this.options.minLength)
638 return // No input, simply return
639  
640 var tokensBefore = this.getTokensList()
641 this.setTokens( this.$input.val(), true )
642  
643 if (tokensBefore == this.getTokensList() && this.$input.val().length)
644 return false // No tokens were added, do nothing (prevent form submit)
645  
646 if (this.$input.hasClass('tt-input')) {
647 // Typeahead acts weird when simply setting input value to empty,
648 // so we set the query to empty instead
649 this.$input.typeahead('val', '')
650 } else {
651 this.$input.val('')
652 }
653  
654 if (this.$input.data( 'edit' )) {
655 this.unedit(focus)
656 }
657  
658 return false // Prevent form being submitted
659 }
660  
661 , next: function (add) {
662 if (add) {
663 var $firstActiveToken = this.$wrapper.find('.active:first')
664 , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
665  
666 if (deactivate) return this.deactivate( $firstActiveToken )
667 }
668  
669 var $lastActiveToken = this.$wrapper.find('.active:last')
670 , $nextToken = $lastActiveToken.nextAll('.token:first')
671  
672 if (!$nextToken.length) {
673 this.$input.focus()
674 return
675 }
676  
677 this.activate($nextToken, add)
678 }
679  
680 , prev: function (add) {
681  
682 if (add) {
683 var $lastActiveToken = this.$wrapper.find('.active:last')
684 , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
685  
686 if (deactivate) return this.deactivate( $lastActiveToken )
687 }
688  
689 var $firstActiveToken = this.$wrapper.find('.active:first')
690 , $prevToken = $firstActiveToken.prevAll('.token:first')
691  
692 if (!$prevToken.length) {
693 $prevToken = this.$wrapper.find('.token:first')
694 }
695  
696 if (!$prevToken.length && !add) {
697 this.$input.focus()
698 return
699 }
700  
701 this.activate( $prevToken, add )
702 }
703  
704 , activate: function ($token, add, multi, remember) {
705  
706 if (!$token) return
707  
708 if (typeof remember === 'undefined') var remember = true
709  
710 if (multi) var add = true
711  
712 this.$copyHelper.focus()
713  
714 if (!add) {
715 this.$wrapper.find('.active').removeClass('active')
716 if (remember) {
717 this.$firstActiveToken = $token
718 } else {
719 delete this.$firstActiveToken
720 }
721 }
722  
723 if (multi && this.$firstActiveToken) {
724 // Determine first active token and the current tokens indicies
725 // Account for the 1 hidden textarea by subtracting 1 from both
726 var i = this.$firstActiveToken.index() - 2
727 , a = $token.index() - 2
728 , _self = this
729  
730 this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
731 _self.activate( $(this), true )
732 })
733 }
734  
735 $token.addClass('active')
736 this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
737 }
738  
739 , activateAll: function() {
740 var _self = this
741  
742 this.$wrapper.find('.token').each( function (i) {
743 _self.activate($(this), i !== 0, false, false)
744 })
745 }
746  
747 , deactivate: function($token) {
748 if (!$token) return
749  
750 $token.removeClass('active')
751 this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
752 }
753  
754 , toggle: function($token) {
755 if (!$token) return
756  
757 $token.toggleClass('active')
758 this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
759 }
760  
761 , edit: function ($token) {
762 if (!$token) return
763  
764 var attrs = $token.data('attrs')
765  
766 // Allow changing input value before editing
767 var options = { attrs: attrs, relatedTarget: $token.get(0) }
768 var editEvent = $.Event('tokenfield:edittoken', options)
769 this.$element.trigger( editEvent )
770  
771 // Edit event can be cancelled if default is prevented
772 if (editEvent.isDefaultPrevented()) return
773  
774 $token.find('.token-label').text(attrs.value)
775 var tokenWidth = $token.outerWidth()
776  
777 var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
778  
779 $token.replaceWith( $_input )
780  
781 this.preventCreateTokens = true
782  
783 this.$input.val( attrs.value )
784 .select()
785 .data( 'edit', true )
786 .width( tokenWidth )
787  
788 this.update();
789  
790 // Indicate that token is now being edited, and is replaced with an input field in the DOM
791 this.$element.trigger($.Event('tokenfield:editedtoken', options ))
792 }
793  
794 , unedit: function (focus) {
795 var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
796 $_input.appendTo( this.$wrapper )
797  
798 this.$input.data('edit', false)
799 this.$mirror.text('')
800  
801 this.update()
802  
803 // Because moving the input element around in DOM
804 // will cause it to lose focus, we provide an option
805 // to re-focus the input after appending it to the wrapper
806 if (focus) {
807 var _self = this
808 setTimeout(function () {
809 _self.$input.focus()
810 }, 1)
811 }
812 }
813  
814 , remove: function (e, direction) {
815 if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
816  
817 var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
818  
819 if (e.type !== 'click') {
820 if (!direction) var direction = 'prev'
821 this[direction]()
822  
823 // Was it the first token?
824 if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
825 }
826  
827 // Prepare events and their options
828 var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
829 , removeEvent = $.Event('tokenfield:removetoken', options)
830  
831 this.$element.trigger(removeEvent);
832  
833 // Remove event can be intercepted and cancelled
834 if (removeEvent.isDefaultPrevented()) return
835  
836 var removedEvent = $.Event('tokenfield:removedtoken', options)
837 , changeEvent = $.Event('change', { initiator: 'tokenfield' })
838  
839 // Remove token from DOM
840 $token.remove()
841  
842 // Trigger events
843 this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
844  
845 // Focus, when necessary:
846 // When there are no more tokens, or if this was the first token
847 // and it was removed with backspace or it was clicked on
848 if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
849  
850 // Adjust input width
851 this.$input.css('width', this.options.minWidth + 'px')
852 this.update()
853  
854 // Cancel original event handlers
855 e.preventDefault()
856 e.stopPropagation()
857 }
858  
859 /**
860 * Update tokenfield dimensions
861 */
862 , update: function (e) {
863 var value = this.$input.val()
864 , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
865 , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
866 , inputPadding = inputPaddingLeft + inputPaddingRight
867  
868 if (this.$input.data('edit')) {
869  
870 if (!value) {
871 value = this.$input.prop("placeholder")
872 }
873 if (value === this.$mirror.text()) return
874  
875 this.$mirror.text(value)
876  
877 var mirrorWidth = this.$mirror.width() + 10;
878 if ( mirrorWidth > this.$wrapper.width() ) {
879 return this.$input.width( this.$wrapper.width() )
880 }
881  
882 this.$input.width( mirrorWidth )
883 }
884 else {
885 var w = (this.textDirection === 'rtl')
886 ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
887 : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
888 //
889 // some usecases pre-render widget before attaching to DOM,
890 // dimensions returned by jquery will be NaN -> we default to 100%
891 // so placeholder won't be cut off.
892 isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
893 }
894 }
895  
896 , focusInput: function (e) {
897 if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
898 // Focus only after the current call stack has cleared,
899 // otherwise has no effect.
900 // Reason: mousedown is too early - input will lose focus
901 // after mousedown. However, since the input may be moved
902 // in DOM, there may be no click or mouseup event triggered.
903 var _self = this
904 setTimeout(function() {
905 _self.$input.focus()
906 }, 0)
907 }
908  
909 , search: function () {
910 if ( this.$input.data('ui-autocomplete') ) {
911 this.$input.autocomplete('search')
912 }
913 }
914  
915 , disable: function () {
916 this.setProperty('disabled', true);
917 }
918  
919 , enable: function () {
920 this.setProperty('disabled', false);
921 }
922  
923 , readonly: function () {
924 this.setProperty('readonly', true);
925 }
926  
927 , writeable: function () {
928 this.setProperty('readonly', false);
929 }
930  
931 , setProperty: function(property, value) {
932 this['_' + property] = value;
933 this.$input.prop(property, value);
934 this.$element.prop(property, value);
935 this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
936 }
937  
938 , destroy: function() {
939 // Set field value
940 this.$element.val( this.getTokensList() );
941 // Restore styles and properties
942 this.$element.css( this.$element.data('original-styles') );
943 this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
944  
945 // Re-route tokenfield label to original input
946 var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
947 if ( $label.length ) {
948 $label.prop( 'for', this.$element.prop('id') )
949 }
950  
951 // Move original element outside of tokenfield wrapper
952 this.$element.insertBefore( this.$wrapper );
953  
954 // Remove tokenfield-related data
955 this.$element.removeData('original-styles')
956 .removeData('original-tabindex')
957 .removeData('bs.tokenfield');
958  
959 // Remove tokenfield from DOM
960 this.$wrapper.remove();
961 this.$mirror.remove();
962  
963 var $_element = this.$element;
964  
965 return $_element;
966 }
967  
968 }
969  
970  
971 /* TOKENFIELD PLUGIN DEFINITION
972 * ======================== */
973  
974 var old = $.fn.tokenfield
975  
976 $.fn.tokenfield = function (option, param) {
977 var value
978 , args = []
979  
980 Array.prototype.push.apply( args, arguments );
981  
982 var elements = this.each(function () {
983 var $this = $(this)
984 , data = $this.data('bs.tokenfield')
985 , options = typeof option == 'object' && option
986  
987 if (typeof option === 'string' && data && data[option]) {
988 args.shift()
989 value = data[option].apply(data, args)
990 } else {
991 if (!data && typeof option !== 'string' && !param) {
992 $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
993 $this.trigger('tokenfield:initialize')
994 }
995 }
996 })
997  
998 return typeof value !== 'undefined' ? value : elements;
999 }
1000  
1001 $.fn.tokenfield.defaults = {
1002 minWidth: 60,
1003 minLength: 0,
1004 allowEditing: true,
1005 allowPasting: true,
1006 limit: 0,
1007 autocomplete: {},
1008 typeahead: {},
1009 showAutocompleteOnFocus: false,
1010 createTokensOnBlur: false,
1011 delimiter: ',',
1012 beautify: true,
1013 inputType: 'text'
1014 }
1015  
1016 $.fn.tokenfield.Constructor = Tokenfield
1017  
1018  
1019 /* TOKENFIELD NO CONFLICT
1020 * ================== */
1021  
1022 $.fn.tokenfield.noConflict = function () {
1023 $.fn.tokenfield = old
1024 return this
1025 }
1026  
1027 return Tokenfield;
1028  
1029 }));