alpaca
Version:
Alpaca provides the easiest and fastest way to generate interactive forms for the web and mobile devices. It runs simply as HTML5 or more elaborately using Bootstrap, jQuery Mobile or jQuery UI. Alpaca uses Handlebars to process JSON schema and provide
1,030 lines (803 loc) • 33.1 kB
JavaScript
/*!
* 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;
}));