rbgkew-bootstrap-tokenfield
Version:
Advanced tagging/tokenizing plugin for input fields with a focus on keyboard and copy-paste support.
1,006 lines (780 loc) • 37.5 kB
JavaScript
/*!
* bootstrap-tokenfield
* https://github.com/Open-Xchange-Frontend/bootstrap-tokenfield
* Copyright 2013-2016 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';
/* 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 elStyleWidth = element.style.width,
elWidth = this.$element.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 (this.$element.parents('.form-inline').length) {
// If input is inside inline-form with no width set, set fixed width
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:-9999px;left:-9999px;white-space:pre;"/>');
this.$input.css('min-width', this.options.minWidth + 'px');
// Insert tokenfield to HTML
this.$wrapper.insertBefore(this.$element);
this.$element.prependTo(this.$wrapper);
// Append mirror to tokenfield wrapper
this.$mirror.appendTo(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" aria-label="Remove">×</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');
if (this.options.html) {
$tokenLabel.html(attrs.label);
} else {
$tokenLabel.text(attrs.label);
}
// 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 change event on the original field
if (triggerChange) {
this.$element.val(this.getTokensList()).trigger($.Event('change', { initiator: 'tokenfield' }));
}
// Update tokenfield dimensions
setTimeout(function () {
if (!_self.maxTokenWidth) {
_self.maxTokenWidth = _self.$wrapper.width() - $closeButton.width() - 10;
}
$tokenLabel.css('max-width', _self.maxTokenWidth);
_self.update();
// Trigger createdtoken event on the original field
// indicating that the token is now in the DOM
_self.$element.trigger($.Event('tokenfield:createdtoken', {
attrs: attrs,
relatedTarget: $token.get(0)
}));
}, 0);
// Return original element
return this.$element.get(0);
},
setTokens: function (tokens, add, triggerChange) {
if (!add) this.$wrapper.find('.token').remove();
if (!tokens) return;
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();
},
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 () {
var _self = this;
this.$element
.on('change.tokenfield', $.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,
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'));
} else if (this.$input.is(document.activeElement) && (e.keyCode === 13)) {
e.preventDefault();
this.$element.trigger('tokenfield:next');
}
// 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'));
}
// no default
}
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)) {
var val = this.$input.val(),
quoting = /^"[^"]*$/.test(val);
if (quoting) return;
if (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;
// no default
}
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)
this.setInput('');
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') remember = true;
if (multi) 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(),
$_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 firstToken,
$token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active');
if (e.type !== 'click') {
if (!direction) direction = 'prev';
this[direction]();
// Was it the first token?
if (direction === 'prev') 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 {
// temporary reset width to minimal value to get proper results
this.$input.width(this.options.minWidth);
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.
if (isNaN(w)) {
this.$input.width('100%');
} else {
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 events
this.$element.off('.tokenfield');
// 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,
html: true,
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;
}));