tawork 0.0.17 → 0.0.18
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 +8 -8
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -0
- data/app/assets/javascripts/application.js +1 -0
- data/app/assets/javascripts/backbone/views/page.js.coffee +15 -1
- data/app/assets/stylesheets/application.css +1 -0
- data/app/assets/stylesheets/bootstrap_and_overrides.sass +9 -0
- data/app/controllers/wiki/pages_controller.rb +12 -0
- data/app/models/page.rb +3 -0
- data/app/views/wiki/pages/_display.html.haml +25 -1
- data/app/views/wiki/pages/_last_updated.html.haml +1 -1
- data/app/views/wiki/pages/_page_header.html.haml +7 -3
- data/app/views/wiki/pages/_tags.html.haml +2 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20140314014648_acts_as_taggable_on_migration.acts_as_taggable_on_engine.rb +31 -0
- data/db/migrate/20140314014649_add_missing_unique_indices.acts_as_taggable_on_engine.rb +22 -0
- data/db/schema.rb +20 -2
- data/lib/tawork/version.rb +1 -1
- data/tags +28 -0
- data/vendor/assets/javascripts/bootstrap-tokenfield.js +1022 -0
- data/vendor/assets/stylesheets/bootstrap-tokenfield.css +209 -0
- data/vendor/assets/stylesheets/tokenfield-typeahead.css +141 -0
- metadata +7 -1
@@ -0,0 +1,1022 @@
|
|
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);
|
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, char) {
|
67
|
+
var pos = $.inArray(char, specialCharacters)
|
68
|
+
if (pos >= 0) _self._delimiters[index] = '\\' + char;
|
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="text" 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 up mirror for input auto-sizing
|
138
|
+
this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
|
139
|
+
this.$input.css('min-width', this.options.minWidth + 'px')
|
140
|
+
$.each([
|
141
|
+
'fontFamily',
|
142
|
+
'fontSize',
|
143
|
+
'fontWeight',
|
144
|
+
'fontStyle',
|
145
|
+
'letterSpacing',
|
146
|
+
'textTransform',
|
147
|
+
'wordSpacing',
|
148
|
+
'textIndent'
|
149
|
+
], function (i, val) {
|
150
|
+
_self.$mirror[0].style[val] = _self.$input.css(val);
|
151
|
+
});
|
152
|
+
this.$mirror.appendTo( 'body' )
|
153
|
+
|
154
|
+
// Insert tokenfield to HTML
|
155
|
+
this.$wrapper.insertBefore( this.$element )
|
156
|
+
this.$element.prependTo( this.$wrapper )
|
157
|
+
|
158
|
+
// Calculate inner input width
|
159
|
+
this.update()
|
160
|
+
|
161
|
+
// Create initial tokens, if any
|
162
|
+
this.setTokens(this.options.tokens, false, false)
|
163
|
+
|
164
|
+
// Start listening to events
|
165
|
+
this.listen()
|
166
|
+
|
167
|
+
// Initialize autocomplete, if necessary
|
168
|
+
if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
|
169
|
+
var side = this.textDirection === 'rtl' ? 'right' : 'left'
|
170
|
+
var autocompleteOptions = $.extend({
|
171
|
+
minLength: this.options.showAutocompleteOnFocus ? 0 : null,
|
172
|
+
position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
|
173
|
+
}, this.options.autocomplete )
|
174
|
+
this.$input.autocomplete( autocompleteOptions )
|
175
|
+
}
|
176
|
+
|
177
|
+
// Initialize typeahead, if necessary
|
178
|
+
if ( ! $.isEmptyObject( this.options.typeahead ) ) {
|
179
|
+
var typeaheadOptions = $.extend({
|
180
|
+
minLength: this.options.showAutocompleteOnFocus ? 0 : null
|
181
|
+
}, this.options.typeahead)
|
182
|
+
this.$input.typeahead( null, typeaheadOptions )
|
183
|
+
this.typeahead = true
|
184
|
+
}
|
185
|
+
|
186
|
+
this.$element.trigger('tokenfield:initialize')
|
187
|
+
}
|
188
|
+
|
189
|
+
Tokenfield.prototype = {
|
190
|
+
|
191
|
+
constructor: Tokenfield
|
192
|
+
|
193
|
+
, createToken: function (attrs, triggerChange) {
|
194
|
+
if (typeof attrs === 'string') {
|
195
|
+
attrs = { value: attrs, label: attrs }
|
196
|
+
}
|
197
|
+
|
198
|
+
if (typeof triggerChange === 'undefined') {
|
199
|
+
triggerChange = true
|
200
|
+
}
|
201
|
+
|
202
|
+
var _self = this
|
203
|
+
, value = $.trim(attrs.value)
|
204
|
+
, label = attrs.label && attrs.label.length ? $.trim(attrs.label) : value
|
205
|
+
|
206
|
+
if (!value.length || !label.length || value.length < this.options.minLength) return
|
207
|
+
|
208
|
+
if (this.options.limit && this.getTokens().length >= this.options.limit) return
|
209
|
+
|
210
|
+
// Allow changing token data before creating it
|
211
|
+
var prepareEvent = $.Event('tokenfield:preparetoken')
|
212
|
+
prepareEvent.token = {
|
213
|
+
value: value,
|
214
|
+
label: label
|
215
|
+
}
|
216
|
+
this.$element.trigger( prepareEvent )
|
217
|
+
|
218
|
+
if (!prepareEvent.token) return
|
219
|
+
|
220
|
+
value = prepareEvent.token.value
|
221
|
+
label = prepareEvent.token.label
|
222
|
+
|
223
|
+
// Check for duplicates
|
224
|
+
if (!this.options.allowDuplicates && $.grep(this.getTokens(), function (token) {
|
225
|
+
return token.value === value
|
226
|
+
}).length) {
|
227
|
+
// Allow listening to when duplicates get prevented
|
228
|
+
var preventDuplicateEvent = $.Event('tokenfield:preventduplicate')
|
229
|
+
preventDuplicateEvent.token = {
|
230
|
+
value: value,
|
231
|
+
label: label
|
232
|
+
}
|
233
|
+
this.$element.trigger( preventDuplicateEvent )
|
234
|
+
// Add duplicate warning class to existing token for 250ms
|
235
|
+
var duplicate = this.$wrapper.find( '.token[data-value="' + value + '"]' ).addClass('duplicate')
|
236
|
+
setTimeout(function() {
|
237
|
+
duplicate.removeClass('duplicate');
|
238
|
+
}, 250)
|
239
|
+
return false
|
240
|
+
}
|
241
|
+
|
242
|
+
var token = $('<div class="token" />')
|
243
|
+
.attr('data-value', value)
|
244
|
+
.append('<span class="token-label" />')
|
245
|
+
.append('<a href="#" class="close" tabindex="-1">×</a>')
|
246
|
+
|
247
|
+
// Insert token into HTML
|
248
|
+
if (this.$input.hasClass('tt-input')) {
|
249
|
+
this.$input.parent().before( token )
|
250
|
+
} else {
|
251
|
+
this.$input.before( token )
|
252
|
+
}
|
253
|
+
this.$input.css('width', this.options.minWidth + 'px')
|
254
|
+
|
255
|
+
var tokenLabel = token.find('.token-label')
|
256
|
+
, closeButton = token.find('.close')
|
257
|
+
|
258
|
+
// Determine maximum possible token label width
|
259
|
+
if (!this.maxTokenWidth) {
|
260
|
+
this.maxTokenWidth =
|
261
|
+
this.$wrapper.width() - closeButton.outerWidth() -
|
262
|
+
parseInt(closeButton.css('margin-left'), 10) -
|
263
|
+
parseInt(closeButton.css('margin-right'), 10) -
|
264
|
+
parseInt(token.css('border-left-width'), 10) -
|
265
|
+
parseInt(token.css('border-right-width'), 10) -
|
266
|
+
parseInt(token.css('padding-left'), 10) -
|
267
|
+
parseInt(token.css('padding-right'), 10)
|
268
|
+
parseInt(tokenLabel.css('border-left-width'), 10) -
|
269
|
+
parseInt(tokenLabel.css('border-right-width'), 10) -
|
270
|
+
parseInt(tokenLabel.css('padding-left'), 10) -
|
271
|
+
parseInt(tokenLabel.css('padding-right'), 10)
|
272
|
+
parseInt(tokenLabel.css('margin-left'), 10) -
|
273
|
+
parseInt(tokenLabel.css('margin-right'), 10)
|
274
|
+
}
|
275
|
+
|
276
|
+
tokenLabel
|
277
|
+
.text(label)
|
278
|
+
.css('max-width', this.maxTokenWidth)
|
279
|
+
|
280
|
+
// Listen to events
|
281
|
+
token
|
282
|
+
.on('mousedown', function (e) {
|
283
|
+
if (_self.disabled) return false;
|
284
|
+
_self.preventDeactivation = true
|
285
|
+
})
|
286
|
+
.on('click', function (e) {
|
287
|
+
if (_self.disabled) return false;
|
288
|
+
_self.preventDeactivation = false
|
289
|
+
|
290
|
+
if (e.ctrlKey || e.metaKey) {
|
291
|
+
e.preventDefault()
|
292
|
+
return _self.toggle( token )
|
293
|
+
}
|
294
|
+
|
295
|
+
_self.activate( token, e.shiftKey, e.shiftKey )
|
296
|
+
})
|
297
|
+
.on('dblclick', function (e) {
|
298
|
+
if (_self.disabled || !_self.options.allowEditing ) return false;
|
299
|
+
_self.edit( token )
|
300
|
+
})
|
301
|
+
|
302
|
+
closeButton
|
303
|
+
.on('click', $.proxy(this.remove, this))
|
304
|
+
|
305
|
+
var createEvent = $.Event('tokenfield:createtoken')
|
306
|
+
createEvent.token = prepareEvent.token
|
307
|
+
createEvent.relatedTarget = token.get(0)
|
308
|
+
this.$element.trigger( createEvent )
|
309
|
+
|
310
|
+
var changeEvent = $.Event('change')
|
311
|
+
changeEvent.initiator = 'tokenfield'
|
312
|
+
if (triggerChange) {
|
313
|
+
this.$element.val( this.getTokensList() ).trigger( changeEvent )
|
314
|
+
}
|
315
|
+
this.update()
|
316
|
+
|
317
|
+
return this.$input.get(0)
|
318
|
+
}
|
319
|
+
|
320
|
+
, setTokens: function (tokens, add, triggerChange) {
|
321
|
+
if (!tokens) return
|
322
|
+
|
323
|
+
if (!add) this.$wrapper.find('.token').remove()
|
324
|
+
|
325
|
+
if (typeof triggerChange === 'undefined') {
|
326
|
+
triggerChange = true
|
327
|
+
}
|
328
|
+
|
329
|
+
if (typeof tokens === 'string') {
|
330
|
+
if (this._delimiters.length) {
|
331
|
+
// Split based on delimiters
|
332
|
+
tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
|
333
|
+
} else {
|
334
|
+
tokens = [tokens];
|
335
|
+
}
|
336
|
+
}
|
337
|
+
|
338
|
+
var _self = this
|
339
|
+
$.each(tokens, function (i, token) {
|
340
|
+
_self.createToken(token, triggerChange)
|
341
|
+
})
|
342
|
+
|
343
|
+
return this.$element.get(0)
|
344
|
+
}
|
345
|
+
|
346
|
+
, getTokenData: function(token) {
|
347
|
+
var data = token.map(function() {
|
348
|
+
var $token = $(this);
|
349
|
+
return {
|
350
|
+
value: $token.attr('data-value'),
|
351
|
+
label: $token.find('.token-label').text()
|
352
|
+
}
|
353
|
+
}).get();
|
354
|
+
|
355
|
+
if (data.length == 1) {
|
356
|
+
data = data[0];
|
357
|
+
}
|
358
|
+
|
359
|
+
return data;
|
360
|
+
}
|
361
|
+
|
362
|
+
, getTokens: function(active) {
|
363
|
+
var self = this
|
364
|
+
, tokens = []
|
365
|
+
, activeClass = active ? '.active' : '' // get active tokens only
|
366
|
+
this.$wrapper.find( '.token' + activeClass ).each( function() {
|
367
|
+
tokens.push( self.getTokenData( $(this) ) )
|
368
|
+
})
|
369
|
+
return tokens
|
370
|
+
}
|
371
|
+
|
372
|
+
, getTokensList: function(delimiter, beautify, active) {
|
373
|
+
delimiter = delimiter || this._firstDelimiter
|
374
|
+
beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
|
375
|
+
|
376
|
+
var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
|
377
|
+
return $.map( this.getTokens(active), function (token) {
|
378
|
+
return token.value
|
379
|
+
}).join(separator)
|
380
|
+
}
|
381
|
+
|
382
|
+
, getInput: function() {
|
383
|
+
return this.$input.val()
|
384
|
+
}
|
385
|
+
|
386
|
+
, listen: function () {
|
387
|
+
var _self = this
|
388
|
+
|
389
|
+
this.$element
|
390
|
+
.on('change', $.proxy(this.change, this))
|
391
|
+
|
392
|
+
this.$wrapper
|
393
|
+
.on('mousedown',$.proxy(this.focusInput, this))
|
394
|
+
|
395
|
+
this.$input
|
396
|
+
.on('focus', $.proxy(this.focus, this))
|
397
|
+
.on('blur', $.proxy(this.blur, this))
|
398
|
+
.on('paste', $.proxy(this.paste, this))
|
399
|
+
.on('keydown', $.proxy(this.keydown, this))
|
400
|
+
.on('keypress', $.proxy(this.keypress, this))
|
401
|
+
.on('keyup', $.proxy(this.keyup, this))
|
402
|
+
|
403
|
+
this.$copyHelper
|
404
|
+
.on('focus', $.proxy(this.focus, this))
|
405
|
+
.on('blur', $.proxy(this.blur, this))
|
406
|
+
.on('keydown', $.proxy(this.keydown, this))
|
407
|
+
.on('keyup', $.proxy(this.keyup, this))
|
408
|
+
|
409
|
+
// Secondary listeners for input width calculation
|
410
|
+
this.$input
|
411
|
+
.on('keypress', $.proxy(this.update, this))
|
412
|
+
.on('keyup', $.proxy(this.update, this))
|
413
|
+
|
414
|
+
this.$input
|
415
|
+
.on('autocompletecreate', function() {
|
416
|
+
// Set minimum autocomplete menu width
|
417
|
+
var $_menuElement = $(this).data('ui-autocomplete').menu.element
|
418
|
+
|
419
|
+
var minWidth = _self.$wrapper.outerWidth() -
|
420
|
+
parseInt( $_menuElement.css('border-left-width'), 10 ) -
|
421
|
+
parseInt( $_menuElement.css('border-right-width'), 10 )
|
422
|
+
|
423
|
+
$_menuElement.css( 'min-width', minWidth + 'px' )
|
424
|
+
})
|
425
|
+
.on('autocompleteselect', function (e, ui) {
|
426
|
+
if (_self.createToken( ui.item )) {
|
427
|
+
_self.$input.val('')
|
428
|
+
if (_self.$input.data( 'edit' )) {
|
429
|
+
_self.unedit(true)
|
430
|
+
}
|
431
|
+
}
|
432
|
+
return false
|
433
|
+
})
|
434
|
+
.on('typeahead:selected', function (e, datum, dataset) {
|
435
|
+
// Create token
|
436
|
+
if (_self.createToken( datum )) {
|
437
|
+
_self.$input.typeahead('val', '')
|
438
|
+
if (_self.$input.data( 'edit' )) {
|
439
|
+
_self.unedit(true)
|
440
|
+
}
|
441
|
+
}
|
442
|
+
})
|
443
|
+
.on('typeahead:autocompleted', function (e, datum, dataset) {
|
444
|
+
_self.createToken( _self.$input.val() )
|
445
|
+
_self.$input.typeahead('val', '')
|
446
|
+
if (_self.$input.data( 'edit' )) {
|
447
|
+
_self.unedit(true)
|
448
|
+
}
|
449
|
+
})
|
450
|
+
|
451
|
+
// Listen to window resize
|
452
|
+
$(window).on('resize', $.proxy(this.update, this ))
|
453
|
+
|
454
|
+
}
|
455
|
+
|
456
|
+
, keydown: function (e) {
|
457
|
+
|
458
|
+
if (!this.focused) return
|
459
|
+
|
460
|
+
var _self = this
|
461
|
+
|
462
|
+
switch(e.keyCode) {
|
463
|
+
case 8: // backspace
|
464
|
+
if (!this.$input.is(document.activeElement)) break
|
465
|
+
this.lastInputValue = this.$input.val()
|
466
|
+
break
|
467
|
+
|
468
|
+
case 37: // left arrow
|
469
|
+
leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
|
470
|
+
break
|
471
|
+
|
472
|
+
case 38: // up arrow
|
473
|
+
upDown('prev')
|
474
|
+
break
|
475
|
+
|
476
|
+
case 39: // right arrow
|
477
|
+
leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
|
478
|
+
break
|
479
|
+
|
480
|
+
case 40: // down arrow
|
481
|
+
upDown('next')
|
482
|
+
break
|
483
|
+
|
484
|
+
case 65: // a (to handle ctrl + a)
|
485
|
+
if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
|
486
|
+
this.activateAll()
|
487
|
+
e.preventDefault()
|
488
|
+
break
|
489
|
+
|
490
|
+
case 9: // tab
|
491
|
+
case 13: // enter
|
492
|
+
|
493
|
+
// We will handle creating tokens from autocomplete in autocomplete events
|
494
|
+
if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus)").length) break
|
495
|
+
|
496
|
+
// We will handle creating tokens from typeahead in typeahead events
|
497
|
+
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
|
498
|
+
if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val().length) break
|
499
|
+
|
500
|
+
// Create token
|
501
|
+
if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
|
502
|
+
return this.createTokensFromInput(e, this.$input.data('edit'));
|
503
|
+
}
|
504
|
+
|
505
|
+
// Edit token
|
506
|
+
if (e.keyCode === 13) {
|
507
|
+
if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
|
508
|
+
if (!_self.options.allowEditing) break
|
509
|
+
this.edit( this.$wrapper.find('.token.active') )
|
510
|
+
}
|
511
|
+
}
|
512
|
+
|
513
|
+
function leftRight(direction) {
|
514
|
+
if (_self.$input.is(document.activeElement)) {
|
515
|
+
if (_self.$input.val().length > 0) return
|
516
|
+
|
517
|
+
direction += 'All'
|
518
|
+
var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
|
519
|
+
if (!token.length) return
|
520
|
+
|
521
|
+
_self.preventInputFocus = true
|
522
|
+
_self.preventDeactivation = true
|
523
|
+
|
524
|
+
_self.activate( token )
|
525
|
+
e.preventDefault()
|
526
|
+
|
527
|
+
} else {
|
528
|
+
_self[direction]( e.shiftKey )
|
529
|
+
e.preventDefault()
|
530
|
+
}
|
531
|
+
}
|
532
|
+
|
533
|
+
function upDown(direction) {
|
534
|
+
if (!e.shiftKey) return
|
535
|
+
|
536
|
+
if (_self.$input.is(document.activeElement)) {
|
537
|
+
if (_self.$input.val().length > 0) return
|
538
|
+
|
539
|
+
var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
|
540
|
+
if (!token.length) return
|
541
|
+
|
542
|
+
_self.activate( token )
|
543
|
+
}
|
544
|
+
|
545
|
+
var opposite = direction === 'prev' ? 'next' : 'prev'
|
546
|
+
, position = direction === 'prev' ? 'first' : 'last'
|
547
|
+
|
548
|
+
_self.firstActiveToken[opposite + 'All']('.token').each(function() {
|
549
|
+
_self.deactivate( $(this) )
|
550
|
+
})
|
551
|
+
|
552
|
+
_self.activate( _self.$wrapper.find('.token:' + position), true, true )
|
553
|
+
e.preventDefault()
|
554
|
+
}
|
555
|
+
|
556
|
+
this.lastKeyDown = e.keyCode
|
557
|
+
}
|
558
|
+
|
559
|
+
, keypress: function(e) {
|
560
|
+
this.lastKeyPressCode = e.keyCode
|
561
|
+
this.lastKeyPressCharCode = e.charCode
|
562
|
+
|
563
|
+
// Comma
|
564
|
+
if ($.inArray( e.charCode, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
|
565
|
+
if (this.$input.val()) {
|
566
|
+
this.createTokensFromInput(e)
|
567
|
+
}
|
568
|
+
return false;
|
569
|
+
}
|
570
|
+
}
|
571
|
+
|
572
|
+
, keyup: function (e) {
|
573
|
+
this.preventInputFocus = false
|
574
|
+
|
575
|
+
if (!this.focused) return
|
576
|
+
|
577
|
+
switch(e.keyCode) {
|
578
|
+
case 8: // backspace
|
579
|
+
if (this.$input.is(document.activeElement)) {
|
580
|
+
if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
|
581
|
+
|
582
|
+
this.preventDeactivation = true
|
583
|
+
var prev = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
|
584
|
+
|
585
|
+
if (!prev.length) break
|
586
|
+
|
587
|
+
this.activate( prev )
|
588
|
+
} else {
|
589
|
+
this.remove(e)
|
590
|
+
}
|
591
|
+
break
|
592
|
+
|
593
|
+
case 46: // delete
|
594
|
+
this.remove(e, 'next')
|
595
|
+
break
|
596
|
+
}
|
597
|
+
this.lastKeyUp = e.keyCode
|
598
|
+
}
|
599
|
+
|
600
|
+
, focus: function (e) {
|
601
|
+
this.focused = true
|
602
|
+
this.$wrapper.addClass('focus')
|
603
|
+
|
604
|
+
if (this.$input.is(document.activeElement)) {
|
605
|
+
this.$wrapper.find('.active').removeClass('active')
|
606
|
+
this.firstActiveToken = null
|
607
|
+
|
608
|
+
if (this.options.showAutocompleteOnFocus) {
|
609
|
+
this.search()
|
610
|
+
}
|
611
|
+
}
|
612
|
+
}
|
613
|
+
|
614
|
+
, blur: function (e) {
|
615
|
+
|
616
|
+
this.focused = false
|
617
|
+
this.$wrapper.removeClass('focus')
|
618
|
+
|
619
|
+
if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
|
620
|
+
this.$wrapper.find('.active').removeClass('active')
|
621
|
+
this.firstActiveToken = null
|
622
|
+
}
|
623
|
+
|
624
|
+
if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
|
625
|
+
this.createTokensFromInput(e)
|
626
|
+
}
|
627
|
+
|
628
|
+
this.preventDeactivation = false
|
629
|
+
this.preventCreateTokens = false
|
630
|
+
}
|
631
|
+
|
632
|
+
, paste: function (e) {
|
633
|
+
var _self = this
|
634
|
+
|
635
|
+
// Add tokens to existing ones
|
636
|
+
setTimeout(function () {
|
637
|
+
_self.createTokensFromInput(e)
|
638
|
+
}, 1)
|
639
|
+
}
|
640
|
+
|
641
|
+
, change: function (e) {
|
642
|
+
if ( e.initiator === 'tokenfield' ) return // Prevent loops
|
643
|
+
|
644
|
+
this.setTokens( this.$element.val() )
|
645
|
+
}
|
646
|
+
|
647
|
+
, createTokensFromInput: function (e, focus) {
|
648
|
+
if (this.$input.val().length < this.options.minLength)
|
649
|
+
return // No input, simply return
|
650
|
+
|
651
|
+
var tokensBefore = this.getTokensList()
|
652
|
+
this.setTokens( this.$input.val(), true )
|
653
|
+
|
654
|
+
if (tokensBefore == this.getTokensList() && this.$input.val().length)
|
655
|
+
return false // No tokens were added, do nothing (prevent form submit)
|
656
|
+
|
657
|
+
if (this.$input.hasClass('tt-input')) {
|
658
|
+
// Typeahead acts weird when simply setting input value to empty,
|
659
|
+
// so we set the query to empty instead
|
660
|
+
this.$input.typeahead('val', '')
|
661
|
+
} else {
|
662
|
+
this.$input.val('')
|
663
|
+
}
|
664
|
+
|
665
|
+
if (this.$input.data( 'edit' )) {
|
666
|
+
this.unedit(focus)
|
667
|
+
}
|
668
|
+
|
669
|
+
return false // Prevent form being submitted
|
670
|
+
}
|
671
|
+
|
672
|
+
, next: function (add) {
|
673
|
+
if (add) {
|
674
|
+
var firstActive = this.$wrapper.find('.active:first')
|
675
|
+
, deactivate = firstActive && this.firstActiveToken ? firstActive.index() < this.firstActiveToken.index() : false
|
676
|
+
|
677
|
+
if (deactivate) return this.deactivate( firstActive )
|
678
|
+
}
|
679
|
+
|
680
|
+
var active = this.$wrapper.find('.active:last')
|
681
|
+
, next = active.nextAll('.token:first')
|
682
|
+
|
683
|
+
if (!next.length) {
|
684
|
+
this.$input.focus()
|
685
|
+
return
|
686
|
+
}
|
687
|
+
|
688
|
+
this.activate(next, add)
|
689
|
+
}
|
690
|
+
|
691
|
+
, prev: function (add) {
|
692
|
+
|
693
|
+
if (add) {
|
694
|
+
var lastActive = this.$wrapper.find('.active:last')
|
695
|
+
, deactivate = lastActive && this.firstActiveToken ? lastActive.index() > this.firstActiveToken.index() : false
|
696
|
+
|
697
|
+
if (deactivate) return this.deactivate( lastActive )
|
698
|
+
}
|
699
|
+
|
700
|
+
var active = this.$wrapper.find('.active:first')
|
701
|
+
, prev = active.prevAll('.token:first')
|
702
|
+
|
703
|
+
if (!prev.length) {
|
704
|
+
prev = this.$wrapper.find('.token:first')
|
705
|
+
}
|
706
|
+
|
707
|
+
if (!prev.length && !add) {
|
708
|
+
this.$input.focus()
|
709
|
+
return
|
710
|
+
}
|
711
|
+
|
712
|
+
this.activate( prev, add )
|
713
|
+
}
|
714
|
+
|
715
|
+
, activate: function (token, add, multi, remember) {
|
716
|
+
|
717
|
+
if (!token) return
|
718
|
+
|
719
|
+
if (this.$wrapper.find('.token.active').length === this.$wrapper.find('.token').length) return
|
720
|
+
|
721
|
+
if (typeof remember === 'undefined') var remember = true
|
722
|
+
|
723
|
+
if (multi) var add = true
|
724
|
+
|
725
|
+
this.$copyHelper.focus()
|
726
|
+
|
727
|
+
if (!add) {
|
728
|
+
this.$wrapper.find('.active').removeClass('active')
|
729
|
+
if (remember) {
|
730
|
+
this.firstActiveToken = token
|
731
|
+
} else {
|
732
|
+
delete this.firstActiveToken
|
733
|
+
}
|
734
|
+
}
|
735
|
+
|
736
|
+
if (multi && this.firstActiveToken) {
|
737
|
+
// Determine first active token and the current tokens indicies
|
738
|
+
// Account for the 1 hidden textarea by subtracting 1 from both
|
739
|
+
var i = this.firstActiveToken.index() - 2
|
740
|
+
, a = token.index() - 2
|
741
|
+
, _self = this
|
742
|
+
|
743
|
+
this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
|
744
|
+
_self.activate( $(this), true )
|
745
|
+
})
|
746
|
+
}
|
747
|
+
|
748
|
+
token.addClass('active')
|
749
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
750
|
+
}
|
751
|
+
|
752
|
+
, activateAll: function() {
|
753
|
+
var _self = this
|
754
|
+
|
755
|
+
this.$wrapper.find('.token').each( function (i) {
|
756
|
+
_self.activate($(this), i !== 0, false, false)
|
757
|
+
})
|
758
|
+
}
|
759
|
+
|
760
|
+
, deactivate: function(token) {
|
761
|
+
if (!token) return
|
762
|
+
|
763
|
+
token.removeClass('active')
|
764
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
765
|
+
}
|
766
|
+
|
767
|
+
, toggle: function(token) {
|
768
|
+
if (!token) return
|
769
|
+
|
770
|
+
token.toggleClass('active')
|
771
|
+
this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
|
772
|
+
}
|
773
|
+
|
774
|
+
, edit: function (token) {
|
775
|
+
if (!token) return
|
776
|
+
|
777
|
+
var value = token.data('value')
|
778
|
+
, label = token.find('.token-label').text()
|
779
|
+
|
780
|
+
// Allow changing input value before editing
|
781
|
+
var editEvent = $.Event('tokenfield:edittoken')
|
782
|
+
editEvent.token = {
|
783
|
+
value: value,
|
784
|
+
label: label
|
785
|
+
}
|
786
|
+
editEvent.relatedTarget = token.get(0)
|
787
|
+
this.$element.trigger( editEvent )
|
788
|
+
|
789
|
+
if (!editEvent.token) return
|
790
|
+
|
791
|
+
value = editEvent.token.value
|
792
|
+
label = editEvent.token.label
|
793
|
+
|
794
|
+
token.find('.token-label').text(value)
|
795
|
+
var tokenWidth = token.outerWidth()
|
796
|
+
|
797
|
+
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
|
798
|
+
|
799
|
+
token.replaceWith( $_input )
|
800
|
+
|
801
|
+
this.preventCreateTokens = true
|
802
|
+
|
803
|
+
this.$input.val( value )
|
804
|
+
.select()
|
805
|
+
.data( 'edit', true )
|
806
|
+
.width( tokenWidth )
|
807
|
+
|
808
|
+
this.update();
|
809
|
+
}
|
810
|
+
|
811
|
+
, unedit: function (focus) {
|
812
|
+
var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
|
813
|
+
$_input.appendTo( this.$wrapper )
|
814
|
+
|
815
|
+
this.$input.data('edit', false)
|
816
|
+
this.$mirror.text('')
|
817
|
+
|
818
|
+
this.update()
|
819
|
+
|
820
|
+
// Because moving the input element around in DOM
|
821
|
+
// will cause it to lose focus, we provide an option
|
822
|
+
// to re-focus the input after appending it to the wrapper
|
823
|
+
if (focus) {
|
824
|
+
var _self = this
|
825
|
+
setTimeout(function () {
|
826
|
+
_self.$input.focus()
|
827
|
+
}, 1)
|
828
|
+
}
|
829
|
+
}
|
830
|
+
|
831
|
+
, remove: function (e, direction) {
|
832
|
+
if (this.$input.is(document.activeElement) || this.disabled) return
|
833
|
+
|
834
|
+
var token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
|
835
|
+
|
836
|
+
if (e.type !== 'click') {
|
837
|
+
if (!direction) var direction = 'prev'
|
838
|
+
this[direction]()
|
839
|
+
|
840
|
+
// Was this the first token?
|
841
|
+
if (direction === 'prev') var firstToken = token.first().prevAll('.token:first').length === 0
|
842
|
+
}
|
843
|
+
|
844
|
+
// Prepare events
|
845
|
+
|
846
|
+
var removeEvent = $.Event('tokenfield:removetoken')
|
847
|
+
removeEvent.token = this.getTokenData( token )
|
848
|
+
|
849
|
+
var changeEvent = $.Event('change')
|
850
|
+
changeEvent.initiator = 'tokenfield'
|
851
|
+
|
852
|
+
// Remove token from DOM
|
853
|
+
token.remove()
|
854
|
+
|
855
|
+
// Trigger events
|
856
|
+
this.$element.val( this.getTokensList() ).trigger( removeEvent ).trigger( changeEvent )
|
857
|
+
|
858
|
+
// Focus, when necessary:
|
859
|
+
// When there are no more tokens, or if this was the first token
|
860
|
+
// and it was removed with backspace or it was clicked on
|
861
|
+
if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
|
862
|
+
|
863
|
+
// Adjust input width
|
864
|
+
this.$input.css('width', this.options.minWidth + 'px')
|
865
|
+
this.update()
|
866
|
+
|
867
|
+
e.preventDefault()
|
868
|
+
e.stopPropagation()
|
869
|
+
}
|
870
|
+
|
871
|
+
, update: function (e) {
|
872
|
+
var value = this.$input.val()
|
873
|
+
, inputLeftPadding = parseInt(this.$input.css('padding-left'), 10)
|
874
|
+
, inputRightPadding = parseInt(this.$input.css('padding-right'), 10)
|
875
|
+
, inputPadding = inputLeftPadding + inputRightPadding
|
876
|
+
|
877
|
+
if (this.$input.data('edit')) {
|
878
|
+
|
879
|
+
if (!value) {
|
880
|
+
value = this.$input.prop("placeholder")
|
881
|
+
}
|
882
|
+
if (value === this.$mirror.text()) return
|
883
|
+
|
884
|
+
this.$mirror.text(value)
|
885
|
+
|
886
|
+
var mirrorWidth = this.$mirror.width() + 10;
|
887
|
+
if ( mirrorWidth > this.$wrapper.width() ) {
|
888
|
+
return this.$input.width( this.$wrapper.width() )
|
889
|
+
}
|
890
|
+
|
891
|
+
this.$input.width( mirrorWidth )
|
892
|
+
}
|
893
|
+
else {
|
894
|
+
this.$input.css( 'width', this.options.minWidth + 'px' )
|
895
|
+
if (this.textDirection === 'rtl') {
|
896
|
+
return this.$input.width( this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 )
|
897
|
+
}
|
898
|
+
this.$input.width( this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding )
|
899
|
+
}
|
900
|
+
}
|
901
|
+
|
902
|
+
, focusInput: function (e) {
|
903
|
+
if ($(e.target).closest('.token').length || $(e.target).closest('.token-input').length) return
|
904
|
+
// Focus only after the current call stack has cleared,
|
905
|
+
// otherwise has no effect.
|
906
|
+
// Reason: mousedown is too early - input will lose focus
|
907
|
+
// after mousedown. However, since the input may be moved
|
908
|
+
// in DOM, there may be no click or mouseup event triggered.
|
909
|
+
var _self = this
|
910
|
+
setTimeout(function() {
|
911
|
+
_self.$input.focus()
|
912
|
+
}, 0)
|
913
|
+
}
|
914
|
+
|
915
|
+
, search: function () {
|
916
|
+
if ( this.$input.data('ui-autocomplete') ) {
|
917
|
+
this.$input.autocomplete('search')
|
918
|
+
}
|
919
|
+
}
|
920
|
+
|
921
|
+
, disable: function () {
|
922
|
+
this.disabled = true;
|
923
|
+
this.$input.prop('disabled', true);
|
924
|
+
this.$element.prop('disabled', true);
|
925
|
+
this.$wrapper.addClass('disabled');
|
926
|
+
}
|
927
|
+
|
928
|
+
, enable: function () {
|
929
|
+
this.disabled = false;
|
930
|
+
this.$input.prop('disabled', false);
|
931
|
+
this.$element.prop('disabled', false);
|
932
|
+
this.$wrapper.removeClass('disabled');
|
933
|
+
}
|
934
|
+
|
935
|
+
, destroy: function() {
|
936
|
+
// Set field value
|
937
|
+
this.$element.val( this.getTokensList() );
|
938
|
+
// Restore styles and properties
|
939
|
+
this.$element.css( this.$element.data('original-styles') );
|
940
|
+
this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
|
941
|
+
|
942
|
+
// Re-route tokenfield labele to original input
|
943
|
+
var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
|
944
|
+
if ( $label.length ) {
|
945
|
+
$label.prop( 'for', this.$element.prop('id') )
|
946
|
+
}
|
947
|
+
|
948
|
+
// Move original element outside of tokenfield wrapper
|
949
|
+
this.$element.insertBefore( this.$wrapper );
|
950
|
+
|
951
|
+
// Remove tokenfield-related data
|
952
|
+
this.$element.removeData('original-styles');
|
953
|
+
this.$element.removeData('original-tabindex');
|
954
|
+
this.$element.removeData('bs.tokenfield');
|
955
|
+
|
956
|
+
// Remove tokenfield from DOM
|
957
|
+
this.$wrapper.remove();
|
958
|
+
|
959
|
+
var $_element = this.$element;
|
960
|
+
delete this;
|
961
|
+
|
962
|
+
return $_element;
|
963
|
+
}
|
964
|
+
|
965
|
+
}
|
966
|
+
|
967
|
+
|
968
|
+
/* TOKENFIELD PLUGIN DEFINITION
|
969
|
+
* ======================== */
|
970
|
+
|
971
|
+
var old = $.fn.tokenfield
|
972
|
+
|
973
|
+
$.fn.tokenfield = function (option, param) {
|
974
|
+
var value
|
975
|
+
, args = []
|
976
|
+
|
977
|
+
Array.prototype.push.apply( args, arguments );
|
978
|
+
|
979
|
+
var elements = this.each(function () {
|
980
|
+
var $this = $(this)
|
981
|
+
, data = $this.data('bs.tokenfield')
|
982
|
+
, options = typeof option == 'object' && option
|
983
|
+
|
984
|
+
if (typeof option === 'string' && data && data[option]) {
|
985
|
+
args.shift()
|
986
|
+
value = data[option].apply(data, args)
|
987
|
+
} else {
|
988
|
+
if (!data && typeof option !== 'string' && !param) $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
|
989
|
+
}
|
990
|
+
})
|
991
|
+
|
992
|
+
return typeof value !== 'undefined' ? value : elements;
|
993
|
+
}
|
994
|
+
|
995
|
+
$.fn.tokenfield.defaults = {
|
996
|
+
minWidth: 60,
|
997
|
+
minLength: 0,
|
998
|
+
allowDuplicates: false,
|
999
|
+
allowEditing: true,
|
1000
|
+
limit: 0,
|
1001
|
+
autocomplete: {},
|
1002
|
+
typeahead: {},
|
1003
|
+
showAutocompleteOnFocus: false,
|
1004
|
+
createTokensOnBlur: false,
|
1005
|
+
delimiter: ',',
|
1006
|
+
beautify: true
|
1007
|
+
}
|
1008
|
+
|
1009
|
+
$.fn.tokenfield.Constructor = Tokenfield
|
1010
|
+
|
1011
|
+
|
1012
|
+
/* TOKENFIELD NO CONFLICT
|
1013
|
+
* ================== */
|
1014
|
+
|
1015
|
+
$.fn.tokenfield.noConflict = function () {
|
1016
|
+
$.fn.tokenfield = old
|
1017
|
+
return this
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
return Tokenfield;
|
1021
|
+
|
1022
|
+
}));
|