Updated to latest version.

This commit is contained in:
Martin Edenhofer 2016-06-02 00:38:36 +02:00
parent 26cd94acab
commit 5977b9e348

451
app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js vendored Executable file → Normal file
View file

@ -25,7 +25,7 @@
}; };
} else { } else {
// Browser globals // Browser globals
factory(jQuery); factory(jQuery, window);
} }
}(function ($, window) { }(function ($, window) {
@ -63,9 +63,9 @@
} }
var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
$.each(this._delimiters, function (index, char) { $.each(this._delimiters, function (index, character) {
var pos = $.inArray(char, specialCharacters) var pos = $.inArray(character, specialCharacters)
if (pos >= 0) _self._delimiters[index] = '\\' + char; if (pos >= 0) _self._delimiters[index] = '\\' + character;
}); });
// Store original input width // Store original input width
@ -102,7 +102,7 @@
// Create a new input // Create a new input
var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
this.$input = $('<input type="text" class="token-input" autocomplete="off" />') this.$input = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />')
.appendTo( this.$wrapper ) .appendTo( this.$wrapper )
.prop( 'placeholder', this.$element.prop('placeholder') ) .prop( 'placeholder', this.$element.prop('placeholder') )
.prop( 'id', id + '-tokenfield' ) .prop( 'id', id + '-tokenfield' )
@ -134,8 +134,13 @@
this.disable(); this.disable();
} }
// Set tokenfield readonly, if original input is readonly
if (this.$element.prop('readonly')) {
this.readonly();
}
// Set up mirror for input auto-sizing // Set up mirror for input auto-sizing
this.$mirror = $('<span class="js-tokenfieldMirror" style="position:absolute; top:-999px; left:0; white-space:pre;"/>'); this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
this.$input.css('min-width', this.options.minWidth + 'px') this.$input.css('min-width', this.options.minWidth + 'px')
$.each([ $.each([
'fontFamily', 'fontFamily',
@ -149,12 +154,7 @@
], function (i, val) { ], function (i, val) {
_self.$mirror[0].style[val] = _self.$input.css(val); _self.$mirror[0].style[val] = _self.$input.css(val);
}); });
if (!$('.js-tokenfieldMirror').get(0)) {
this.$mirror.appendTo( 'body' ) this.$mirror.appendTo( 'body' )
}
else {
this.$mirror = $('.js-tokenfieldMirror')
}
// Insert tokenfield to HTML // Insert tokenfield to HTML
this.$wrapper.insertBefore( this.$element ) this.$wrapper.insertBefore( this.$element )
@ -164,7 +164,7 @@
this.update() this.update()
// Create initial tokens, if any // Create initial tokens, if any
this.setTokens(this.options.tokens, false, false) this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
// Start listening to events // Start listening to events
this.listen() this.listen()
@ -172,23 +172,28 @@
// Initialize autocomplete, if necessary // Initialize autocomplete, if necessary
if ( ! $.isEmptyObject( this.options.autocomplete ) ) { if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
var side = this.textDirection === 'rtl' ? 'right' : 'left' var side = this.textDirection === 'rtl' ? 'right' : 'left'
var autocompleteOptions = $.extend({ , autocompleteOptions = $.extend({
minLength: this.options.showAutocompleteOnFocus ? 0 : null, minLength: this.options.showAutocompleteOnFocus ? 0 : null,
position: { my: side + " top", at: side + " bottom", of: this.$wrapper } position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
}, this.options.autocomplete ) }, this.options.autocomplete )
this.$input.autocomplete( autocompleteOptions ) this.$input.autocomplete( autocompleteOptions )
} }
// Initialize typeahead, if necessary // Initialize typeahead, if necessary
if ( ! $.isEmptyObject( this.options.typeahead ) ) { if ( ! $.isEmptyObject( this.options.typeahead ) ) {
var typeaheadOptions = $.extend({
var typeaheadOptions = this.options.typeahead
, defaults = {
minLength: this.options.showAutocompleteOnFocus ? 0 : null minLength: this.options.showAutocompleteOnFocus ? 0 : null
}, this.options.typeahead) }
this.$input.typeahead( null, typeaheadOptions ) , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
args[0] = $.extend( {}, defaults, args[0] )
this.$input.typeahead.apply( this.$input, args )
this.typeahead = true this.typeahead = true
} }
this.$element.trigger('tokenfield:initialize')
} }
Tokenfield.prototype = { Tokenfield.prototype = {
@ -196,137 +201,131 @@
constructor: Tokenfield constructor: Tokenfield
, createToken: function (attrs, triggerChange) { , createToken: function (attrs, triggerChange) {
var _self = this
if (typeof attrs === 'string') { if (typeof attrs === 'string') {
attrs = { value: attrs, label: attrs } attrs = { value: attrs, label: attrs }
} else {
// Copy objects to prevent contamination of data sources.
attrs = $.extend( {}, attrs )
} }
if (typeof triggerChange === 'undefined') { if (typeof triggerChange === 'undefined') {
triggerChange = true triggerChange = true
} }
var _self = this // Normalize label and value
, value = $.trim(attrs.value) attrs.value = $.trim(attrs.value.toString());
, label = attrs.label && attrs.label.length ? $.trim(attrs.label) : value attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
if (!value.length || !label.length || value.length < this.options.minLength) return // Bail out if has no value or label, or label is too short
if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
// Bail out if maximum number of tokens is reached
if (this.options.limit && this.getTokens().length >= this.options.limit) return if (this.options.limit && this.getTokens().length >= this.options.limit) return
// Allow changing token data before creating it // Allow changing token data before creating it
var prepareEvent = $.Event('tokenfield:preparetoken') var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
prepareEvent.token = { this.$element.trigger(createEvent)
value: value,
label: label
}
this.$element.trigger( prepareEvent )
if (!prepareEvent.token) return // Bail out if there if attributes are empty or event was defaultPrevented
if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
value = prepareEvent.token.value var $token = $('<div class="token" />')
label = prepareEvent.token.label
// Check for duplicates
if (!this.options.allowDuplicates && $.grep(this.getTokens(), function (token) {
return token.value === value
}).length) {
// Allow listening to when duplicates get prevented
var preventDuplicateEvent = $.Event('tokenfield:preventduplicate')
preventDuplicateEvent.token = {
value: value,
label: label
}
this.$element.trigger( preventDuplicateEvent )
// Add duplicate warning class to existing token for 250ms
var duplicate = this.$wrapper.find( '.token[data-value="' + value + '"]' ).addClass('duplicate')
setTimeout(function() {
duplicate.removeClass('duplicate');
}, 250)
return false
}
var token = $('<div class="token" />')
.attr('data-value', value)
.append('<span class="token-label" />') .append('<span class="token-label" />')
.append('<a href="#" class="close" tabindex="-1">&times;</a>') .append('<a href="#" class="close" tabindex="-1">&times;</a>')
.data('attrs', attrs)
// Insert token into HTML // Insert token into HTML
if (this.$input.hasClass('tt-input')) { if (this.$input.hasClass('tt-input')) {
this.$input.parent().before( token ) // If the input has typeahead enabled, insert token before it's parent
this.$input.parent().before( $token )
} else { } else {
this.$input.before( token ) this.$input.before( $token )
} }
// Temporarily set input width to minimum
this.$input.css('width', this.options.minWidth + 'px') this.$input.css('width', this.options.minWidth + 'px')
var tokenLabel = token.find('.token-label') var $tokenLabel = $token.find('.token-label')
, closeButton = token.find('.close') , $closeButton = $token.find('.close')
// Determine maximum possible token label width // Determine maximum possible token label width
if (!this.maxTokenWidth) { if (!this.maxTokenWidth) {
this.maxTokenWidth = this.maxTokenWidth =
this.$wrapper.width() - closeButton.outerWidth() - this.$wrapper.width() - $closeButton.outerWidth() -
parseInt(closeButton.css('margin-left'), 10) - parseInt($closeButton.css('margin-left'), 10) -
parseInt(closeButton.css('margin-right'), 10) - parseInt($closeButton.css('margin-right'), 10) -
parseInt(token.css('border-left-width'), 10) - parseInt($token.css('border-left-width'), 10) -
parseInt(token.css('border-right-width'), 10) - parseInt($token.css('border-right-width'), 10) -
parseInt(token.css('padding-left'), 10) - parseInt($token.css('padding-left'), 10) -
parseInt(token.css('padding-right'), 10) parseInt($token.css('padding-right'), 10)
parseInt(tokenLabel.css('border-left-width'), 10) - parseInt($tokenLabel.css('border-left-width'), 10) -
parseInt(tokenLabel.css('border-right-width'), 10) - parseInt($tokenLabel.css('border-right-width'), 10) -
parseInt(tokenLabel.css('padding-left'), 10) - parseInt($tokenLabel.css('padding-left'), 10) -
parseInt(tokenLabel.css('padding-right'), 10) parseInt($tokenLabel.css('padding-right'), 10)
parseInt(tokenLabel.css('margin-left'), 10) - parseInt($tokenLabel.css('margin-left'), 10) -
parseInt(tokenLabel.css('margin-right'), 10) parseInt($tokenLabel.css('margin-right'), 10)
} }
tokenLabel $tokenLabel.css('max-width', this.maxTokenWidth)
.text(label) if (this.options.html)
.css('max-width', this.maxTokenWidth) $tokenLabel.html(attrs.label)
else
$tokenLabel.text(attrs.label)
// Listen to events // Listen to events on token
token $token
.on('mousedown', function (e) { .on('mousedown', function (e) {
if (_self.disabled) return false; if (_self._disabled || _self._readonly) return false
_self.preventDeactivation = true _self.preventDeactivation = true
}) })
.on('click', function (e) { .on('click', function (e) {
if (_self.disabled) return false; if (_self._disabled || _self._readonly) return false
_self.preventDeactivation = false _self.preventDeactivation = false
if (e.ctrlKey || e.metaKey) { if (e.ctrlKey || e.metaKey) {
e.preventDefault() e.preventDefault()
return _self.toggle( token ) return _self.toggle( $token )
} }
_self.activate( token, e.shiftKey, e.shiftKey ) _self.activate( $token, e.shiftKey, e.shiftKey )
}) })
.on('dblclick', function (e) { .on('dblclick', function (e) {
if (_self.disabled || !_self.options.allowEditing ) return false; if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
_self.edit( token ) _self.edit( $token )
}) })
closeButton $closeButton
.on('click', $.proxy(this.remove, this)) .on('click', $.proxy(this.remove, this))
var createEvent = $.Event('tokenfield:createtoken') // Trigger createdtoken event on the original field
createEvent.token = prepareEvent.token // indicating that the token is now in the DOM
createEvent.relatedTarget = token.get(0) this.$element.trigger($.Event('tokenfield:createdtoken', {
this.$element.trigger( createEvent ) attrs: attrs,
relatedTarget: $token.get(0)
}))
var changeEvent = $.Event('change') // Trigger change event on the original field
changeEvent.initiator = 'tokenfield'
if (triggerChange) { if (triggerChange) {
this.$element.val( this.getTokensList() ).trigger( changeEvent ) this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
} }
this.update()
return this.$input.get(0) // Update tokenfield dimensions
var _self = this
setTimeout(function () {
_self.update()
}, 0)
// Return original element
return this.$element.get(0)
} }
, setTokens: function (tokens, add, triggerChange) { , setTokens: function (tokens, add, triggerChange) {
if (!tokens) return
if (!add) this.$wrapper.find('.token').remove() if (!add) this.$wrapper.find('.token').remove()
if (!tokens) return
if (typeof triggerChange === 'undefined') { if (typeof triggerChange === 'undefined') {
triggerChange = true triggerChange = true
} }
@ -341,20 +340,17 @@
} }
var _self = this var _self = this
$.each(tokens, function (i, token) { $.each(tokens, function (i, attrs) {
_self.createToken(token, triggerChange) _self.createToken(attrs, triggerChange)
}) })
return this.$element.get(0) return this.$element.get(0)
} }
, getTokenData: function(token) { , getTokenData: function($token) {
var data = token.map(function() { var data = $token.map(function() {
var $token = $(this); var $token = $(this);
return { return $token.data('attrs')
value: $token.attr('data-value'),
label: $token.find('.token-label').text()
}
}).get(); }).get();
if (data.length == 1) { if (data.length == 1) {
@ -388,6 +384,16 @@
return this.$input.val() return this.$input.val()
} }
, setInput: function (val) {
if (this.$input.hasClass('tt-input')) {
// Typeahead acts weird when simply setting input value to empty,
// so we set the query to empty instead
this.$input.typeahead('val', val)
} else {
this.$input.val(val)
}
}
, listen: function () { , listen: function () {
var _self = this var _self = this
@ -436,7 +442,7 @@
} }
return false return false
}) })
.on('typeahead:selected', function (e, datum, dataset) { .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
// Create token // Create token
if (_self.createToken( datum )) { if (_self.createToken( datum )) {
_self.$input.typeahead('val', '') _self.$input.typeahead('val', '')
@ -445,13 +451,6 @@
} }
} }
}) })
.on('typeahead:autocompleted', function (e, datum, dataset) {
_self.createToken( _self.$input.val() )
_self.$input.typeahead('val', '')
if (_self.$input.data( 'edit' )) {
_self.unedit(true)
}
})
// Listen to window resize // Listen to window resize
$(window).on('resize', $.proxy(this.update, this )) $(window).on('resize', $.proxy(this.update, this ))
@ -496,11 +495,11 @@
case 13: // enter case 13: // enter
// We will handle creating tokens from autocomplete in autocomplete events // We will handle creating tokens from autocomplete in autocomplete events
if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus)").length) break 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
// We will handle creating tokens from typeahead in typeahead events // We will handle creating tokens from typeahead in typeahead events
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val().length) break if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
// Create token // Create token
if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
@ -520,13 +519,13 @@
if (_self.$input.val().length > 0) return if (_self.$input.val().length > 0) return
direction += 'All' direction += 'All'
var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
if (!token.length) return if (!$token.length) return
_self.preventInputFocus = true _self.preventInputFocus = true
_self.preventDeactivation = true _self.preventDeactivation = true
_self.activate( token ) _self.activate( $token )
e.preventDefault() e.preventDefault()
} else { } else {
@ -541,16 +540,16 @@
if (_self.$input.is(document.activeElement)) { if (_self.$input.is(document.activeElement)) {
if (_self.$input.val().length > 0) return if (_self.$input.val().length > 0) return
var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first') var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
if (!token.length) return if (!$token.length) return
_self.activate( token ) _self.activate( $token )
} }
var opposite = direction === 'prev' ? 'next' : 'prev' var opposite = direction === 'prev' ? 'next' : 'prev'
, position = direction === 'prev' ? 'first' : 'last' , position = direction === 'prev' ? 'first' : 'last'
_self.firstActiveToken[opposite + 'All']('.token').each(function() { _self.$firstActiveToken[opposite + 'All']('.token').each(function() {
_self.deactivate( $(this) ) _self.deactivate( $(this) )
}) })
@ -562,11 +561,9 @@
} }
, keypress: function(e) { , keypress: function(e) {
this.lastKeyPressCode = e.keyCode
this.lastKeyPressCharCode = e.charCode
// Comma // Comma
if ($.inArray( e.charCode, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
if (this.$input.val()) { if (this.$input.val()) {
this.createTokensFromInput(e) this.createTokensFromInput(e)
} }
@ -585,11 +582,11 @@
if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
this.preventDeactivation = true this.preventDeactivation = true
var prev = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
if (!prev.length) break if (!$prevToken.length) break
this.activate( prev ) this.activate( $prevToken )
} else { } else {
this.remove(e) this.remove(e)
} }
@ -608,7 +605,7 @@
if (this.$input.is(document.activeElement)) { if (this.$input.is(document.activeElement)) {
this.$wrapper.find('.active').removeClass('active') this.$wrapper.find('.active').removeClass('active')
this.firstActiveToken = null this.$firstActiveToken = null
if (this.options.showAutocompleteOnFocus) { if (this.options.showAutocompleteOnFocus) {
this.search() this.search()
@ -623,7 +620,7 @@
if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
this.$wrapper.find('.active').removeClass('active') this.$wrapper.find('.active').removeClass('active')
this.firstActiveToken = null this.$firstActiveToken = null
} }
if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
@ -638,10 +635,12 @@
var _self = this var _self = this
// Add tokens to existing ones // Add tokens to existing ones
if (_self.options.allowPasting) {
setTimeout(function () { setTimeout(function () {
_self.createTokensFromInput(e) _self.createTokensFromInput(e)
}, 1) }, 1)
} }
}
, change: function (e) { , change: function (e) {
if ( e.initiator === 'tokenfield' ) return // Prevent loops if ( e.initiator === 'tokenfield' ) return // Prevent loops
@ -659,13 +658,7 @@
if (tokensBefore == this.getTokensList() && this.$input.val().length) if (tokensBefore == this.getTokensList() && this.$input.val().length)
return false // No tokens were added, do nothing (prevent form submit) return false // No tokens were added, do nothing (prevent form submit)
if (this.$input.hasClass('tt-input')) { this.setInput('')
// Typeahead acts weird when simply setting input value to empty,
// so we set the query to empty instead
this.$input.typeahead('val', '')
} else {
this.$input.val('')
}
if (this.$input.data( 'edit' )) { if (this.$input.data( 'edit' )) {
this.unedit(focus) this.unedit(focus)
@ -676,50 +669,50 @@
, next: function (add) { , next: function (add) {
if (add) { if (add) {
var firstActive = this.$wrapper.find('.active:first') var $firstActiveToken = this.$wrapper.find('.active:first')
, deactivate = firstActive && this.firstActiveToken ? firstActive.index() < this.firstActiveToken.index() : false , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
if (deactivate) return this.deactivate( firstActive ) if (deactivate) return this.deactivate( $firstActiveToken )
} }
var active = this.$wrapper.find('.active:last') var $lastActiveToken = this.$wrapper.find('.active:last')
, next = active.nextAll('.token:first') , $nextToken = $lastActiveToken.nextAll('.token:first')
if (!next.length) { if (!$nextToken.length) {
this.$input.focus() this.$input.focus()
return return
} }
this.activate(next, add) this.activate($nextToken, add)
} }
, prev: function (add) { , prev: function (add) {
if (add) { if (add) {
var lastActive = this.$wrapper.find('.active:last') var $lastActiveToken = this.$wrapper.find('.active:last')
, deactivate = lastActive && this.firstActiveToken ? lastActive.index() > this.firstActiveToken.index() : false , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
if (deactivate) return this.deactivate( lastActive ) if (deactivate) return this.deactivate( $lastActiveToken )
} }
var active = this.$wrapper.find('.active:first') var $firstActiveToken = this.$wrapper.find('.active:first')
, prev = active.prevAll('.token:first') , $prevToken = $firstActiveToken.prevAll('.token:first')
if (!prev.length) { if (!$prevToken.length) {
prev = this.$wrapper.find('.token:first') $prevToken = this.$wrapper.find('.token:first')
} }
if (!prev.length && !add) { if (!$prevToken.length && !add) {
this.$input.focus() this.$input.focus()
return return
} }
this.activate( prev, add ) this.activate( $prevToken, add )
} }
, activate: function (token, add, multi, remember) { , activate: function ($token, add, multi, remember) {
if (!token) return if (!$token) return
if (typeof remember === 'undefined') var remember = true if (typeof remember === 'undefined') var remember = true
@ -730,17 +723,17 @@
if (!add) { if (!add) {
this.$wrapper.find('.active').removeClass('active') this.$wrapper.find('.active').removeClass('active')
if (remember) { if (remember) {
this.firstActiveToken = token this.$firstActiveToken = $token
} else { } else {
delete this.firstActiveToken delete this.$firstActiveToken
} }
} }
if (multi && this.firstActiveToken) { if (multi && this.$firstActiveToken) {
// Determine first active token and the current tokens indicies // Determine first active token and the current tokens indicies
// Account for the 1 hidden textarea by subtracting 1 from both // Account for the 1 hidden textarea by subtracting 1 from both
var i = this.firstActiveToken.index() - 2 var i = this.$firstActiveToken.index() - 2
, a = token.index() - 2 , a = $token.index() - 2
, _self = this , _self = this
this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
@ -748,7 +741,7 @@
}) })
} }
token.addClass('active') $token.addClass('active')
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
} }
@ -760,55 +753,51 @@
}) })
} }
, deactivate: function(token) { , deactivate: function($token) {
if (!token) return if (!$token) return
token.removeClass('active') $token.removeClass('active')
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
} }
, toggle: function(token) { , toggle: function($token) {
if (!token) return if (!$token) return
token.toggleClass('active') $token.toggleClass('active')
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
} }
, edit: function (token) { , edit: function ($token) {
if (!token) return if (!$token) return
var value = token.data('value') var attrs = $token.data('attrs')
, label = token.find('.token-label').text()
// Allow changing input value before editing // Allow changing input value before editing
var editEvent = $.Event('tokenfield:edittoken') var options = { attrs: attrs, relatedTarget: $token.get(0) }
editEvent.token = { var editEvent = $.Event('tokenfield:edittoken', options)
value: value,
label: label
}
editEvent.relatedTarget = token.get(0)
this.$element.trigger( editEvent ) this.$element.trigger( editEvent )
if (!editEvent.token) return // Edit event can be cancelled if default is prevented
if (editEvent.isDefaultPrevented()) return
value = editEvent.token.value $token.find('.token-label').text(attrs.value)
label = editEvent.token.label var tokenWidth = $token.outerWidth()
token.find('.token-label').text(value)
var tokenWidth = token.outerWidth()
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
token.replaceWith( $_input ) $token.replaceWith( $_input )
this.preventCreateTokens = true this.preventCreateTokens = true
this.$input.val( value ) this.$input.val( attrs.value )
.select() .select()
.data( 'edit', true ) .data( 'edit', true )
.width( tokenWidth ) .width( tokenWidth )
this.update(); this.update();
// Indicate that token is now being edited, and is replaced with an input field in the DOM
this.$element.trigger($.Event('tokenfield:editedtoken', options ))
} }
, unedit: function (focus) { , unedit: function (focus) {
@ -832,31 +821,35 @@
} }
, remove: function (e, direction) { , remove: function (e, direction) {
if (this.$input.is(document.activeElement) || this.disabled) return if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
var token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active') var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
if (e.type !== 'click') { if (e.type !== 'click') {
if (!direction) var direction = 'prev' if (!direction) var direction = 'prev'
this[direction]() this[direction]()
// Was this the first token? // Was it the first token?
if (direction === 'prev') var firstToken = token.first().prevAll('.token:first').length === 0 if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
} }
// Prepare events // Prepare events and their options
var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
, removeEvent = $.Event('tokenfield:removetoken', options)
var removeEvent = $.Event('tokenfield:removetoken') this.$element.trigger(removeEvent);
removeEvent.token = this.getTokenData( token )
var changeEvent = $.Event('change') // Remove event can be intercepted and cancelled
changeEvent.initiator = 'tokenfield' if (removeEvent.isDefaultPrevented()) return
var removedEvent = $.Event('tokenfield:removedtoken', options)
, changeEvent = $.Event('change', { initiator: 'tokenfield' })
// Remove token from DOM // Remove token from DOM
token.remove() $token.remove()
// Trigger events // Trigger events
this.$element.val( this.getTokensList() ).trigger( removeEvent ).trigger( changeEvent ) this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
// Focus, when necessary: // Focus, when necessary:
// When there are no more tokens, or if this was the first token // When there are no more tokens, or if this was the first token
@ -867,15 +860,19 @@
this.$input.css('width', this.options.minWidth + 'px') this.$input.css('width', this.options.minWidth + 'px')
this.update() this.update()
// Cancel original event handlers
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
} }
/**
* Update tokenfield dimensions
*/
, update: function (e) { , update: function (e) {
var value = this.$input.val() var value = this.$input.val()
, inputLeftPadding = parseInt(this.$input.css('padding-left'), 10) , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
, inputRightPadding = parseInt(this.$input.css('padding-right'), 10) , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
, inputPadding = inputLeftPadding + inputRightPadding , inputPadding = inputPaddingLeft + inputPaddingRight
if (this.$input.data('edit')) { if (this.$input.data('edit')) {
@ -894,16 +891,22 @@
this.$input.width( mirrorWidth ) this.$input.width( mirrorWidth )
} }
else { else {
this.$input.css( 'width', this.options.minWidth + 'px' ) //temporary reset width to minimal value to get proper results
if (this.textDirection === 'rtl') { this.$input.width(this.options.minWidth);
return this.$input.width( this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 )
} var w = (this.textDirection === 'rtl')
this.$input.width( this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding ) ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
: this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
//
// some usecases pre-render widget before attaching to DOM,
// dimensions returned by jquery will be NaN -> we default to 100%
// so placeholder won't be cut off.
isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
} }
} }
, focusInput: function (e) { , focusInput: function (e) {
if ($(e.target).closest('.token').length || $(e.target).closest('.token-input').length) return if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
// Focus only after the current call stack has cleared, // Focus only after the current call stack has cleared,
// otherwise has no effect. // otherwise has no effect.
// Reason: mousedown is too early - input will lose focus // Reason: mousedown is too early - input will lose focus
@ -922,17 +925,26 @@
} }
, disable: function () { , disable: function () {
this.disabled = true; this.setProperty('disabled', true);
this.$input.prop('disabled', true);
this.$element.prop('disabled', true);
this.$wrapper.addClass('disabled');
} }
, enable: function () { , enable: function () {
this.disabled = false; this.setProperty('disabled', false);
this.$input.prop('disabled', false); }
this.$element.prop('disabled', false);
this.$wrapper.removeClass('disabled'); , readonly: function () {
this.setProperty('readonly', true);
}
, writeable: function () {
this.setProperty('readonly', false);
}
, setProperty: function(property, value) {
this['_' + property] = value;
this.$input.prop(property, value);
this.$element.prop(property, value);
this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
} }
, destroy: function() { , destroy: function() {
@ -942,7 +954,7 @@
this.$element.css( this.$element.data('original-styles') ); this.$element.css( this.$element.data('original-styles') );
this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
// Re-route tokenfield labele to original input // Re-route tokenfield label to original input
var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
if ( $label.length ) { if ( $label.length ) {
$label.prop( 'for', this.$element.prop('id') ) $label.prop( 'for', this.$element.prop('id') )
@ -952,15 +964,15 @@
this.$element.insertBefore( this.$wrapper ); this.$element.insertBefore( this.$wrapper );
// Remove tokenfield-related data // Remove tokenfield-related data
this.$element.removeData('original-styles'); this.$element.removeData('original-styles')
this.$element.removeData('original-tabindex'); .removeData('original-tabindex')
this.$element.removeData('bs.tokenfield'); .removeData('bs.tokenfield');
// Remove tokenfield from DOM // Remove tokenfield from DOM
this.$wrapper.remove(); this.$wrapper.remove();
this.$mirror.remove();
var $_element = this.$element; var $_element = this.$element;
delete this;
return $_element; return $_element;
} }
@ -988,7 +1000,10 @@
args.shift() args.shift()
value = data[option].apply(data, args) value = data[option].apply(data, args)
} else { } else {
if (!data && typeof option !== 'string' && !param) $this.data('bs.tokenfield', (data = new Tokenfield(this, options))) if (!data && typeof option !== 'string' && !param) {
$this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
$this.trigger('tokenfield:initialize')
}
} }
}) })
@ -998,15 +1013,17 @@
$.fn.tokenfield.defaults = { $.fn.tokenfield.defaults = {
minWidth: 60, minWidth: 60,
minLength: 0, minLength: 0,
allowDuplicates: false, html: true,
allowEditing: true, allowEditing: true,
allowPasting: true,
limit: 0, limit: 0,
autocomplete: {}, autocomplete: {},
typeahead: {}, typeahead: {},
showAutocompleteOnFocus: false, showAutocompleteOnFocus: false,
createTokensOnBlur: false, createTokensOnBlur: false,
delimiter: ',', delimiter: ',',
beautify: true beautify: true,
inputType: 'text'
} }
$.fn.tokenfield.Constructor = Tokenfield $.fn.tokenfield.Constructor = Tokenfield