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.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tagify.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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);
@@ -0,0 +1,7 @@
1
+ require "tagify/version"
2
+
3
+ module Tagify
4
+ class Engine < Rails::Engine
5
+ require 'jquery-ui-rails'
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Tagify
2
+ VERSION = "0.0.2"
3
+ end
@@ -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">&times;</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
+ }));