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