/bower_components/bootstrap-tokenfield/dist/bootstrap-tokenfield.js |
@@ -0,0 +1,1029 @@ |
/*! |
* bootstrap-tokenfield |
* https://github.com/sliptree/bootstrap-tokenfield |
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT |
*/ |
|
(function (factory) { |
if (typeof define === 'function' && define.amd) { |
// AMD. Register as an anonymous module. |
define(['jquery'], factory); |
} else if (typeof exports === 'object') { |
// For CommonJS and CommonJS-like environments where a window with jQuery |
// is present, execute the factory with the jQuery instance from the window object |
// For environments that do not inherently posses a window with a document |
// (such as Node.js), expose a Tokenfield-making factory as module.exports |
// This accentuates the need for the creation of a real window or passing in a jQuery instance |
// e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($); |
module.exports = global.window && global.window.$ ? |
factory( global.window.$ ) : |
function( input ) { |
if ( !input.$ && !input.fn ) { |
throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" ); |
} |
return factory( input.$ || input ); |
}; |
} else { |
// Browser globals |
factory(jQuery, window); |
} |
}(function ($, window) { |
|
"use strict"; // jshint ;_; |
|
/* TOKENFIELD PUBLIC CLASS DEFINITION |
* ============================== */ |
|
var Tokenfield = function (element, options) { |
var _self = this |
|
this.$element = $(element) |
this.textDirection = this.$element.css('direction'); |
|
// Extend options |
this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options) |
|
// Setup delimiters and trigger keys |
this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter |
this._triggerKeys = $.map(this._delimiters, function (delimiter) { |
return delimiter.charCodeAt(0); |
}); |
this._firstDelimiter = this._delimiters[0]; |
|
// Check for whitespace, dash and special characters |
var whitespace = $.inArray(' ', this._delimiters) |
, dash = $.inArray('-', this._delimiters) |
|
if (whitespace >= 0) |
this._delimiters[whitespace] = '\\s' |
|
if (dash >= 0) { |
delete this._delimiters[dash] |
this._delimiters.unshift('-') |
} |
|
var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')'] |
$.each(this._delimiters, function (index, character) { |
var pos = $.inArray(character, specialCharacters) |
if (pos >= 0) _self._delimiters[index] = '\\' + character; |
}); |
|
// Store original input width |
var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null |
, elStyleWidth = element.style.width |
, elCSSWidth |
, elWidth = this.$element.width() |
|
if (elRules) { |
$.each( elRules, function (i, rule) { |
if (rule.style.width) { |
elCSSWidth = rule.style.width; |
} |
}); |
} |
|
// Move original input out of the way |
var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left', |
originalStyles = { position: this.$element.css('position') }; |
originalStyles[hidingPosition] = this.$element.css(hidingPosition); |
|
this.$element |
.data('original-styles', originalStyles) |
.data('original-tabindex', this.$element.prop('tabindex')) |
.css('position', 'absolute') |
.css(hidingPosition, '-10000px') |
.prop('tabindex', -1) |
|
// Create a wrapper |
this.$wrapper = $('<div class="tokenfield form-control" />') |
if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg') |
if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm') |
if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl') |
|
// Create a new input |
var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100) |
this.$input = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />') |
.appendTo( this.$wrapper ) |
.prop( 'placeholder', this.$element.prop('placeholder') ) |
.prop( 'id', id + '-tokenfield' ) |
.prop( 'tabindex', this.$element.data('original-tabindex') ) |
|
// Re-route original input label to new input |
var $label = $( 'label[for="' + this.$element.prop('id') + '"]' ) |
if ( $label.length ) { |
$label.prop( 'for', this.$input.prop('id') ) |
} |
|
// Set up a copy helper to handle copy & paste |
this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper ) |
|
// Set wrapper width |
if (elStyleWidth) { |
this.$wrapper.css('width', elStyleWidth); |
} |
else if (elCSSWidth) { |
this.$wrapper.css('width', elCSSWidth); |
} |
// If input is inside inline-form with no width set, set fixed width |
else if (this.$element.parents('.form-inline').length) { |
this.$wrapper.width( elWidth ) |
} |
|
// Set tokenfield disabled, if original or fieldset input is disabled |
if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) { |
this.disable(); |
} |
|
// Set tokenfield readonly, if original input is readonly |
if (this.$element.prop('readonly')) { |
this.readonly(); |
} |
|
// Set up mirror for input auto-sizing |
this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>'); |
this.$input.css('min-width', this.options.minWidth + 'px') |
$.each([ |
'fontFamily', |
'fontSize', |
'fontWeight', |
'fontStyle', |
'letterSpacing', |
'textTransform', |
'wordSpacing', |
'textIndent' |
], function (i, val) { |
_self.$mirror[0].style[val] = _self.$input.css(val); |
}); |
this.$mirror.appendTo( 'body' ) |
|
// Insert tokenfield to HTML |
this.$wrapper.insertBefore( this.$element ) |
this.$element.prependTo( this.$wrapper ) |
|
// Calculate inner input width |
this.update() |
|
// Create initial tokens, if any |
this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens ) |
|
// Start listening to events |
this.listen() |
|
// Initialize autocomplete, if necessary |
if ( ! $.isEmptyObject( this.options.autocomplete ) ) { |
var side = this.textDirection === 'rtl' ? 'right' : 'left' |
, autocompleteOptions = $.extend({ |
minLength: this.options.showAutocompleteOnFocus ? 0 : null, |
position: { my: side + " top", at: side + " bottom", of: this.$wrapper } |
}, this.options.autocomplete ) |
|
this.$input.autocomplete( autocompleteOptions ) |
} |
|
// Initialize typeahead, if necessary |
if ( ! $.isEmptyObject( this.options.typeahead ) ) { |
|
var typeaheadOptions = this.options.typeahead |
, defaults = { |
minLength: this.options.showAutocompleteOnFocus ? 0 : null |
} |
, args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions] |
|
args[0] = $.extend( {}, defaults, args[0] ) |
|
this.$input.typeahead.apply( this.$input, args ) |
this.typeahead = true |
} |
} |
|
Tokenfield.prototype = { |
|
constructor: Tokenfield |
|
, createToken: function (attrs, triggerChange) { |
var _self = this |
|
if (typeof attrs === 'string') { |
attrs = { value: attrs, label: attrs } |
} else { |
// Copy objects to prevent contamination of data sources. |
attrs = $.extend( {}, attrs ) |
} |
|
if (typeof triggerChange === 'undefined') { |
triggerChange = true |
} |
|
// Normalize label and value |
attrs.value = $.trim(attrs.value.toString()); |
attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value |
|
// 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 |
|
// Allow changing token data before creating it |
var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs }) |
this.$element.trigger(createEvent) |
|
// Bail out if there if attributes are empty or event was defaultPrevented |
if (!createEvent.attrs || createEvent.isDefaultPrevented()) return |
|
var $token = $('<div class="token" />') |
.append('<span class="token-label" />') |
.append('<a href="#" class="close" tabindex="-1">×</a>') |
.data('attrs', attrs) |
|
// Insert token into HTML |
if (this.$input.hasClass('tt-input')) { |
// If the input has typeahead enabled, insert token before it's parent |
this.$input.parent().before( $token ) |
} else { |
this.$input.before( $token ) |
} |
|
// Temporarily set input width to minimum |
this.$input.css('width', this.options.minWidth + 'px') |
|
var $tokenLabel = $token.find('.token-label') |
, $closeButton = $token.find('.close') |
|
// Determine maximum possible token label width |
if (!this.maxTokenWidth) { |
this.maxTokenWidth = |
this.$wrapper.width() - $closeButton.outerWidth() - |
parseInt($closeButton.css('margin-left'), 10) - |
parseInt($closeButton.css('margin-right'), 10) - |
parseInt($token.css('border-left-width'), 10) - |
parseInt($token.css('border-right-width'), 10) - |
parseInt($token.css('padding-left'), 10) - |
parseInt($token.css('padding-right'), 10) |
parseInt($tokenLabel.css('border-left-width'), 10) - |
parseInt($tokenLabel.css('border-right-width'), 10) - |
parseInt($tokenLabel.css('padding-left'), 10) - |
parseInt($tokenLabel.css('padding-right'), 10) |
parseInt($tokenLabel.css('margin-left'), 10) - |
parseInt($tokenLabel.css('margin-right'), 10) |
} |
|
$tokenLabel |
.text(attrs.label) |
.css('max-width', this.maxTokenWidth) |
|
// Listen to events on token |
$token |
.on('mousedown', function (e) { |
if (_self._disabled || _self._readonly) return false |
_self.preventDeactivation = true |
}) |
.on('click', function (e) { |
if (_self._disabled || _self._readonly) return false |
_self.preventDeactivation = false |
|
if (e.ctrlKey || e.metaKey) { |
e.preventDefault() |
return _self.toggle( $token ) |
} |
|
_self.activate( $token, e.shiftKey, e.shiftKey ) |
}) |
.on('dblclick', function (e) { |
if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false |
_self.edit( $token ) |
}) |
|
$closeButton |
.on('click', $.proxy(this.remove, this)) |
|
// Trigger createdtoken event on the original field |
// indicating that the token is now in the DOM |
this.$element.trigger($.Event('tokenfield:createdtoken', { |
attrs: attrs, |
relatedTarget: $token.get(0) |
})) |
|
// Trigger change event on the original field |
if (triggerChange) { |
this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) ) |
} |
|
// Update tokenfield dimensions |
this.update() |
|
// Return original element |
return this.$element.get(0) |
} |
|
, setTokens: function (tokens, add, triggerChange) { |
if (!tokens) return |
|
if (!add) this.$wrapper.find('.token').remove() |
|
if (typeof triggerChange === 'undefined') { |
triggerChange = true |
} |
|
if (typeof tokens === 'string') { |
if (this._delimiters.length) { |
// Split based on delimiters |
tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) ) |
} else { |
tokens = [tokens]; |
} |
} |
|
var _self = this |
$.each(tokens, function (i, attrs) { |
_self.createToken(attrs, triggerChange) |
}) |
|
return this.$element.get(0) |
} |
|
, getTokenData: function($token) { |
var data = $token.map(function() { |
var $token = $(this); |
return $token.data('attrs') |
}).get(); |
|
if (data.length == 1) { |
data = data[0]; |
} |
|
return data; |
} |
|
, getTokens: function(active) { |
var self = this |
, tokens = [] |
, activeClass = active ? '.active' : '' // get active tokens only |
this.$wrapper.find( '.token' + activeClass ).each( function() { |
tokens.push( self.getTokenData( $(this) ) ) |
}) |
return tokens |
} |
|
, getTokensList: function(delimiter, beautify, active) { |
delimiter = delimiter || this._firstDelimiter |
beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify |
|
var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '') |
return $.map( this.getTokens(active), function (token) { |
return token.value |
}).join(separator) |
} |
|
, getInput: function() { |
return this.$input.val() |
} |
|
, listen: function () { |
var _self = this |
|
this.$element |
.on('change', $.proxy(this.change, this)) |
|
this.$wrapper |
.on('mousedown',$.proxy(this.focusInput, this)) |
|
this.$input |
.on('focus', $.proxy(this.focus, this)) |
.on('blur', $.proxy(this.blur, this)) |
.on('paste', $.proxy(this.paste, this)) |
.on('keydown', $.proxy(this.keydown, this)) |
.on('keypress', $.proxy(this.keypress, this)) |
.on('keyup', $.proxy(this.keyup, this)) |
|
this.$copyHelper |
.on('focus', $.proxy(this.focus, this)) |
.on('blur', $.proxy(this.blur, this)) |
.on('keydown', $.proxy(this.keydown, this)) |
.on('keyup', $.proxy(this.keyup, this)) |
|
// Secondary listeners for input width calculation |
this.$input |
.on('keypress', $.proxy(this.update, this)) |
.on('keyup', $.proxy(this.update, this)) |
|
this.$input |
.on('autocompletecreate', function() { |
// Set minimum autocomplete menu width |
var $_menuElement = $(this).data('ui-autocomplete').menu.element |
|
var minWidth = _self.$wrapper.outerWidth() - |
parseInt( $_menuElement.css('border-left-width'), 10 ) - |
parseInt( $_menuElement.css('border-right-width'), 10 ) |
|
$_menuElement.css( 'min-width', minWidth + 'px' ) |
}) |
.on('autocompleteselect', function (e, ui) { |
if (_self.createToken( ui.item )) { |
_self.$input.val('') |
if (_self.$input.data( 'edit' )) { |
_self.unedit(true) |
} |
} |
return false |
}) |
.on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) { |
// Create token |
if (_self.createToken( datum )) { |
_self.$input.typeahead('val', '') |
if (_self.$input.data( 'edit' )) { |
_self.unedit(true) |
} |
} |
}) |
|
// Listen to window resize |
$(window).on('resize', $.proxy(this.update, this )) |
|
} |
|
, keydown: function (e) { |
|
if (!this.focused) return |
|
var _self = this |
|
switch(e.keyCode) { |
case 8: // backspace |
if (!this.$input.is(document.activeElement)) break |
this.lastInputValue = this.$input.val() |
break |
|
case 37: // left arrow |
leftRight( this.textDirection === 'rtl' ? 'next': 'prev' ) |
break |
|
case 38: // up arrow |
upDown('prev') |
break |
|
case 39: // right arrow |
leftRight( this.textDirection === 'rtl' ? 'prev': 'next' ) |
break |
|
case 40: // down arrow |
upDown('next') |
break |
|
case 65: // a (to handle ctrl + a) |
if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break |
this.activateAll() |
e.preventDefault() |
break |
|
case 9: // tab |
case 13: // enter |
|
// 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), li.ui-state-focus").length) break |
|
// 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-hint').val() && this.$wrapper.find('.tt-hint').val().length) break |
|
// Create token |
if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) { |
return this.createTokensFromInput(e, this.$input.data('edit')); |
} |
|
// Edit token |
if (e.keyCode === 13) { |
if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break |
if (!_self.options.allowEditing) break |
this.edit( this.$wrapper.find('.token.active') ) |
} |
} |
|
function leftRight(direction) { |
if (_self.$input.is(document.activeElement)) { |
if (_self.$input.val().length > 0) return |
|
direction += 'All' |
var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first') |
if (!$token.length) return |
|
_self.preventInputFocus = true |
_self.preventDeactivation = true |
|
_self.activate( $token ) |
e.preventDefault() |
|
} else { |
_self[direction]( e.shiftKey ) |
e.preventDefault() |
} |
} |
|
function upDown(direction) { |
if (!e.shiftKey) return |
|
if (_self.$input.is(document.activeElement)) { |
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') |
if (!$token.length) return |
|
_self.activate( $token ) |
} |
|
var opposite = direction === 'prev' ? 'next' : 'prev' |
, position = direction === 'prev' ? 'first' : 'last' |
|
_self.$firstActiveToken[opposite + 'All']('.token').each(function() { |
_self.deactivate( $(this) ) |
}) |
|
_self.activate( _self.$wrapper.find('.token:' + position), true, true ) |
e.preventDefault() |
} |
|
this.lastKeyDown = e.keyCode |
} |
|
, keypress: function(e) { |
|
// Comma |
if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) { |
if (this.$input.val()) { |
this.createTokensFromInput(e) |
} |
return false; |
} |
} |
|
, keyup: function (e) { |
this.preventInputFocus = false |
|
if (!this.focused) return |
|
switch(e.keyCode) { |
case 8: // backspace |
if (this.$input.is(document.activeElement)) { |
if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break |
|
this.preventDeactivation = true |
var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first') |
|
if (!$prevToken.length) break |
|
this.activate( $prevToken ) |
} else { |
this.remove(e) |
} |
break |
|
case 46: // delete |
this.remove(e, 'next') |
break |
} |
this.lastKeyUp = e.keyCode |
} |
|
, focus: function (e) { |
this.focused = true |
this.$wrapper.addClass('focus') |
|
if (this.$input.is(document.activeElement)) { |
this.$wrapper.find('.active').removeClass('active') |
this.$firstActiveToken = null |
|
if (this.options.showAutocompleteOnFocus) { |
this.search() |
} |
} |
} |
|
, blur: function (e) { |
|
this.focused = false |
this.$wrapper.removeClass('focus') |
|
if (!this.preventDeactivation && !this.$element.is(document.activeElement)) { |
this.$wrapper.find('.active').removeClass('active') |
this.$firstActiveToken = null |
} |
|
if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) { |
this.createTokensFromInput(e) |
} |
|
this.preventDeactivation = false |
this.preventCreateTokens = false |
} |
|
, paste: function (e) { |
var _self = this |
|
// Add tokens to existing ones |
if (_self.options.allowPasting) { |
setTimeout(function () { |
_self.createTokensFromInput(e) |
}, 1) |
} |
} |
|
, change: function (e) { |
if ( e.initiator === 'tokenfield' ) return // Prevent loops |
|
this.setTokens( this.$element.val() ) |
} |
|
, createTokensFromInput: function (e, focus) { |
if (this.$input.val().length < this.options.minLength) |
return // No input, simply return |
|
var tokensBefore = this.getTokensList() |
this.setTokens( this.$input.val(), true ) |
|
if (tokensBefore == this.getTokensList() && this.$input.val().length) |
return false // No tokens were added, do nothing (prevent form submit) |
|
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', '') |
} else { |
this.$input.val('') |
} |
|
if (this.$input.data( 'edit' )) { |
this.unedit(focus) |
} |
|
return false // Prevent form being submitted |
} |
|
, next: function (add) { |
if (add) { |
var $firstActiveToken = this.$wrapper.find('.active:first') |
, deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false |
|
if (deactivate) return this.deactivate( $firstActiveToken ) |
} |
|
var $lastActiveToken = this.$wrapper.find('.active:last') |
, $nextToken = $lastActiveToken.nextAll('.token:first') |
|
if (!$nextToken.length) { |
this.$input.focus() |
return |
} |
|
this.activate($nextToken, add) |
} |
|
, prev: function (add) { |
|
if (add) { |
var $lastActiveToken = this.$wrapper.find('.active:last') |
, deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false |
|
if (deactivate) return this.deactivate( $lastActiveToken ) |
} |
|
var $firstActiveToken = this.$wrapper.find('.active:first') |
, $prevToken = $firstActiveToken.prevAll('.token:first') |
|
if (!$prevToken.length) { |
$prevToken = this.$wrapper.find('.token:first') |
} |
|
if (!$prevToken.length && !add) { |
this.$input.focus() |
return |
} |
|
this.activate( $prevToken, add ) |
} |
|
, activate: function ($token, add, multi, remember) { |
|
if (!$token) return |
|
if (typeof remember === 'undefined') var remember = true |
|
if (multi) var add = true |
|
this.$copyHelper.focus() |
|
if (!add) { |
this.$wrapper.find('.active').removeClass('active') |
if (remember) { |
this.$firstActiveToken = $token |
} else { |
delete this.$firstActiveToken |
} |
} |
|
if (multi && this.$firstActiveToken) { |
// Determine first active token and the current tokens indicies |
// Account for the 1 hidden textarea by subtracting 1 from both |
var i = this.$firstActiveToken.index() - 2 |
, a = $token.index() - 2 |
, _self = this |
|
this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() { |
_self.activate( $(this), true ) |
}) |
} |
|
$token.addClass('active') |
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() |
} |
|
, activateAll: function() { |
var _self = this |
|
this.$wrapper.find('.token').each( function (i) { |
_self.activate($(this), i !== 0, false, false) |
}) |
} |
|
, deactivate: function($token) { |
if (!$token) return |
|
$token.removeClass('active') |
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() |
} |
|
, toggle: function($token) { |
if (!$token) return |
|
$token.toggleClass('active') |
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select() |
} |
|
, edit: function ($token) { |
if (!$token) return |
|
var attrs = $token.data('attrs') |
|
// Allow changing input value before editing |
var options = { attrs: attrs, relatedTarget: $token.get(0) } |
var editEvent = $.Event('tokenfield:edittoken', options) |
this.$element.trigger( editEvent ) |
|
// Edit event can be cancelled if default is prevented |
if (editEvent.isDefaultPrevented()) return |
|
$token.find('.token-label').text(attrs.value) |
var tokenWidth = $token.outerWidth() |
|
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input |
|
$token.replaceWith( $_input ) |
|
this.preventCreateTokens = true |
|
this.$input.val( attrs.value ) |
.select() |
.data( 'edit', true ) |
.width( tokenWidth ) |
|
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) { |
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input |
$_input.appendTo( this.$wrapper ) |
|
this.$input.data('edit', false) |
this.$mirror.text('') |
|
this.update() |
|
// Because moving the input element around in DOM |
// will cause it to lose focus, we provide an option |
// to re-focus the input after appending it to the wrapper |
if (focus) { |
var _self = this |
setTimeout(function () { |
_self.$input.focus() |
}, 1) |
} |
} |
|
, remove: function (e, direction) { |
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') |
|
if (e.type !== 'click') { |
if (!direction) var direction = 'prev' |
this[direction]() |
|
// Was it the first token? |
if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0 |
} |
|
// Prepare events and their options |
var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) } |
, removeEvent = $.Event('tokenfield:removetoken', options) |
|
this.$element.trigger(removeEvent); |
|
// Remove event can be intercepted and cancelled |
if (removeEvent.isDefaultPrevented()) return |
|
var removedEvent = $.Event('tokenfield:removedtoken', options) |
, changeEvent = $.Event('change', { initiator: 'tokenfield' }) |
|
// Remove token from DOM |
$token.remove() |
|
// Trigger events |
this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent ) |
|
// Focus, when necessary: |
// When there are no more tokens, or if this was the first token |
// and it was removed with backspace or it was clicked on |
if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus() |
|
// Adjust input width |
this.$input.css('width', this.options.minWidth + 'px') |
this.update() |
|
// Cancel original event handlers |
e.preventDefault() |
e.stopPropagation() |
} |
|
/** |
* Update tokenfield dimensions |
*/ |
, update: function (e) { |
var value = this.$input.val() |
, inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10) |
, inputPaddingRight = parseInt(this.$input.css('padding-right'), 10) |
, inputPadding = inputPaddingLeft + inputPaddingRight |
|
if (this.$input.data('edit')) { |
|
if (!value) { |
value = this.$input.prop("placeholder") |
} |
if (value === this.$mirror.text()) return |
|
this.$mirror.text(value) |
|
var mirrorWidth = this.$mirror.width() + 10; |
if ( mirrorWidth > this.$wrapper.width() ) { |
return this.$input.width( this.$wrapper.width() ) |
} |
|
this.$input.width( mirrorWidth ) |
} |
else { |
var w = (this.textDirection === 'rtl') |
? 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) { |
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, |
// otherwise has no effect. |
// Reason: mousedown is too early - input will lose focus |
// after mousedown. However, since the input may be moved |
// in DOM, there may be no click or mouseup event triggered. |
var _self = this |
setTimeout(function() { |
_self.$input.focus() |
}, 0) |
} |
|
, search: function () { |
if ( this.$input.data('ui-autocomplete') ) { |
this.$input.autocomplete('search') |
} |
} |
|
, disable: function () { |
this.setProperty('disabled', true); |
} |
|
, enable: function () { |
this.setProperty('disabled', false); |
} |
|
, 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() { |
// Set field value |
this.$element.val( this.getTokensList() ); |
// Restore styles and properties |
this.$element.css( this.$element.data('original-styles') ); |
this.$element.prop( 'tabindex', this.$element.data('original-tabindex') ); |
|
// Re-route tokenfield label to original input |
var $label = $( 'label[for="' + this.$input.prop('id') + '"]' ) |
if ( $label.length ) { |
$label.prop( 'for', this.$element.prop('id') ) |
} |
|
// Move original element outside of tokenfield wrapper |
this.$element.insertBefore( this.$wrapper ); |
|
// Remove tokenfield-related data |
this.$element.removeData('original-styles') |
.removeData('original-tabindex') |
.removeData('bs.tokenfield'); |
|
// Remove tokenfield from DOM |
this.$wrapper.remove(); |
this.$mirror.remove(); |
|
var $_element = this.$element; |
|
return $_element; |
} |
|
} |
|
|
/* TOKENFIELD PLUGIN DEFINITION |
* ======================== */ |
|
var old = $.fn.tokenfield |
|
$.fn.tokenfield = function (option, param) { |
var value |
, args = [] |
|
Array.prototype.push.apply( args, arguments ); |
|
var elements = this.each(function () { |
var $this = $(this) |
, data = $this.data('bs.tokenfield') |
, options = typeof option == 'object' && option |
|
if (typeof option === 'string' && data && data[option]) { |
args.shift() |
value = data[option].apply(data, args) |
} else { |
if (!data && typeof option !== 'string' && !param) { |
$this.data('bs.tokenfield', (data = new Tokenfield(this, options))) |
$this.trigger('tokenfield:initialize') |
} |
} |
}) |
|
return typeof value !== 'undefined' ? value : elements; |
} |
|
$.fn.tokenfield.defaults = { |
minWidth: 60, |
minLength: 0, |
allowEditing: true, |
allowPasting: true, |
limit: 0, |
autocomplete: {}, |
typeahead: {}, |
showAutocompleteOnFocus: false, |
createTokensOnBlur: false, |
delimiter: ',', |
beautify: true, |
inputType: 'text' |
} |
|
$.fn.tokenfield.Constructor = Tokenfield |
|
|
/* TOKENFIELD NO CONFLICT |
* ================== */ |
|
$.fn.tokenfield.noConflict = function () { |
$.fn.tokenfield = old |
return this |
} |
|
return Tokenfield; |
|
})); |