tagify 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }));