tagify 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/tagify.coffee +77 -0
- data/lib/tagify.rb +7 -0
- data/lib/tagify/version.rb +3 -0
- data/tagify.gemspec +28 -0
- data/vendor/assets/javascripts/bootstrap-tokenfield.js +1029 -0
- metadata +138 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 977ff70e79b306bcbf7172a0c9d4a01b91104afc
|
4
|
+
data.tar.gz: cc307aa99be30ad55716f8f24ab3a15d33a6998a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5052f0434337c5238e746790f49e320893a8ce9eb9fad5c0a36c904ea58e59f7afaf23452d5b459ce8737c49d8e31b4483795e30a14e785ffda25197991b7cde
|
7
|
+
data.tar.gz: 9507b679004b976f01f4c4ec23df3ac287f321709f257ebc44e0d9537ee6be679abe55f966723628edf704fd1856df822aa017fd9d4447173fe2fccbdeabfd8a
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Azzurrio
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Tagify
|
2
|
+
|
3
|
+
Switch multiple selection fields into tags with autocomplete.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'tagify'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install tagify
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it ( https://github.com/[my-github-username]/tagify/fork )
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#= require jquery-ui/autocomplete
|
2
|
+
#= require bootstrap-tokenfield
|
3
|
+
|
4
|
+
$ ->
|
5
|
+
customizeAutocomplete = ->
|
6
|
+
oldFn = $.ui.autocomplete::_renderItem
|
7
|
+
$.ui.autocomplete::_renderItem = (ul, item) ->
|
8
|
+
term = @term.toLowerCase()
|
9
|
+
new_label = item.label.toLowerCase().replace(term, "<span style='font-weight:bold;color:black;'>" + term + "</span>")
|
10
|
+
$("<li></li>").data("item.autocomplete", item).append("<a>" + new_label + "</a>").appendTo ul
|
11
|
+
|
12
|
+
getCurrentValues = (target) ->
|
13
|
+
inputField = getInputField(target)
|
14
|
+
if !!inputField.val() then JSON.parse(inputField.val()) else []
|
15
|
+
|
16
|
+
getInputField = (target) ->
|
17
|
+
getWrapperField(target).next()
|
18
|
+
|
19
|
+
getWrapperField = (target) ->
|
20
|
+
target.closest('.tokenfield')
|
21
|
+
|
22
|
+
enterNewValue = (selectedValue, target) ->
|
23
|
+
inputField = getInputField(target)
|
24
|
+
currentValues = getCurrentValues(target)
|
25
|
+
currentValues.push(selectedValue)
|
26
|
+
inputField.val(JSON.stringify(currentValues))
|
27
|
+
|
28
|
+
removeExistingValue = (selectedValue, target) ->
|
29
|
+
inputField = getInputField(target)
|
30
|
+
currentValues = getCurrentValues(target)
|
31
|
+
currentValues = $.grep currentValues, (value) ->
|
32
|
+
value != parseInt(selectedValue)
|
33
|
+
inputField.val(JSON.stringify(currentValues))
|
34
|
+
|
35
|
+
searchJSON = (data, regex, target) ->
|
36
|
+
result = []
|
37
|
+
currentValues = getCurrentValues(target)
|
38
|
+
$.each data, (i, row) ->
|
39
|
+
|
40
|
+
if row.label.match(regex) && currentValues.indexOf(row.value) == -1
|
41
|
+
result.push row
|
42
|
+
return
|
43
|
+
result.slice(0,10)
|
44
|
+
|
45
|
+
retrieveExistingTokens = (data, target) ->
|
46
|
+
tokens = []
|
47
|
+
currentValues = getCurrentValues(target)
|
48
|
+
$.each data, (i, row) ->
|
49
|
+
if currentValues.indexOf(row.value) != -1
|
50
|
+
tokens.push row
|
51
|
+
return
|
52
|
+
tokens
|
53
|
+
|
54
|
+
$('.tagify').each ->
|
55
|
+
|
56
|
+
$this = $(this)
|
57
|
+
filename = $this.data('file')
|
58
|
+
|
59
|
+
$.getJSON '/' + filename + '.json', (data) ->
|
60
|
+
|
61
|
+
customizeAutocomplete()
|
62
|
+
|
63
|
+
$this.tokenfield
|
64
|
+
autocomplete:
|
65
|
+
source: (request, response) ->
|
66
|
+
result = searchJSON(data,request.term.toLowerCase(), $this)
|
67
|
+
response result
|
68
|
+
select: (e, ui) ->
|
69
|
+
selectedValue = ui.item.value
|
70
|
+
enterNewValue(selectedValue, $this)
|
71
|
+
delimiter: ''
|
72
|
+
.on 'tokenfield:removedtoken', (e) ->
|
73
|
+
selectedValue = e.attrs.value
|
74
|
+
removeExistingValue(selectedValue, $this)
|
75
|
+
|
76
|
+
tokens = retrieveExistingTokens(data, $this)
|
77
|
+
$this.tokenfield('setTokens', tokens);
|
data/lib/tagify.rb
ADDED
data/tagify.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tagify/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tagify"
|
8
|
+
spec.version = Tagify::VERSION
|
9
|
+
spec.authors = ["Azzurrio"]
|
10
|
+
spec.email = ["just.azzurri@gmail.com"]
|
11
|
+
spec.summary = %q{Switch multiple selection fields into tags with autocomplete.}
|
12
|
+
spec.description = %q{Switch multiple selection fields into tags with autocomplete.}
|
13
|
+
spec.homepage = "https://github.com/Azzurrio/tagify"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'jquery-rails', '~> 3.1'
|
22
|
+
spec.add_runtime_dependency 'jquery-ui-rails', '~> 5.0'
|
23
|
+
spec.add_runtime_dependency 'coffee-rails', '~> 4.0'
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
28
|
+
end
|
@@ -0,0 +1,1029 @@
|
|
1
|
+
/*!
|
2
|
+
* bootstrap-tokenfield
|
3
|
+
* https://github.com/sliptree/bootstrap-tokenfield
|
4
|
+
* Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
|
5
|
+
*/
|
6
|
+
|
7
|
+
(function (factory) {
|
8
|
+
if (typeof define === 'function' && define.amd) {
|
9
|
+
// AMD. Register as an anonymous module.
|
10
|
+
define(['jquery'], factory);
|
11
|
+
} else if (typeof exports === 'object') {
|
12
|
+
// For CommonJS and CommonJS-like environments where a window with jQuery
|
13
|
+
// is present, execute the factory with the jQuery instance from the window object
|
14
|
+
// For environments that do not inherently posses a window with a document
|
15
|
+
// (such as Node.js), expose a Tokenfield-making factory as module.exports
|
16
|
+
// This accentuates the need for the creation of a real window or passing in a jQuery instance
|
17
|
+
// e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
|
18
|
+
module.exports = global.window && global.window.$ ?
|
19
|
+
factory( global.window.$ ) :
|
20
|
+
function( input ) {
|
21
|
+
if ( !input.$ && !input.fn ) {
|
22
|
+
throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
|
23
|
+
}
|
24
|
+
return factory( input.$ || input );
|
25
|
+
};
|
26
|
+
} else {
|
27
|
+
// Browser globals
|
28
|
+
factory(jQuery, window);
|
29
|
+
}
|
30
|
+
}(function ($, window) {
|
31
|
+
|
32
|
+
"use strict"; // jshint ;_;
|
33
|
+
|
34
|
+
/* TOKENFIELD PUBLIC CLASS DEFINITION
|
35
|
+
* ============================== */
|
36
|
+
|
37
|
+
var Tokenfield = function (element, options) {
|
38
|
+
var _self = this
|
39
|
+
|
40
|
+
this.$element = $(element)
|
41
|
+
this.textDirection = this.$element.css('direction');
|
42
|
+
|
43
|
+
// Extend options
|
44
|
+
this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
|
45
|
+
|
46
|
+
// Setup delimiters and trigger keys
|
47
|
+
this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
|
48
|
+
this._triggerKeys = $.map(this._delimiters, function (delimiter) {
|
49
|
+
return delimiter.charCodeAt(0);
|
50
|
+
});
|
51
|
+
this._firstDelimiter = this._delimiters[0];
|
52
|
+
|
53
|
+
// Check for whitespace, dash and special characters
|
54
|
+
var whitespace = $.inArray(' ', this._delimiters)
|
55
|
+
, dash = $.inArray('-', this._delimiters)
|
56
|
+
|
57
|
+
if (whitespace >= 0)
|
58
|
+
this._delimiters[whitespace] = '\\s'
|
59
|
+
|
60
|
+
if (dash >= 0) {
|
61
|
+
delete this._delimiters[dash]
|
62
|
+
this._delimiters.unshift('-')
|
63
|
+
}
|
64
|
+
|
65
|
+
var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
|
66
|
+
$.each(this._delimiters, function (index, character) {
|
67
|
+
var pos = $.inArray(character, specialCharacters)
|
68
|
+
if (pos >= 0) _self._delimiters[index] = '\\' + character;
|
69
|
+
});
|
70
|
+
|
71
|
+
// Store original input width
|
72
|
+
var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
|
73
|
+
, elStyleWidth = element.style.width
|
74
|
+
, elCSSWidth
|
75
|
+
, elWidth = this.$element.width()
|
76
|
+
|
77
|
+
if (elRules) {
|
78
|
+
$.each( elRules, function (i, rule) {
|
79
|
+
if (rule.style.width) {
|
80
|
+
elCSSWidth = rule.style.width;
|
81
|
+
}
|
82
|
+
});
|
83
|
+
}
|
84
|
+
|
85
|
+
// Move original input out of the way
|
86
|
+
var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
|
87
|
+
originalStyles = { position: this.$element.css('position') };
|
88
|
+
originalStyles[hidingPosition] = this.$element.css(hidingPosition);
|
89
|
+
|
90
|
+
this.$element
|
91
|
+
.data('original-styles', originalStyles)
|
92
|
+
.data('original-tabindex', this.$element.prop('tabindex'))
|
93
|
+
.css('position', 'absolute')
|
94
|
+
.css(hidingPosition, '-10000px')
|
95
|
+
.prop('tabindex', -1)
|
96
|
+
|
97
|
+
// Create a wrapper
|
98
|
+
this.$wrapper = $('<div class="tokenfield form-control" />')
|
99
|
+
if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
|
100
|
+
if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
|
101
|
+
if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
|
102
|
+
|
103
|
+
// Create a new input
|
104
|
+
var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
|
105
|
+
this.$input = $('<input type="'+this.options.inputType+'" class="token-input" autocomplete="off" />')
|
106
|
+
.appendTo( this.$wrapper )
|
107
|
+
.prop( 'placeholder', this.$element.prop('placeholder') )
|
108
|
+
.prop( 'id', id + '-tokenfield' )
|
109
|
+
.prop( 'tabindex', this.$element.data('original-tabindex') )
|
110
|
+
|
111
|
+
// Re-route original input label to new input
|
112
|
+
var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
|
113
|
+
if ( $label.length ) {
|
114
|
+
$label.prop( 'for', this.$input.prop('id') )
|
115
|
+
}
|
116
|
+
|
117
|
+
// Set up a copy helper to handle copy & paste
|
118
|
+
this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
|
119
|
+
|
120
|
+
// Set wrapper width
|
121
|
+
if (elStyleWidth) {
|
122
|
+
this.$wrapper.css('width', elStyleWidth);
|
123
|
+
}
|
124
|
+
else if (elCSSWidth) {
|
125
|
+
this.$wrapper.css('width', elCSSWidth);
|
126
|
+
}
|
127
|
+
// If input is inside inline-form with no width set, set fixed width
|
128
|
+
else if (this.$element.parents('.form-inline').length) {
|
129
|
+
this.$wrapper.width( elWidth )
|
130
|
+
}
|
131
|
+
|
132
|
+
// Set tokenfield disabled, if original or fieldset input is disabled
|
133
|
+
if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
|
134
|
+
this.disable();
|
135
|
+
}
|
136
|
+
|
137
|
+
// Set tokenfield readonly, if original input is readonly
|
138
|
+
if (this.$element.prop('readonly')) {
|
139
|
+
this.readonly();
|
140
|
+
}
|
141
|
+
|
142
|
+
// Set up mirror for input auto-sizing
|
143
|
+
this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
|
144
|
+
this.$input.css('min-width', this.options.minWidth + 'px')
|
145
|
+
$.each([
|
146
|
+
'fontFamily',
|
147
|
+
'fontSize',
|
148
|
+
'fontWeight',
|
149
|
+
'fontStyle',
|
150
|
+
'letterSpacing',
|
151
|
+
'textTransform',
|
152
|
+
'wordSpacing',
|
153
|
+
'textIndent'
|
154
|
+
], function (i, val) {
|
155
|
+
_self.$mirror[0].style[val] = _self.$input.css(val);
|
156
|
+
});
|
157
|
+
this.$mirror.appendTo( 'body' )
|
158
|
+
|
159
|
+
// Insert tokenfield to HTML
|
160
|
+
this.$wrapper.insertBefore( this.$element )
|
161
|
+
this.$element.prependTo( this.$wrapper )
|
162
|
+
|
163
|
+
// Calculate inner input width
|
164
|
+
this.update()
|
165
|
+
|
166
|
+
// Create initial tokens, if any
|
167
|
+
this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
|
168
|
+
|
169
|
+
// Start listening to events
|
170
|
+
this.listen()
|
171
|
+
|
172
|
+
// Initialize autocomplete, if necessary
|
173
|
+
if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
|
174
|
+
var side = this.textDirection === 'rtl' ? 'right' : 'left'
|
175
|
+
, autocompleteOptions = $.extend({
|
176
|
+
minLength: this.options.showAutocompleteOnFocus ? 0 : null,
|
177
|
+
position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
|
178
|
+
}, this.options.autocomplete )
|
179
|
+
|
180
|
+
this.$input.autocomplete( autocompleteOptions )
|
181
|
+
}
|
182
|
+
|
183
|
+
// Initialize typeahead, if necessary
|
184
|
+
if ( ! $.isEmptyObject( this.options.typeahead ) ) {
|
185
|
+
|
186
|
+
var typeaheadOptions = this.options.typeahead
|
187
|
+
, defaults = {
|
188
|
+
minLength: this.options.showAutocompleteOnFocus ? 0 : null
|
189
|
+
}
|
190
|
+
, args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
|
191
|
+
|
192
|
+
args[0] = $.extend( {}, defaults, args[0] )
|
193
|
+
|
194
|
+
this.$input.typeahead.apply( this.$input, args )
|
195
|
+
this.typeahead = true
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
Tokenfield.prototype = {
|
200
|
+
|
201
|
+
constructor: Tokenfield
|
202
|
+
|
203
|
+
, createToken: function (attrs, triggerChange) {
|
204
|
+
var _self = this
|
205
|
+
|
206
|
+
if (typeof attrs === 'string') {
|
207
|
+
attrs = { value: attrs, label: attrs }
|
208
|
+
} else {
|
209
|
+
// Copy objects to prevent contamination of data sources.
|
210
|
+
attrs = $.extend( {}, attrs )
|
211
|
+
}
|
212
|
+
|
213
|
+
if (typeof triggerChange === 'undefined') {
|
214
|
+
triggerChange = true
|
215
|
+
}
|
216
|
+
|
217
|
+
// Normalize label and value
|
218
|
+
attrs.value = $.trim(attrs.value.toString());
|
219
|
+
attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
|
220
|
+
|
221
|
+
// Bail out if has no value or label, or label is too short
|
222
|
+
if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
|
223
|
+
|
224
|
+
// Bail out if maximum number of tokens is reached
|
225
|
+
if (this.options.limit && this.getTokens().length >= this.options.limit) return
|
226
|
+
|
227
|
+
// Allow changing token data before creating it
|
228
|
+
var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
|
229
|
+
this.$element.trigger(createEvent)
|
230
|
+
|
231
|
+
// Bail out if there if attributes are empty or event was defaultPrevented
|
232
|
+
if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
|
233
|
+
|
234
|
+
var $token = $('<div class="token" />')
|
235
|
+
.append('<span class="token-label" />')
|
236
|
+
.append('<a href="#" class="close" tabindex="-1">×</a>')
|
237
|
+
.data('attrs', attrs)
|
238
|
+
|
239
|
+
// Insert token into HTML
|
240
|
+
if (this.$input.hasClass('tt-input')) {
|
241
|
+
// If the input has typeahead enabled, insert token before it's parent
|
242
|
+
this.$input.parent().before( $token )
|
243
|
+
} else {
|
244
|
+
this.$input.before( $token )
|
245
|
+
}
|
246
|
+
|
247
|
+
// Temporarily set input width to minimum
|
248
|
+
this.$input.css('width', this.options.minWidth + 'px')
|
249
|
+
|
250
|
+
var $tokenLabel = $token.find('.token-label')
|
251
|
+
, $closeButton = $token.find('.close')
|
252
|
+
|
253
|
+
// Determine maximum possible token label width
|
254
|
+
if (!this.maxTokenWidth) {
|
255
|
+
this.maxTokenWidth =
|
256
|
+
this.$wrapper.width() - $closeButton.outerWidth() -
|
257
|
+
parseInt($closeButton.css('margin-left'), 10) -
|
258
|
+
parseInt($closeButton.css('margin-right'), 10) -
|
259
|
+
parseInt($token.css('border-left-width'), 10) -
|
260
|
+
parseInt($token.css('border-right-width'), 10) -
|
261
|
+
parseInt($token.css('padding-left'), 10) -
|
262
|
+
parseInt($token.css('padding-right'), 10)
|
263
|
+
parseInt($tokenLabel.css('border-left-width'), 10) -
|
264
|
+
parseInt($tokenLabel.css('border-right-width'), 10) -
|
265
|
+
parseInt($tokenLabel.css('padding-left'), 10) -
|
266
|
+
parseInt($tokenLabel.css('padding-right'), 10)
|
267
|
+
parseInt($tokenLabel.css('margin-left'), 10) -
|
268
|
+
parseInt($tokenLabel.css('margin-right'), 10)
|
269
|
+
}
|
270
|
+
|
271
|
+
$tokenLabel
|
272
|
+
.text(attrs.label)
|
273
|
+
.css('max-width', this.maxTokenWidth)
|
274
|
+
|
275
|
+
// Listen to events on token
|
276
|
+
$token
|
277
|
+
.on('mousedown', function (e) {
|
278
|
+
if (_self._disabled || _self._readonly) return false
|
279
|
+
_self.preventDeactivation = true
|
280
|
+
})
|
281
|
+
.on('click', function (e) {
|
282
|
+
if (_self._disabled || _self._readonly) return false
|
283
|
+
_self.preventDeactivation = false
|
284
|
+
|
285
|
+
if (e.ctrlKey || e.metaKey) {
|
286
|
+
e.preventDefault()
|
287
|
+
return _self.toggle( $token )
|
288
|
+
}
|
289
|
+
|
290
|
+
_self.activate( $token, e.shiftKey, e.shiftKey )
|
291
|
+
})
|
292
|
+
.on('dblclick', function (e) {
|
293
|
+
if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
|
294
|
+
_self.edit( $token )
|
295
|
+
})
|
296
|
+
|
297
|
+
$closeButton
|
298
|
+
.on('click', $.proxy(this.remove, this))
|
299
|
+
|
300
|
+
// Trigger createdtoken event on the original field
|
301
|
+
// indicating that the token is now in the DOM
|
302
|
+
this.$element.trigger($.Event('tokenfield:createdtoken', {
|
303
|
+
attrs: attrs,
|
304
|
+
relatedTarget: $token.get(0)
|
305
|
+
}))
|
306
|
+
|
307
|
+
// Trigger change event on the original field
|
308
|
+
if (triggerChange) {
|
309
|
+
this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
|
310
|
+
}
|
311
|
+
|
312
|
+
// Update tokenfield dimensions
|
313
|
+
this.update()
|
314
|
+
|
315
|
+
// Return original element
|
316
|
+
return this.$element.get(0)
|
317
|
+
}
|
318
|
+
|
319
|
+
, setTokens: function (tokens, add, triggerChange) {
|
320
|
+
if (!tokens) return
|
321
|
+
|
322
|
+
if (!add) this.$wrapper.find('.token').remove()
|
323
|
+
|
324
|
+
if (typeof triggerChange === 'undefined') {
|
325
|
+
triggerChange = true
|
326
|
+
}
|
327
|
+
|
328
|
+
if (typeof tokens === 'string') {
|
329
|
+
if (this._delimiters.length) {
|
330
|
+
// Split based on delimiters
|
331
|
+
tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
|
332
|
+
} else {
|
333
|
+
tokens = [tokens];
|
334
|
+
}
|
335
|
+
}
|
336
|
+
|
337
|
+
var _self = this
|
338
|
+
$.each(tokens, function (i, attrs) {
|
339
|
+
_self.createToken(attrs, triggerChange)
|
340
|
+
})
|
341
|
+
|
342
|
+
return this.$element.get(0)
|
343
|
+
}
|
344
|
+
|
345
|
+
, getTokenData: function($token) {
|
346
|
+
var data = $token.map(function() {
|
347
|
+
var $token = $(this);
|
348
|
+
return $token.data('attrs')
|
349
|
+
}).get();
|
350
|
+
|
351
|
+
if (data.length == 1) {
|
352
|
+
data = data[0];
|
353
|
+
}
|
354
|
+
|
355
|
+
return data;
|
356
|
+
}
|
357
|
+
|
358
|
+
, getTokens: function(active) {
|
359
|
+
var self = this
|
360
|
+
, tokens = []
|
361
|
+
, activeClass = active ? '.active' : '' // get active tokens only
|
362
|
+
this.$wrapper.find( '.token' + activeClass ).each( function() {
|
363
|
+
tokens.push( self.getTokenData( $(this) ) )
|
364
|
+
})
|
365
|
+
return tokens
|
366
|
+
}
|
367
|
+
|
368
|
+
, getTokensList: function(delimiter, beautify, active) {
|
369
|
+
delimiter = delimiter || this._firstDelimiter
|
370
|
+
beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
|
371
|
+
|
372
|
+
var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
|
373
|
+
return $.map( this.getTokens(active), function (token) {
|
374
|
+
return token.value
|
375
|
+
}).join(separator)
|
376
|
+
}
|
377
|
+
|
378
|
+
, getInput: function() {
|
379
|
+
return this.$input.val()
|
380
|
+
}
|
381
|
+
|
382
|
+
, listen: function () {
|
383
|
+
var _self = this
|
384
|
+
|
385
|
+
this.$element
|
386
|
+
.on('change', $.proxy(this.change, this))
|
387
|
+
|
388
|
+
this.$wrapper
|
389
|
+
.on('mousedown',$.proxy(this.focusInput, this))
|
390
|
+
|
391
|
+
this.$input
|
392
|
+
.on('focus', $.proxy(this.focus, this))
|
393
|
+
.on('blur', $.proxy(this.blur, this))
|
394
|
+
.on('paste', $.proxy(this.paste, this))
|
395
|
+
.on('keydown', $.proxy(this.keydown, this))
|
396
|
+
.on('keypress', $.proxy(this.keypress, this))
|
397
|
+
.on('keyup', $.proxy(this.keyup, this))
|
398
|
+
|
399
|
+
this.$copyHelper
|
400
|
+
.on('focus', $.proxy(this.focus, this))
|
401
|
+
.on('blur', $.proxy(this.blur, this))
|
402
|
+
.on('keydown', $.proxy(this.keydown, this))
|
403
|
+
.on('keyup', $.proxy(this.keyup, this))
|
404
|
+
|
405
|
+
// Secondary listeners for input width calculation
|
406
|
+
this.$input
|
407
|
+
.on('keypress', $.proxy(this.update, this))
|
408
|
+
.on('keyup', $.proxy(this.update, this))
|
409
|
+
|
410
|
+
this.$input
|
411
|
+
.on('autocompletecreate', function() {
|
412
|
+
// Set minimum autocomplete menu width
|
413
|
+
var $_menuElement = $(this).data('ui-autocomplete').menu.element
|
414
|
+
|
415
|
+
var minWidth = _self.$wrapper.outerWidth() -
|
416
|
+
parseInt( $_menuElement.css('border-left-width'), 10 ) -
|
417
|
+
parseInt( $_menuElement.css('border-right-width'), 10 )
|
418
|
+
|
419
|
+
$_menuElement.css( 'min-width', minWidth + 'px' )
|
420
|
+
})
|
421
|
+
.on('autocompleteselect', function (e, ui) {
|
422
|
+
if (_self.createToken( ui.item )) {
|
423
|
+
_self.$input.val('')
|
424
|
+
if (_self.$input.data( 'edit' )) {
|
425
|
+
_self.unedit(true)
|
426
|
+
}
|
427
|
+
}
|
428
|
+
return false
|
429
|
+
})
|
430
|
+
.on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
|
431
|
+
// Create token
|
432
|
+
if (_self.createToken( datum )) {
|
433
|
+
_self.$input.typeahead('val', '')
|
434
|
+
if (_self.$input.data( 'edit' )) {
|
435
|
+
_self.unedit(true)
|
436
|
+
}
|
437
|
+
}
|
438
|
+
})
|
439
|
+
|
440
|
+
// Listen to window resize
|
441
|
+
$(window).on('resize', $.proxy(this.update, this ))
|
442
|
+
|
443
|
+
}
|
444
|
+
|
445
|
+
, keydown: function (e) {
|
446
|
+
|
447
|
+
if (!this.focused) return
|
448
|
+
|
449
|
+
var _self = this
|
450
|
+
|
451
|
+
switch(e.keyCode) {
|
452
|
+
case 8: // backspace
|
453
|
+
if (!this.$input.is(document.activeElement)) break
|
454
|
+
this.lastInputValue = this.$input.val()
|
455
|
+
break
|
456
|
+
|
457
|
+
case 37: // left arrow
|
458
|
+
leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
|
459
|
+
break
|
460
|
+
|
461
|
+
case 38: // up arrow
|
462
|
+
upDown('prev')
|
463
|
+
break
|
464
|
+
|
465
|
+
case 39: // right arrow
|
466
|
+
leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
|
467
|
+
break
|
468
|
+
|
469
|
+
case 40: // down arrow
|
470
|
+
upDown('next')
|
471
|
+
break
|
472
|
+
|
473
|
+
case 65: // a (to handle ctrl + a)
|
474
|
+
if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
|
475
|
+
this.activateAll()
|
476
|
+
e.preventDefault()
|
477
|
+
break
|
478
|
+
|
479
|
+
case 9: // tab
|
480
|
+
case 13: // enter
|
481
|
+
|
482
|
+
// We will handle creating tokens from autocomplete in autocomplete events
|
483
|
+
if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break
|
484
|
+
|
485
|
+
// We will handle creating tokens from typeahead in typeahead events
|
486
|
+
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
|
487
|
+
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
|
488
|
+
|
489
|
+
// Create token
|
490
|
+
if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
|
491
|
+
return this.createTokensFromInput(e, this.$input.data('edit'));
|
492
|
+
}
|
493
|
+
|
494
|
+
// Edit token
|
495
|
+
if (e.keyCode === 13) {
|
496
|
+
if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
|
497
|
+
if (!_self.options.allowEditing) break
|
498
|
+
this.edit( this.$wrapper.find('.token.active') )
|
499
|
+
}
|
500
|
+
}
|
501
|
+
|
502
|
+
function leftRight(direction) {
|
503
|
+
if (_self.$input.is(document.activeElement)) {
|
504
|
+
if (_self.$input.val().length > 0) return
|
505
|
+
|
506
|
+
direction += 'All'
|
507
|
+
var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
|
508
|
+
if (!$token.length) return
|
509
|
+
|
510
|
+
_self.preventInputFocus = true
|
511
|
+
_self.preventDeactivation = true
|
512
|
+
|
513
|
+
_self.activate( $token )
|
514
|
+
e.preventDefault()
|
515
|
+
|
516
|
+
} else {
|
517
|
+
_self[direction]( e.shiftKey )
|
518
|
+
e.preventDefault()
|
519
|
+
}
|
520
|
+
}
|
521
|
+
|
522
|
+
function upDown(direction) {
|
523
|
+
if (!e.shiftKey) return
|
524
|
+
|
525
|
+
if (_self.$input.is(document.activeElement)) {
|
526
|
+
if (_self.$input.val().length > 0) return
|
527
|
+
|
528
|
+
var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
|
529
|
+
if (!$token.length) return
|
530
|
+
|
531
|
+
_self.activate( $token )
|
532
|
+
}
|
533
|
+
|
534
|
+
var opposite = direction === 'prev' ? 'next' : 'prev'
|
535
|
+
, position = direction === 'prev' ? 'first' : 'last'
|
536
|
+
|
537
|
+
_self.$firstActiveToken[opposite + 'All']('.token').each(function() {
|
538
|
+
_self.deactivate( $(this) )
|
539
|
+
})
|
540
|
+
|
541
|
+
_self.activate( _self.$wrapper.find('.token:' + position), true, true )
|
542
|
+
e.preventDefault()
|
543
|
+
}
|
544
|
+
|
545
|
+
this.lastKeyDown = e.keyCode
|
546
|
+
}
|
547
|
+
|
548
|
+
, keypress: function(e) {
|
549
|
+
|
550
|
+
// Comma
|
551
|
+
if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
|
552
|
+
if (this.$input.val()) {
|
553
|
+
this.createTokensFromInput(e)
|
554
|
+
}
|
555
|
+
return false;
|
556
|
+
}
|
557
|
+
}
|
558
|
+
|
559
|
+
, keyup: function (e) {
|
560
|
+
this.preventInputFocus = false
|
561
|
+
|
562
|
+
if (!this.focused) return
|
563
|
+
|
564
|
+
switch(e.keyCode) {
|
565
|
+
case 8: // backspace
|
566
|
+
if (this.$input.is(document.activeElement)) {
|
567
|
+
if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
|
568
|
+
|
569
|
+
this.preventDeactivation = true
|
570
|
+
var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
|
571
|
+
|
572
|
+
if (!$prevToken.length) break
|
573
|
+
|
574
|
+
this.activate( $prevToken )
|
575
|
+
} else {
|
576
|
+
this.remove(e)
|
577
|
+
}
|
578
|
+
break
|
579
|
+
|
580
|
+
case 46: // delete
|
581
|
+
this.remove(e, 'next')
|
582
|
+
break
|
583
|
+
}
|
584
|
+
this.lastKeyUp = e.keyCode
|
585
|
+
}
|
586
|
+
|
587
|
+
, focus: function (e) {
|
588
|
+
this.focused = true
|
589
|
+
this.$wrapper.addClass('focus')
|
590
|
+
|
591
|
+
if (this.$input.is(document.activeElement)) {
|
592
|
+
this.$wrapper.find('.active').removeClass('active')
|
593
|
+
this.$firstActiveToken = null
|
594
|
+
|
595
|
+
if (this.options.showAutocompleteOnFocus) {
|
596
|
+
this.search()
|
597
|
+
}
|
598
|
+
}
|
599
|
+
}
|
600
|
+
|
601
|
+
, blur: function (e) {
|
602
|
+
|
603
|
+
this.focused = false
|
604
|
+
this.$wrapper.removeClass('focus')
|
605
|
+
|
606
|
+
if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
|
607
|
+
this.$wrapper.find('.active').removeClass('active')
|
608
|
+
this.$firstActiveToken = null
|
609
|
+
}
|
610
|
+
|
611
|
+
if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
|
612
|
+
this.createTokensFromInput(e)
|
613
|
+
}
|
614
|
+
|
615
|
+
this.preventDeactivation = false
|
616
|
+
this.preventCreateTokens = false
|
617
|
+
}
|
618
|
+
|
619
|
+
, paste: function (e) {
|
620
|
+
var _self = this
|
621
|
+
|
622
|
+
// Add tokens to existing ones
|
623
|
+
if (_self.options.allowPasting) {
|
624
|
+
setTimeout(function () {
|
625
|
+
_self.createTokensFromInput(e)
|
626
|
+
}, 1)
|
627
|
+
}
|
628
|
+
}
|
629
|
+
|
630
|
+
, change: function (e) {
|
631
|
+
if ( e.initiator === 'tokenfield' ) return // Prevent loops
|
632
|
+
|
633
|
+
this.setTokens( this.$element.val() )
|
634
|
+
}
|
635
|
+
|
636
|
+
, createTokensFromInput: function (e, focus) {
|
637
|
+
if (this.$input.val().length < this.options.minLength)
|
638
|
+
return // No input, simply return
|
639
|
+
|
640
|
+
var tokensBefore = this.getTokensList()
|
641
|
+
this.setTokens( this.$input.val(), true )
|
642
|
+
|
643
|
+
if (tokensBefore == this.getTokensList() && this.$input.val().length)
|
644
|
+
return false // No tokens were added, do nothing (prevent form submit)
|
645
|
+
|
646
|
+
if (this.$input.hasClass('tt-input')) {
|
647
|
+
// Typeahead acts weird when simply setting input value to empty,
|
648
|
+
// so we set the query to empty instead
|
649
|
+
this.$input.typeahead('val', '')
|
650
|
+
} else {
|
651
|
+
this.$input.val('')
|
652
|
+
}
|
653
|
+
|
654
|
+
if (this.$input.data( 'edit' )) {
|
655
|
+
this.unedit(focus)
|
656
|
+
}
|
657
|
+
|
658
|
+
return false // Prevent form being submitted
|
659
|
+
}
|
660
|
+
|
661
|
+
, next: function (add) {
|
662
|
+
if (add) {
|
663
|
+
var $firstActiveToken = this.$wrapper.find('.active:first')
|
664
|
+
, deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
|
665
|
+
|
666
|
+
if (deactivate) return this.deactivate( $firstActiveToken )
|
667
|
+
}
|
668
|
+
|
669
|
+
var $lastActiveToken = this.$wrapper.find('.active:last')
|
670
|
+
, $nextToken = $lastActiveToken.nextAll('.token:first')
|
671
|
+
|
672
|
+
if (!$nextToken.length) {
|
673
|
+
this.$input.focus()
|
674
|
+
return
|
675
|
+
}
|
676
|
+
|
677
|
+
this.activate($nextToken, add)
|
678
|
+
}
|
679
|
+
|
680
|
+
, prev: function (add) {
|
681
|
+
|
682
|
+
if (add) {
|
683
|
+
var $lastActiveToken = this.$wrapper.find('.active:last')
|
684
|
+
, deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
|
685
|
+
|
686
|
+
if (deactivate) return this.deactivate( $lastActiveToken )
|
687
|
+
}
|
688
|
+
|
689
|
+
var $firstActiveToken = this.$wrapper.find('.active:first')
|
690
|
+
, $prevToken = $firstActiveToken.prevAll('.token:first')
|
691
|
+
|
692
|
+
if (!$prevToken.length) {
|
693
|
+
$prevToken = this.$wrapper.find('.token:first')
|
694
|
+
}
|
695
|
+
|
696
|
+
if (!$prevToken.length && !add) {
|
697
|
+
this.$input.focus()
|
698
|
+
return
|
699
|
+
}
|
700
|
+
|
701
|
+
this.activate( $prevToken, add )
|
702
|
+
}
|
703
|
+
|
704
|
+
, activate: function ($token, add, multi, remember) {
|
705
|
+
|
706
|
+
if (!$token) return
|
707
|
+
|
708
|
+
if (typeof remember === 'undefined') var remember = true
|
709
|
+
|
710
|
+
if (multi) var add = true
|
711
|
+
|
712
|
+
this.$copyHelper.focus()
|
713
|
+
|
714
|
+
if (!add) {
|
715
|
+
this.$wrapper.find('.active').removeClass('active')
|
716
|
+
if (remember) {
|
717
|
+
this.$firstActiveToken = $token
|
718
|
+
} else {
|
719
|
+
delete this.$firstActiveToken
|
720
|
+
}
|
721
|
+
}
|
722
|
+
|
723
|
+
if (multi && this.$firstActiveToken) {
|
724
|
+
// Determine first active token and the current tokens indicies
|
725
|
+
// Account for the 1 hidden textarea by subtracting 1 from both
|
726
|
+
var i = this.$firstActiveToken.index() - 2
|
727
|
+
, a = $token.index() - 2
|
728
|
+
, _self = this
|
729
|
+
|
730
|
+
this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
|
731
|
+
_self.activate( $(this), true )
|
732
|
+
})
|
733
|
+
}
|
734
|
+
|
735
|
+
$token.addClass('active')
|
736
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
737
|
+
}
|
738
|
+
|
739
|
+
, activateAll: function() {
|
740
|
+
var _self = this
|
741
|
+
|
742
|
+
this.$wrapper.find('.token').each( function (i) {
|
743
|
+
_self.activate($(this), i !== 0, false, false)
|
744
|
+
})
|
745
|
+
}
|
746
|
+
|
747
|
+
, deactivate: function($token) {
|
748
|
+
if (!$token) return
|
749
|
+
|
750
|
+
$token.removeClass('active')
|
751
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
752
|
+
}
|
753
|
+
|
754
|
+
, toggle: function($token) {
|
755
|
+
if (!$token) return
|
756
|
+
|
757
|
+
$token.toggleClass('active')
|
758
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
759
|
+
}
|
760
|
+
|
761
|
+
, edit: function ($token) {
|
762
|
+
if (!$token) return
|
763
|
+
|
764
|
+
var attrs = $token.data('attrs')
|
765
|
+
|
766
|
+
// Allow changing input value before editing
|
767
|
+
var options = { attrs: attrs, relatedTarget: $token.get(0) }
|
768
|
+
var editEvent = $.Event('tokenfield:edittoken', options)
|
769
|
+
this.$element.trigger( editEvent )
|
770
|
+
|
771
|
+
// Edit event can be cancelled if default is prevented
|
772
|
+
if (editEvent.isDefaultPrevented()) return
|
773
|
+
|
774
|
+
$token.find('.token-label').text(attrs.value)
|
775
|
+
var tokenWidth = $token.outerWidth()
|
776
|
+
|
777
|
+
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
|
778
|
+
|
779
|
+
$token.replaceWith( $_input )
|
780
|
+
|
781
|
+
this.preventCreateTokens = true
|
782
|
+
|
783
|
+
this.$input.val( attrs.value )
|
784
|
+
.select()
|
785
|
+
.data( 'edit', true )
|
786
|
+
.width( tokenWidth )
|
787
|
+
|
788
|
+
this.update();
|
789
|
+
|
790
|
+
// Indicate that token is now being edited, and is replaced with an input field in the DOM
|
791
|
+
this.$element.trigger($.Event('tokenfield:editedtoken', options ))
|
792
|
+
}
|
793
|
+
|
794
|
+
, unedit: function (focus) {
|
795
|
+
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
|
796
|
+
$_input.appendTo( this.$wrapper )
|
797
|
+
|
798
|
+
this.$input.data('edit', false)
|
799
|
+
this.$mirror.text('')
|
800
|
+
|
801
|
+
this.update()
|
802
|
+
|
803
|
+
// Because moving the input element around in DOM
|
804
|
+
// will cause it to lose focus, we provide an option
|
805
|
+
// to re-focus the input after appending it to the wrapper
|
806
|
+
if (focus) {
|
807
|
+
var _self = this
|
808
|
+
setTimeout(function () {
|
809
|
+
_self.$input.focus()
|
810
|
+
}, 1)
|
811
|
+
}
|
812
|
+
}
|
813
|
+
|
814
|
+
, remove: function (e, direction) {
|
815
|
+
if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
|
816
|
+
|
817
|
+
var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
|
818
|
+
|
819
|
+
if (e.type !== 'click') {
|
820
|
+
if (!direction) var direction = 'prev'
|
821
|
+
this[direction]()
|
822
|
+
|
823
|
+
// Was it the first token?
|
824
|
+
if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
|
825
|
+
}
|
826
|
+
|
827
|
+
// Prepare events and their options
|
828
|
+
var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
|
829
|
+
, removeEvent = $.Event('tokenfield:removetoken', options)
|
830
|
+
|
831
|
+
this.$element.trigger(removeEvent);
|
832
|
+
|
833
|
+
// Remove event can be intercepted and cancelled
|
834
|
+
if (removeEvent.isDefaultPrevented()) return
|
835
|
+
|
836
|
+
var removedEvent = $.Event('tokenfield:removedtoken', options)
|
837
|
+
, changeEvent = $.Event('change', { initiator: 'tokenfield' })
|
838
|
+
|
839
|
+
// Remove token from DOM
|
840
|
+
$token.remove()
|
841
|
+
|
842
|
+
// Trigger events
|
843
|
+
this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
|
844
|
+
|
845
|
+
// Focus, when necessary:
|
846
|
+
// When there are no more tokens, or if this was the first token
|
847
|
+
// and it was removed with backspace or it was clicked on
|
848
|
+
if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
|
849
|
+
|
850
|
+
// Adjust input width
|
851
|
+
this.$input.css('width', this.options.minWidth + 'px')
|
852
|
+
this.update()
|
853
|
+
|
854
|
+
// Cancel original event handlers
|
855
|
+
e.preventDefault()
|
856
|
+
e.stopPropagation()
|
857
|
+
}
|
858
|
+
|
859
|
+
/**
|
860
|
+
* Update tokenfield dimensions
|
861
|
+
*/
|
862
|
+
, update: function (e) {
|
863
|
+
var value = this.$input.val()
|
864
|
+
, inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
|
865
|
+
, inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
|
866
|
+
, inputPadding = inputPaddingLeft + inputPaddingRight
|
867
|
+
|
868
|
+
if (this.$input.data('edit')) {
|
869
|
+
|
870
|
+
if (!value) {
|
871
|
+
value = this.$input.prop("placeholder")
|
872
|
+
}
|
873
|
+
if (value === this.$mirror.text()) return
|
874
|
+
|
875
|
+
this.$mirror.text(value)
|
876
|
+
|
877
|
+
var mirrorWidth = this.$mirror.width() + 10;
|
878
|
+
if ( mirrorWidth > this.$wrapper.width() ) {
|
879
|
+
return this.$input.width( this.$wrapper.width() )
|
880
|
+
}
|
881
|
+
|
882
|
+
this.$input.width( mirrorWidth )
|
883
|
+
}
|
884
|
+
else {
|
885
|
+
var w = (this.textDirection === 'rtl')
|
886
|
+
? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
|
887
|
+
: this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
|
888
|
+
//
|
889
|
+
// some usecases pre-render widget before attaching to DOM,
|
890
|
+
// dimensions returned by jquery will be NaN -> we default to 100%
|
891
|
+
// so placeholder won't be cut off.
|
892
|
+
isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
|
893
|
+
}
|
894
|
+
}
|
895
|
+
|
896
|
+
, focusInput: function (e) {
|
897
|
+
if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
|
898
|
+
// Focus only after the current call stack has cleared,
|
899
|
+
// otherwise has no effect.
|
900
|
+
// Reason: mousedown is too early - input will lose focus
|
901
|
+
// after mousedown. However, since the input may be moved
|
902
|
+
// in DOM, there may be no click or mouseup event triggered.
|
903
|
+
var _self = this
|
904
|
+
setTimeout(function() {
|
905
|
+
_self.$input.focus()
|
906
|
+
}, 0)
|
907
|
+
}
|
908
|
+
|
909
|
+
, search: function () {
|
910
|
+
if ( this.$input.data('ui-autocomplete') ) {
|
911
|
+
this.$input.autocomplete('search')
|
912
|
+
}
|
913
|
+
}
|
914
|
+
|
915
|
+
, disable: function () {
|
916
|
+
this.setProperty('disabled', true);
|
917
|
+
}
|
918
|
+
|
919
|
+
, enable: function () {
|
920
|
+
this.setProperty('disabled', false);
|
921
|
+
}
|
922
|
+
|
923
|
+
, readonly: function () {
|
924
|
+
this.setProperty('readonly', true);
|
925
|
+
}
|
926
|
+
|
927
|
+
, writeable: function () {
|
928
|
+
this.setProperty('readonly', false);
|
929
|
+
}
|
930
|
+
|
931
|
+
, setProperty: function(property, value) {
|
932
|
+
this['_' + property] = value;
|
933
|
+
this.$input.prop(property, value);
|
934
|
+
this.$element.prop(property, value);
|
935
|
+
this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
|
936
|
+
}
|
937
|
+
|
938
|
+
, destroy: function() {
|
939
|
+
// Set field value
|
940
|
+
this.$element.val( this.getTokensList() );
|
941
|
+
// Restore styles and properties
|
942
|
+
this.$element.css( this.$element.data('original-styles') );
|
943
|
+
this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
|
944
|
+
|
945
|
+
// Re-route tokenfield label to original input
|
946
|
+
var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
|
947
|
+
if ( $label.length ) {
|
948
|
+
$label.prop( 'for', this.$element.prop('id') )
|
949
|
+
}
|
950
|
+
|
951
|
+
// Move original element outside of tokenfield wrapper
|
952
|
+
this.$element.insertBefore( this.$wrapper );
|
953
|
+
|
954
|
+
// Remove tokenfield-related data
|
955
|
+
this.$element.removeData('original-styles')
|
956
|
+
.removeData('original-tabindex')
|
957
|
+
.removeData('bs.tokenfield');
|
958
|
+
|
959
|
+
// Remove tokenfield from DOM
|
960
|
+
this.$wrapper.remove();
|
961
|
+
this.$mirror.remove();
|
962
|
+
|
963
|
+
var $_element = this.$element;
|
964
|
+
|
965
|
+
return $_element;
|
966
|
+
}
|
967
|
+
|
968
|
+
}
|
969
|
+
|
970
|
+
|
971
|
+
/* TOKENFIELD PLUGIN DEFINITION
|
972
|
+
* ======================== */
|
973
|
+
|
974
|
+
var old = $.fn.tokenfield
|
975
|
+
|
976
|
+
$.fn.tokenfield = function (option, param) {
|
977
|
+
var value
|
978
|
+
, args = []
|
979
|
+
|
980
|
+
Array.prototype.push.apply( args, arguments );
|
981
|
+
|
982
|
+
var elements = this.each(function () {
|
983
|
+
var $this = $(this)
|
984
|
+
, data = $this.data('bs.tokenfield')
|
985
|
+
, options = typeof option == 'object' && option
|
986
|
+
|
987
|
+
if (typeof option === 'string' && data && data[option]) {
|
988
|
+
args.shift()
|
989
|
+
value = data[option].apply(data, args)
|
990
|
+
} else {
|
991
|
+
if (!data && typeof option !== 'string' && !param) {
|
992
|
+
$this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
|
993
|
+
$this.trigger('tokenfield:initialize')
|
994
|
+
}
|
995
|
+
}
|
996
|
+
})
|
997
|
+
|
998
|
+
return typeof value !== 'undefined' ? value : elements;
|
999
|
+
}
|
1000
|
+
|
1001
|
+
$.fn.tokenfield.defaults = {
|
1002
|
+
minWidth: 60,
|
1003
|
+
minLength: 0,
|
1004
|
+
allowEditing: true,
|
1005
|
+
allowPasting: true,
|
1006
|
+
limit: 0,
|
1007
|
+
autocomplete: {},
|
1008
|
+
typeahead: {},
|
1009
|
+
showAutocompleteOnFocus: false,
|
1010
|
+
createTokensOnBlur: false,
|
1011
|
+
delimiter: ',',
|
1012
|
+
beautify: true,
|
1013
|
+
inputType: 'text'
|
1014
|
+
}
|
1015
|
+
|
1016
|
+
$.fn.tokenfield.Constructor = Tokenfield
|
1017
|
+
|
1018
|
+
|
1019
|
+
/* TOKENFIELD NO CONFLICT
|
1020
|
+
* ================== */
|
1021
|
+
|
1022
|
+
$.fn.tokenfield.noConflict = function () {
|
1023
|
+
$.fn.tokenfield = old
|
1024
|
+
return this
|
1025
|
+
}
|
1026
|
+
|
1027
|
+
return Tokenfield;
|
1028
|
+
|
1029
|
+
}));
|