Browse Source

Moved to new tag lib.

Martin Edenhofer 11 years ago
parent
commit
591fab6d79

+ 3 - 29
app/assets/javascripts/app/controllers/_application_controller_form.js.coffee

@@ -389,35 +389,9 @@ class App.ControllerForm extends App.Controller
     else if attribute.tag is 'tag'
       item = $( App.view('generic/input')( attribute: attribute ) )
       a = =>
-        siteUpdate = (reorder) =>
-          container = document.getElementById( attribute.id + "_tagsinput" )
-          if reorder
-            $('#' + attribute.id + "_tagsinput" ).height( 20 )
-          height = container.scrollHeight || 45
-          console.log('siteUpdate', height)
-          $('#' + attribute.id + "_tagsinput" ).height( height - 16 )
-
-        onAddTag = =>
-          siteUpdate()
-
-        onRemoveTag = =>
-          siteUpdate(true)
-
-        $('#' + attribute.id + '_tagsinput').remove()
-        h = $('#' + attribute.id).height()
-        $('#' + attribute.id).tagsInput(
-          width: '100%'
-#          height: (h + 30 )+ 'px'
-          onAddTag:    onAddTag
-          onRemoveTag: onRemoveTag
-        )
-        siteUpdate(true)
-
-        # update box size
-        App.Event.bind 'ui:rerender:content', =>
-          siteUpdate(true)
-
-      App.Delay.set( a, 80, undefined, 'form_tags' )
+        $('#' + attribute.id ).tokenfield()
+        $('#' + attribute.id ).parent().css('height', 'auto')
+      App.Delay.set( a, 120, undefined, 'tags' )
 
     # autocompletion
     else if attribute.tag is 'autocompletion'

+ 9 - 21
app/assets/javascripts/app/controllers/widget/tag.js.coffee

@@ -30,16 +30,16 @@ class App.WidgetTag extends App.Controller
       tags: tags || [],
       tag_id: @attribute_id
     )
-    @el.find('#' + @attribute_id ).tagsInput(
-      width:       '100%'
-      defaultText: App.i18n.translateContent('add a Tag')
-      onAddTag:    @onAddTag
-      onRemoveTag: @onRemoveTag
-      height:      '45px'
+    @el.find('#' + @attribute_id ).tokenfield().on(
+      'tokenfield:createtoken'
+      (e) =>
+        @onAddTag( e.token.value )
+    ).on(
+      'tokenfield:removetoken'
+      (e) =>
+        @onRemoveTag( e.token.value )
     )
-    @delay @siteUpdate, 250
-
-#    @el.find('#tags').elastic()
+    @el.find('#' + @attribute_id ).parent().css('height', 'auto')
 
   onAddTag: (item) =>
     @ajax(
@@ -50,8 +50,6 @@ class App.WidgetTag extends App.Controller
         o_id:   @object.id,
         item:   item
       processData: true,
-      success: (data, status, xhr) =>
-        @siteUpdate()
     )
 
   onRemoveTag: (item) =>
@@ -63,14 +61,4 @@ class App.WidgetTag extends App.Controller
         o_id:   @object.id
         item:   item
       processData: true
-      success: (data, status, xhr) =>
-        @siteUpdate(true)
     )
-
-  siteUpdate: (reorder) =>
-    container = document.getElementById( @attribute_id + '_tagsinput' )
-    if reorder
-      $('#' + @attribute_id + '_tagsinput').height( 20 )
-    return if !container
-    height = container.scrollHeight
-    $('#' + @attribute_id + '_tagsinput').height( height - 10 )

+ 1020 - 0
app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js

@@ -0,0 +1,1020 @@
+/*!
+ * bootstrap-tokenfield
+ * https://github.com/sliptree/bootstrap-tokenfield
+ * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
+ */
+
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as an anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof exports === 'object') {
+    // For CommonJS and CommonJS-like environments where a window with jQuery
+    // is present, execute the factory with the jQuery instance from the window object
+    // For environments that do not inherently posses a window with a document
+    // (such as Node.js), expose a Tokenfield-making factory as module.exports
+    // This accentuates the need for the creation of a real window or passing in a jQuery instance
+    // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
+    module.exports = global.window && global.window.$ ?
+      factory( global.window.$ ) :
+      function( input ) {
+        if ( !input.$ && !input.fn ) {
+          throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
+        }
+        return factory( input.$ || input );
+      };
+  } else {
+    // Browser globals
+    factory(jQuery);
+  }
+}(function ($, window) {
+
+  "use strict"; // jshint ;_;
+
+ /* TOKENFIELD PUBLIC CLASS DEFINITION
+  * ============================== */
+
+  var Tokenfield = function (element, options) {
+    var _self = this
+
+    this.$element = $(element)
+    this.textDirection = this.$element.css('direction');
+
+    // Extend options
+    this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
+    
+    // Setup delimiters and trigger keys
+    this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
+    this._triggerKeys = $.map(this._delimiters, function (delimiter) {
+      return delimiter.charCodeAt(0);
+    });
+    this._firstDelimiter = this._delimiters[0];
+
+    // Check for whitespace, dash and special characters
+    var whitespace = $.inArray(' ', this._delimiters)
+      , dash = $.inArray('-', this._delimiters)
+
+    if (whitespace >= 0)
+      this._delimiters[whitespace] = '\\s'
+
+    if (dash >= 0) {
+      delete this._delimiters[dash]
+      this._delimiters.unshift('-')
+    }
+
+    var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
+    $.each(this._delimiters, function (index, char) {
+      var pos = $.inArray(char, specialCharacters)
+      if (pos >= 0) _self._delimiters[index] = '\\' + char;
+    });
+
+    // Store original input width
+    var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
+      , elStyleWidth = element.style.width
+      , elCSSWidth
+      , elWidth = this.$element.width()
+
+    if (elRules) {
+      $.each( elRules, function (i, rule) {
+        if (rule.style.width) {
+          elCSSWidth = rule.style.width;
+        }
+      });
+    }
+
+    // Move original input out of the way
+    var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
+        originalStyles = { position: this.$element.css('position') };
+    originalStyles[hidingPosition] = this.$element.css(hidingPosition);
+    
+    this.$element
+      .data('original-styles', originalStyles)
+      .data('original-tabindex', this.$element.prop('tabindex'))
+      .css('position', 'absolute')
+      .css(hidingPosition, '-10000px')
+      .prop('tabindex', -1)
+
+    // Create a wrapper
+    this.$wrapper = $('<div class="tokenfield form-control" />')
+    if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
+    if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
+    if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
+
+    // Create a new input
+    var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
+    this.$input = $('<input type="text" class="token-input" autocomplete="off" />')
+                    .appendTo( this.$wrapper )
+                    .prop( 'placeholder',  this.$element.prop('placeholder') )
+                    .prop( 'id', id + '-tokenfield' )
+                    .prop( 'tabindex', this.$element.data('original-tabindex') )
+
+    // Re-route original input label to new input
+    var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
+    if ( $label.length ) {
+      $label.prop( 'for', this.$input.prop('id') )
+    }
+
+    // Set up a copy helper to handle copy & paste
+    this.$copyHelper = $('<input type="text" />').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
+    
+    // Set wrapper width
+    if (elStyleWidth) {
+      this.$wrapper.css('width', elStyleWidth);
+    }
+    else if (elCSSWidth) {
+      this.$wrapper.css('width', elCSSWidth);
+    }
+    // If input is inside inline-form with no width set, set fixed width
+    else if (this.$element.parents('.form-inline').length) {
+      this.$wrapper.width( elWidth )
+    }
+
+    // Set tokenfield disabled, if original or fieldset input is disabled
+    if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
+      this.disable();
+    }
+
+    // Set up mirror for input auto-sizing
+    this.$mirror = $('<span style="position:absolute; top:-999px; left:0; white-space:pre;"/>');
+    this.$input.css('min-width', this.options.minWidth + 'px')
+    $.each([
+        'fontFamily', 
+        'fontSize', 
+        'fontWeight', 
+        'fontStyle', 
+        'letterSpacing', 
+        'textTransform', 
+        'wordSpacing', 
+        'textIndent'
+    ], function (i, val) {
+        _self.$mirror[0].style[val] = _self.$input.css(val);
+    });
+    this.$mirror.appendTo( 'body' )
+
+    // Insert tokenfield to HTML
+    this.$wrapper.insertBefore( this.$element )
+    this.$element.prependTo( this.$wrapper )
+
+    // Calculate inner input width
+    this.update()
+    
+    // Create initial tokens, if any
+    this.setTokens(this.options.tokens, false, false)
+
+    // Start listening to events
+    this.listen()
+
+    // Initialize autocomplete, if necessary
+    if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
+      var side = this.textDirection === 'rtl' ? 'right' : 'left'
+      var autocompleteOptions = $.extend({
+        minLength: this.options.showAutocompleteOnFocus ? 0 : null,
+        position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
+      }, this.options.autocomplete )
+      this.$input.autocomplete( autocompleteOptions )
+    }
+
+    // Initialize typeahead, if necessary
+    if ( ! $.isEmptyObject( this.options.typeahead ) ) {
+      var typeaheadOptions = $.extend({
+        minLength: this.options.showAutocompleteOnFocus ? 0 : null
+      }, this.options.typeahead)
+      this.$input.typeahead( null, typeaheadOptions )
+      this.typeahead = true
+    }
+
+    this.$element.trigger('tokenfield:initialize')
+  }
+
+  Tokenfield.prototype = {
+
+    constructor: Tokenfield
+
+  , createToken: function (attrs, triggerChange) {
+      if (typeof attrs === 'string') {
+        attrs = { value: attrs, label: attrs }
+      }
+
+     if (typeof triggerChange === 'undefined') {
+         triggerChange = true
+     }
+      
+      var _self = this
+        , value = $.trim(attrs.value)
+        , label = attrs.label && attrs.label.length ? $.trim(attrs.label) : value
+
+      if (!value.length || !label.length || value.length < this.options.minLength) return
+
+      if (this.options.limit && this.getTokens().length >= this.options.limit) return
+
+      // Allow changing token data before creating it
+      var prepareEvent = $.Event('tokenfield:preparetoken')
+      prepareEvent.token = {
+        value: value,
+        label: label
+      }
+      this.$element.trigger( prepareEvent )
+
+      if (!prepareEvent.token) return
+
+      value = prepareEvent.token.value
+      label = prepareEvent.token.label
+
+      // Check for duplicates
+      if (!this.options.allowDuplicates && $.grep(this.getTokens(), function (token) {
+        return token.value === value
+      }).length) {
+        // Allow listening to when duplicates get prevented
+        var preventDuplicateEvent = $.Event('tokenfield:preventduplicate')
+        preventDuplicateEvent.token = {
+          value: value,
+          label: label
+        }
+        this.$element.trigger( preventDuplicateEvent )
+        // Add duplicate warning class to existing token for 250ms
+        var duplicate = this.$wrapper.find( '.token[data-value="' + value + '"]' ).addClass('duplicate')
+        setTimeout(function() {
+          duplicate.removeClass('duplicate');
+        }, 250)
+        return false
+      }
+
+      var token = $('<div class="token" />')
+            .attr('data-value', value)
+            .append('<span class="token-label" />')
+            .append('<a href="#" class="close" tabindex="-1">&times;</a>')
+
+      // Insert token into HTML
+      if (this.$input.hasClass('tt-input')) {
+        this.$input.parent().before( token )
+      } else {
+        this.$input.before( token )
+      }
+      this.$input.css('width', this.options.minWidth + 'px')
+
+      var tokenLabel = token.find('.token-label')
+        , closeButton = token.find('.close')
+
+      // Determine maximum possible token label width
+      if (!this.maxTokenWidth) {
+        this.maxTokenWidth =
+          this.$wrapper.width() - closeButton.outerWidth() - 
+          parseInt(closeButton.css('margin-left'), 10) -
+          parseInt(closeButton.css('margin-right'), 10) -
+          parseInt(token.css('border-left-width'), 10) -
+          parseInt(token.css('border-right-width'), 10) -
+          parseInt(token.css('padding-left'), 10) -
+          parseInt(token.css('padding-right'), 10)
+          parseInt(tokenLabel.css('border-left-width'), 10) -
+          parseInt(tokenLabel.css('border-right-width'), 10) -
+          parseInt(tokenLabel.css('padding-left'), 10) -
+          parseInt(tokenLabel.css('padding-right'), 10)
+          parseInt(tokenLabel.css('margin-left'), 10) -
+          parseInt(tokenLabel.css('margin-right'), 10)
+      }
+
+      tokenLabel
+        .text(label)
+        .css('max-width', this.maxTokenWidth)
+
+      // Listen to events
+      token
+        .on('mousedown',  function (e) {
+          if (_self.disabled) return false;
+          _self.preventDeactivation = true
+        })
+        .on('click',    function (e) {
+          if (_self.disabled) return false;
+          _self.preventDeactivation = false
+
+          if (e.ctrlKey || e.metaKey) {
+            e.preventDefault()
+            return _self.toggle( token )
+          }
+          
+          _self.activate( token, e.shiftKey, e.shiftKey )          
+        })
+        .on('dblclick', function (e) {
+          if (_self.disabled || !_self.options.allowEditing ) return false;
+          _self.edit( token )
+        })
+
+      closeButton
+          .on('click',  $.proxy(this.remove, this))
+
+      var createEvent = $.Event('tokenfield:createtoken')
+      createEvent.token = prepareEvent.token
+      createEvent.relatedTarget = token.get(0)
+      this.$element.trigger( createEvent )
+
+      var changeEvent = $.Event('change')
+      changeEvent.initiator = 'tokenfield'
+      if (triggerChange) {
+        this.$element.val( this.getTokensList() ).trigger( changeEvent )
+      }
+      this.update()
+
+      return this.$input.get(0)
+    }    
+
+  , setTokens: function (tokens, add, triggerChange) {
+      if (!tokens) return
+
+      if (!add) this.$wrapper.find('.token').remove()
+
+      if (typeof triggerChange === 'undefined') {
+          triggerChange = true
+      }
+
+      if (typeof tokens === 'string') {
+        if (this._delimiters.length) {
+          // Split based on delimiters
+          tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
+        } else {
+          tokens = [tokens];
+        }
+      }
+
+      var _self = this
+      $.each(tokens, function (i, token) {
+        _self.createToken(token, triggerChange)
+      })
+
+      return this.$element.get(0)
+    }
+
+  , getTokenData: function(token) {
+      var data = token.map(function() {
+        var $token = $(this);
+        return {
+          value: $token.attr('data-value'),
+          label: $token.find('.token-label').text()
+        }
+      }).get();
+
+      if (data.length == 1) {
+        data = data[0];
+      }
+
+      return data;
+    }
+
+  , getTokens: function(active) {
+      var self = this
+        , tokens = []
+        , activeClass = active ? '.active' : '' // get active tokens only
+      this.$wrapper.find( '.token' + activeClass ).each( function() {
+        tokens.push( self.getTokenData( $(this) ) )
+      })
+      return tokens
+  }
+
+  , getTokensList: function(delimiter, beautify, active) {
+      delimiter = delimiter || this._firstDelimiter
+      beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
+      
+      var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
+      return $.map( this.getTokens(active), function (token) {
+        return token.value
+      }).join(separator)
+  }
+
+  , getInput: function() {
+    return this.$input.val()
+  }
+
+  , listen: function () {
+      var _self = this
+
+      this.$element
+        .on('change',   $.proxy(this.change, this))
+
+      this.$wrapper
+        .on('mousedown',$.proxy(this.focusInput, this))
+
+      this.$input
+        .on('focus',    $.proxy(this.focus, this))
+        .on('blur',     $.proxy(this.blur, this))
+        .on('paste',    $.proxy(this.paste, this))
+        .on('keydown',  $.proxy(this.keydown, this))
+        .on('keypress', $.proxy(this.keypress, this))
+        .on('keyup',    $.proxy(this.keyup, this))
+
+      this.$copyHelper
+        .on('focus',    $.proxy(this.focus, this))
+        .on('blur',     $.proxy(this.blur, this))        
+        .on('keydown',  $.proxy(this.keydown, this))
+        .on('keyup',    $.proxy(this.keyup, this))
+
+      // Secondary listeners for input width calculation
+      this.$input
+        .on('keypress', $.proxy(this.update, this))
+        .on('keyup',    $.proxy(this.update, this))
+
+      this.$input
+        .on('autocompletecreate', function() {
+          // Set minimum autocomplete menu width
+          var $_menuElement = $(this).data('ui-autocomplete').menu.element
+          
+          var minWidth = _self.$wrapper.outerWidth() -
+              parseInt( $_menuElement.css('border-left-width'), 10 ) -
+              parseInt( $_menuElement.css('border-right-width'), 10 )
+
+          $_menuElement.css( 'min-width', minWidth + 'px' )
+        })
+        .on('autocompleteselect', function (e, ui) {
+          if (_self.createToken( ui.item )) {
+            _self.$input.val('')
+            if (_self.$input.data( 'edit' )) {
+              _self.unedit(true)
+            }
+          }
+          return false
+        })
+        .on('typeahead:selected', function (e, datum, dataset) {
+          // Create token
+          if (_self.createToken( datum )) {
+            _self.$input.typeahead('val', '')
+            if (_self.$input.data( 'edit' )) {
+              _self.unedit(true)
+            }
+          }
+        })
+        .on('typeahead:autocompleted', function (e, datum, dataset) {
+          _self.createToken( _self.$input.val() )
+          _self.$input.typeahead('val', '')
+          if (_self.$input.data( 'edit' )) {
+            _self.unedit(true)
+          }
+        })
+
+      // Listen to window resize
+      $(window).on('resize', $.proxy(this.update, this ))
+
+    }
+
+  , keydown: function (e) {
+
+      if (!this.focused) return
+
+      var _self = this
+
+      switch(e.keyCode) {
+        case 8: // backspace
+          if (!this.$input.is(document.activeElement)) break
+          this.lastInputValue = this.$input.val()
+          break
+
+        case 37: // left arrow
+          leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
+          break
+
+        case 38: // up arrow
+          upDown('prev')
+          break
+
+        case 39: // right arrow
+          leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
+          break
+
+        case 40: // down arrow
+          upDown('next')
+          break        
+
+        case 65: // a (to handle ctrl + a)
+          if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
+          this.activateAll()
+          e.preventDefault()
+          break
+
+        case 9: // tab
+        case 13: // enter     
+
+          // We will handle creating tokens from autocomplete in autocomplete events
+          if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus)").length) break
+          
+          // We will handle creating tokens from typeahead in typeahead events
+          if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
+          if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val().length) break
+          
+          // Create token
+          if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
+            return this.createTokensFromInput(e, this.$input.data('edit'));
+          }
+
+          // Edit token
+          if (e.keyCode === 13) {
+            if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
+            if (!_self.options.allowEditing) break
+            this.edit( this.$wrapper.find('.token.active') )
+          }
+      }
+
+      function leftRight(direction) {
+        if (_self.$input.is(document.activeElement)) {
+          if (_self.$input.val().length > 0) return
+
+          direction += 'All'
+          var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
+          if (!token.length) return
+
+          _self.preventInputFocus = true
+          _self.preventDeactivation = true
+
+          _self.activate( token )
+          e.preventDefault()
+
+        } else {
+          _self[direction]( e.shiftKey )
+          e.preventDefault()
+        }
+      }
+
+      function upDown(direction) {
+        if (!e.shiftKey) return
+
+        if (_self.$input.is(document.activeElement)) {
+          if (_self.$input.val().length > 0) return
+
+          var token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
+          if (!token.length) return
+
+          _self.activate( token )
+        }
+
+        var opposite = direction === 'prev' ? 'next' : 'prev'
+          , position = direction === 'prev' ? 'first' : 'last'
+
+        _self.firstActiveToken[opposite + 'All']('.token').each(function() {
+          _self.deactivate( $(this) )
+        })
+
+        _self.activate( _self.$wrapper.find('.token:' + position), true, true )
+        e.preventDefault()
+      }
+
+      this.lastKeyDown = e.keyCode
+    }
+
+  , keypress: function(e) {
+      this.lastKeyPressCode = e.keyCode
+      this.lastKeyPressCharCode = e.charCode
+
+      // Comma
+      if ($.inArray( e.charCode, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
+        if (this.$input.val()) {
+          this.createTokensFromInput(e)
+        }
+        return false;
+      }
+    }
+
+  , keyup: function (e) {
+      this.preventInputFocus = false
+
+      if (!this.focused) return
+
+      switch(e.keyCode) {
+        case 8: // backspace
+          if (this.$input.is(document.activeElement)) {
+            if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
+            
+            this.preventDeactivation = true
+            var prev = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
+
+            if (!prev.length) break
+
+            this.activate( prev )
+          } else {
+            this.remove(e)
+          }
+          break
+
+        case 46: // delete
+          this.remove(e, 'next')
+          break
+      }
+      this.lastKeyUp = e.keyCode
+    }
+
+  , focus: function (e) {
+      this.focused = true
+      this.$wrapper.addClass('focus')
+
+      if (this.$input.is(document.activeElement)) {
+        this.$wrapper.find('.active').removeClass('active')
+        this.firstActiveToken = null
+
+        if (this.options.showAutocompleteOnFocus) {
+          this.search()
+        }
+      }
+    }
+
+  , blur: function (e) {
+
+      this.focused = false
+      this.$wrapper.removeClass('focus')
+
+      if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
+        this.$wrapper.find('.active').removeClass('active')
+        this.firstActiveToken = null
+      }
+
+      if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
+        this.createTokensFromInput(e) 
+      }
+      
+      this.preventDeactivation = false
+      this.preventCreateTokens = false
+    }
+
+  , paste: function (e) {
+      var _self = this
+      
+      // Add tokens to existing ones
+      setTimeout(function () {
+        _self.createTokensFromInput(e)
+      }, 1)
+    }
+
+  , change: function (e) {
+      if ( e.initiator === 'tokenfield' ) return // Prevent loops
+      
+      this.setTokens( this.$element.val() )
+    }
+
+  , createTokensFromInput: function (e, focus) {
+      if (this.$input.val().length < this.options.minLength)
+        return // No input, simply return
+
+      var tokensBefore = this.getTokensList()
+      this.setTokens( this.$input.val(), true )
+      
+      if (tokensBefore == this.getTokensList() && this.$input.val().length)
+        return false // No tokens were added, do nothing (prevent form submit)
+
+      if (this.$input.hasClass('tt-input')) {
+        // Typeahead acts weird when simply setting input value to empty,
+        // so we set the query to empty instead
+        this.$input.typeahead('val', '')
+      } else {
+        this.$input.val('')
+      }
+
+      if (this.$input.data( 'edit' )) {
+        this.unedit(focus)
+      }
+
+      return false // Prevent form being submitted
+    }  
+
+  , next: function (add) {
+      if (add) {
+        var firstActive = this.$wrapper.find('.active:first')
+          , deactivate = firstActive && this.firstActiveToken ? firstActive.index() < this.firstActiveToken.index() : false
+
+        if (deactivate) return this.deactivate( firstActive )
+      }
+
+      var active = this.$wrapper.find('.active:last')
+        , next = active.nextAll('.token:first')
+
+      if (!next.length) {
+        this.$input.focus()
+        return
+      }
+
+      this.activate(next, add)
+    }
+
+  , prev: function (add) {
+
+      if (add) {
+        var lastActive = this.$wrapper.find('.active:last')
+          , deactivate = lastActive && this.firstActiveToken ? lastActive.index() > this.firstActiveToken.index() : false
+
+        if (deactivate) return this.deactivate( lastActive )
+      }
+
+      var active = this.$wrapper.find('.active:first')
+        , prev = active.prevAll('.token:first')
+
+      if (!prev.length) {
+        prev = this.$wrapper.find('.token:first')
+      }
+
+      if (!prev.length && !add) {
+        this.$input.focus()
+        return
+      }
+
+      this.activate( prev, add )
+    }
+
+  , activate: function (token, add, multi, remember) {
+
+      if (!token) return
+
+      if (typeof remember === 'undefined') var remember = true
+
+      if (multi) var add = true
+
+      this.$copyHelper.focus()
+
+      if (!add) {
+        this.$wrapper.find('.active').removeClass('active')
+        if (remember) {
+          this.firstActiveToken = token 
+        } else {
+          delete this.firstActiveToken
+        }
+      }
+
+      if (multi && this.firstActiveToken) {
+        // Determine first active token and the current tokens indicies
+        // Account for the 1 hidden textarea by subtracting 1 from both
+        var i = this.firstActiveToken.index() - 2
+          , a = token.index() - 2
+          , _self = this
+
+        this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
+          _self.activate( $(this), true )
+        })
+      }
+
+      token.addClass('active')
+      this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+    }
+
+  , activateAll: function() {
+      var _self = this
+
+      this.$wrapper.find('.token').each( function (i) {
+        _self.activate($(this), i !== 0, false, false)
+      })
+    }
+
+  , deactivate: function(token) {
+      if (!token) return
+
+      token.removeClass('active')
+      this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+    }
+
+  , toggle: function(token) {
+      if (!token) return
+
+      token.toggleClass('active')
+      this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+    }
+
+  , edit: function (token) {
+      if (!token) return
+
+      var value = token.data('value')
+        , label = token.find('.token-label').text()
+
+      // Allow changing input value before editing
+      var editEvent = $.Event('tokenfield:edittoken')
+      editEvent.token = {
+        value: value,
+        label: label
+      }
+      editEvent.relatedTarget = token.get(0)
+      this.$element.trigger( editEvent )
+      
+      if (!editEvent.token) return
+
+      value = editEvent.token.value
+      label = editEvent.token.label
+
+      token.find('.token-label').text(value)
+      var tokenWidth = token.outerWidth()
+
+      var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
+
+      token.replaceWith( $_input )
+
+      this.preventCreateTokens = true
+
+      this.$input.val( value )
+                .select()
+                .data( 'edit', true )
+                .width( tokenWidth )
+
+      this.update();
+    }
+
+  , unedit: function (focus) {
+      var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
+      $_input.appendTo( this.$wrapper )
+      
+      this.$input.data('edit', false)
+      this.$mirror.text('')
+
+      this.update()
+
+      // Because moving the input element around in DOM 
+      // will cause it to lose focus, we provide an option
+      // to re-focus the input after appending it to the wrapper
+      if (focus) {
+        var _self = this
+        setTimeout(function () {
+          _self.$input.focus()
+        }, 1)
+      }
+    }
+
+  , remove: function (e, direction) {
+      if (this.$input.is(document.activeElement) || this.disabled) return
+
+      var token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
+      
+      if (e.type !== 'click') {
+        if (!direction) var direction = 'prev'
+        this[direction]()
+
+        // Was this the first token?
+        if (direction === 'prev') var firstToken = token.first().prevAll('.token:first').length === 0
+      }
+
+      // Prepare events
+
+      var removeEvent = $.Event('tokenfield:removetoken')
+      removeEvent.token = this.getTokenData( token )
+
+      var changeEvent = $.Event('change')
+      changeEvent.initiator = 'tokenfield'
+
+      // Remove token from DOM
+      token.remove()
+
+      // Trigger events
+      this.$element.val( this.getTokensList() ).trigger( removeEvent ).trigger( changeEvent )
+
+      // Focus, when necessary:
+      // When there are no more tokens, or if this was the first token
+      // and it was removed with backspace or it was clicked on
+      if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
+
+      // Adjust input width
+      this.$input.css('width', this.options.minWidth + 'px')
+      this.update()
+
+      e.preventDefault()
+      e.stopPropagation()
+    }
+
+  , update: function (e) {
+      var value = this.$input.val()
+        , inputLeftPadding = parseInt(this.$input.css('padding-left'), 10)
+        , inputRightPadding = parseInt(this.$input.css('padding-right'), 10)
+        , inputPadding = inputLeftPadding + inputRightPadding
+
+      if (this.$input.data('edit')) {
+
+        if (!value) {
+          value = this.$input.prop("placeholder")
+        }
+        if (value === this.$mirror.text()) return
+
+        this.$mirror.text(value)
+        
+        var mirrorWidth = this.$mirror.width() + 10;
+        if ( mirrorWidth > this.$wrapper.width() ) {
+          return this.$input.width( this.$wrapper.width() )
+        }
+
+        this.$input.width( mirrorWidth )
+      }
+      else {
+        this.$input.css( 'width', this.options.minWidth + 'px' )
+        if (this.textDirection === 'rtl') {
+          return this.$input.width( this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1 )
+        }
+        this.$input.width( this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding )
+      }
+    }
+
+  , focusInput: function (e) {
+      if ($(e.target).closest('.token').length || $(e.target).closest('.token-input').length) return
+      // Focus only after the current call stack has cleared,
+      // otherwise has no effect.
+      // Reason: mousedown is too early - input will lose focus
+      // after mousedown. However, since the input may be moved
+      // in DOM, there may be no click or mouseup event triggered.
+      var _self = this
+      setTimeout(function() {
+        _self.$input.focus()
+      }, 0)
+    }
+
+  , search: function () {
+      if ( this.$input.data('ui-autocomplete') ) {
+        this.$input.autocomplete('search')
+      }
+    }
+
+  , disable: function () {
+      this.disabled = true;
+      this.$input.prop('disabled', true);
+      this.$element.prop('disabled', true);
+      this.$wrapper.addClass('disabled');
+    }
+
+  , enable: function () {
+      this.disabled = false;
+      this.$input.prop('disabled', false);
+      this.$element.prop('disabled', false);
+      this.$wrapper.removeClass('disabled');
+    }
+
+  , destroy: function() {
+      // Set field value
+      this.$element.val( this.getTokensList() );
+      // Restore styles and properties
+      this.$element.css( this.$element.data('original-styles') );
+      this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
+      
+      // Re-route tokenfield labele to original input
+      var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
+      if ( $label.length ) {
+        $label.prop( 'for', this.$element.prop('id') )
+      }
+
+      // Move original element outside of tokenfield wrapper
+      this.$element.insertBefore( this.$wrapper );
+
+      // Remove tokenfield-related data
+      this.$element.removeData('original-styles');
+      this.$element.removeData('original-tabindex');
+      this.$element.removeData('bs.tokenfield');
+
+      // Remove tokenfield from DOM
+      this.$wrapper.remove();
+
+      var $_element = this.$element;
+      delete this;
+
+      return $_element;
+  }
+
+  }
+
+
+ /* TOKENFIELD PLUGIN DEFINITION
+  * ======================== */
+
+  var old = $.fn.tokenfield
+
+  $.fn.tokenfield = function (option, param) {
+    var value
+      , args = []
+    
+    Array.prototype.push.apply( args, arguments );
+
+    var elements = this.each(function () {
+      var $this = $(this)
+        , data = $this.data('bs.tokenfield')
+        , options = typeof option == 'object' && option
+
+      if (typeof option === 'string' && data && data[option]) {
+        args.shift()
+        value = data[option].apply(data, args)
+      } else {
+        if (!data && typeof option !== 'string' && !param) $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
+      }
+    })
+
+    return typeof value !== 'undefined' ? value : elements;
+  }
+
+  $.fn.tokenfield.defaults = {
+    minWidth: 60,
+    minLength: 0,
+    allowDuplicates: false,
+    allowEditing: true,
+    limit: 0,
+    autocomplete: {},
+    typeahead: {},
+    showAutocompleteOnFocus: false,
+    createTokensOnBlur: false,
+    delimiter: ',',
+    beautify: true
+  }
+
+  $.fn.tokenfield.Constructor = Tokenfield
+
+
+ /* TOKENFIELD NO CONFLICT
+  * ================== */
+
+  $.fn.tokenfield.noConflict = function () {
+    $.fn.tokenfield = old
+    return this
+  }
+
+  return Tokenfield;
+
+}));

+ 0 - 353
app/assets/javascripts/app/lib/base/jquery.tagsinput.js

@@ -1,353 +0,0 @@
-/*
-
-	jQuery Tags Input Plugin 1.3.3
-	
-	Copyright (c) 2011 XOXCO, Inc
-	
-	Documentation for this plugin lives here:
-	http://xoxco.com/clickable/jquery-tags-input
-	
-	Licensed under the MIT license:
-	http://www.opensource.org/licenses/mit-license.php
-
-	ben@xoxco.com
-
-*/
-
-(function($) {
-
-	var delimiter = new Array();
-	var tags_callbacks = new Array();
-	$.fn.doAutosize = function(o){
-	    var minWidth = $(this).data('minwidth'),
-	        maxWidth = $(this).data('maxwidth'),
-	        val = '',
-	        input = $(this),
-	        testSubject = $('#'+$(this).data('tester_id'));
-	
-	    if (val === (val = input.val())) {return;}
-	
-	    // Enter new content into testSubject
-	    var escaped = val.replace(/&/g, '&amp;').replace(/\s/g,' ').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-	    testSubject.html(escaped);
-	    // Calculate new width + whether to change
-	    var testerWidth = testSubject.width(),
-	        newWidth = (testerWidth + o.comfortZone) >= minWidth ? testerWidth + o.comfortZone : minWidth,
-	        currentWidth = input.width(),
-	        isValidWidthChange = (newWidth < currentWidth && newWidth >= minWidth)
-	                             || (newWidth > minWidth && newWidth < maxWidth);
-	
-	    // Animate width
-	    if (isValidWidthChange) {
-	        input.width(newWidth);
-	    }
-
-
-  };
-  $.fn.resetAutosize = function(options){
-    // alert(JSON.stringify(options));
-    var minWidth =  $(this).data('minwidth') || options.minInputWidth || $(this).width(),
-        maxWidth = $(this).data('maxwidth') || options.maxInputWidth || ($(this).closest('.tagsinput').width() - options.inputPadding),
-        val = '',
-        input = $(this),
-        testSubject = $('<tester/>').css({
-            position: 'absolute',
-            top: -9999,
-            left: -9999,
-            width: 'auto',
-            fontSize: input.css('fontSize'),
-            fontFamily: input.css('fontFamily'),
-            fontWeight: input.css('fontWeight'),
-            letterSpacing: input.css('letterSpacing'),
-            whiteSpace: 'nowrap'
-        }),
-        testerId = $(this).attr('id')+'_autosize_tester';
-    if(! $('#'+testerId).length > 0){
-      testSubject.attr('id', testerId);
-      testSubject.appendTo('body');
-    }
-
-    input.data('minwidth', minWidth);
-    input.data('maxwidth', maxWidth);
-    input.data('tester_id', testerId);
-    input.css('width', minWidth);
-  };
-  
-	$.fn.addTag = function(value,options) {
-			options = jQuery.extend({focus:false,callback:true},options);
-			this.each(function() { 
-				var id = $(this).attr('id');
-
-				var tagslist = $(this).val().split(delimiter[id]);
-				if (tagslist[0] == '') { 
-					tagslist = new Array();
-				}
-
-				value = jQuery.trim(value);
-		
-				if (options.unique) {
-					var skipTag = $(tagslist).tagExist(value);
-					if(skipTag == true) {
-					    //Marks fake input as not_valid to let styling it
-    				    $('#'+id+'_tag').addClass('not_valid');
-    				}
-				} else {
-					var skipTag = false; 
-				}
-				
-				if (value !='' && skipTag != true) { 
-                    $('<span>').addClass('tag').append(
-                        $('<span>').text(value).append('&nbsp;&nbsp;'),
-                        $('<a>', {
-                            href  : '#',
-                            title : 'Removing tag',
-                            text  : 'x'
-                        }).click(function () {
-                            return $('#' + id).removeTag(escape(value));
-                        })
-                    ).insertBefore('#' + id + '_addTag');
-
-					tagslist.push(value);
-				
-					$('#'+id+'_tag').val('');
-					if (options.focus) {
-						$('#'+id+'_tag').focus();
-					} else {		
-						$('#'+id+'_tag').blur();
-					}
-					
-					$.fn.tagsInput.updateTagsField(this,tagslist);
-					
-					if (options.callback && tags_callbacks[id] && tags_callbacks[id]['onAddTag']) {
-						var f = tags_callbacks[id]['onAddTag'];
-						f.call(this, value);
-					}
-					if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
-					{
-						var i = tagslist.length;
-						var f = tags_callbacks[id]['onChange'];
-						f.call(this, $(this), tagslist[i-1]);
-					}					
-				}
-		
-			});		
-			
-			return false;
-		};
-		
-	$.fn.removeTag = function(value) { 
-			value = unescape(value);
-			this.each(function() { 
-				var id = $(this).attr('id');
-	
-				var old = $(this).val().split(delimiter[id]);
-					
-				$('#'+id+'_tagsinput .tag').remove();
-				str = '';
-				for (i=0; i< old.length; i++) { 
-					if (old[i]!=value) { 
-						str = str + delimiter[id] +old[i];
-					}
-				}
-				
-				$.fn.tagsInput.importTags(this,str);
-
-				if (tags_callbacks[id] && tags_callbacks[id]['onRemoveTag']) {
-					var f = tags_callbacks[id]['onRemoveTag'];
-					f.call(this, value);
-				}
-			});
-					
-			return false;
-		};
-	
-	$.fn.tagExist = function(val) {
-		return (jQuery.inArray(val, $(this)) >= 0); //true when tag exists, false when not
-	};
-	
-	// clear all existing tags and import new ones from a string
-	$.fn.importTags = function(str) {
-                id = $(this).attr('id');
-		$('#'+id+'_tagsinput .tag').remove();
-		$.fn.tagsInput.importTags(this,str);
-	}
-		
-	$.fn.tagsInput = function(options) { 
-    var settings = jQuery.extend({
-      interactive:true,
-      defaultText:'add a tag',
-      minChars:0,
-      width:'300px',
-      height:'100px',
-      autocomplete: {selectFirst: false },
-      'hide':true,
-      'delimiter':',',
-      'unique':true,
-      removeWithBackspace:true,
-      placeholderColor:'#666666',
-      autosize: true,
-      comfortZone: 20,
-      inputPadding: 6*2
-    },options);
-
-		this.each(function() { 
-			if (settings.hide) { 
-				$(this).hide();				
-			}
-				
-			var id = $(this).attr('id')
-			
-			var data = jQuery.extend({
-				pid:id,
-				real_input: '#'+id,
-				holder: '#'+id+'_tagsinput',
-				input_wrapper: '#'+id+'_addTag',
-				fake_input: '#'+id+'_tag'
-			},settings);
-	
-			delimiter[id] = data.delimiter;
-			
-			if (settings.onAddTag || settings.onRemoveTag || settings.onChange) {
-				tags_callbacks[id] = new Array();
-				tags_callbacks[id]['onAddTag'] = settings.onAddTag;
-				tags_callbacks[id]['onRemoveTag'] = settings.onRemoveTag;
-				tags_callbacks[id]['onChange'] = settings.onChange;
-			}
-	
-			var markup = '<div id="'+id+'_tagsinput" class="tagsinput"><div id="'+id+'_addTag">';
-			
-			if (settings.interactive) {
-				markup = markup + '<input id="'+id+'_tag" value="" data-default="'+settings.defaultText+'" />';
-			}
-			
-			markup = markup + '</div><div class="tags_clear"></div></div>';
-			
-			$(markup).insertAfter(this);
-
-			$(data.holder).css('width',settings.width);
-			$(data.holder).css('height',settings.height);
-	
-			if ($(data.real_input).val()!='') { 
-				$.fn.tagsInput.importTags($(data.real_input),$(data.real_input).val());
-			}		
-			if (settings.interactive) { 
-				$(data.fake_input).val($(data.fake_input).attr('data-default'));
-				$(data.fake_input).css('color',settings.placeholderColor);
-		        $(data.fake_input).resetAutosize(settings);
-		
-				$(data.holder).bind('click',data,function(event) {
-					$(event.data.fake_input).focus();
-				});
-			
-				$(data.fake_input).bind('focus',data,function(event) {
-					if ($(event.data.fake_input).val()==$(event.data.fake_input).attr('data-default')) { 
-						$(event.data.fake_input).val('');
-					}
-					$(event.data.fake_input).css('color','#000000');		
-				});
-						
-				if (settings.autocomplete_url != undefined) {
-          // 2012-02-23 me
-//          autocomplete_options = {source: settings.autocomplete_url};
-          autocomplete_options = settings.auto;
-          // 2012-02-23 me
-					for (attrname in settings.autocomplete) { 
-						autocomplete_options[attrname] = settings.autocomplete[attrname]; 
-					}
-				
-					if (jQuery.Autocompleter !== undefined) {
-						$(data.fake_input).autocomplete(settings.autocomplete_url, settings.autocomplete);
-						$(data.fake_input).bind('result',data,function(event,data,formatted) {
-							if (data) {
-								$('#'+id).addTag(data[0] + "",{focus:true,unique:(settings.unique)});
-							}
-					  	});
-					} else if (jQuery.ui.autocomplete !== undefined) {
-						$(data.fake_input).autocomplete(autocomplete_options);
-						$(data.fake_input).bind('autocompleteselect',data,function(event,ui) {
-							$(event.data.real_input).addTag(ui.item.value,{focus:true,unique:(settings.unique)});
-							return false;
-						});
-					}
-				
-					
-				} else {
-						// if a user tabs out of the field, create a new tag
-						// this is only available if autocomplete is not used.
-						$(data.fake_input).bind('blur',data,function(event) { 
-							var d = $(this).attr('data-default');
-							if ($(event.data.fake_input).val()!='' && $(event.data.fake_input).val()!=d) { 
-								if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
-									$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
-							} else {
-								$(event.data.fake_input).val($(event.data.fake_input).attr('data-default'));
-								$(event.data.fake_input).css('color',settings.placeholderColor);
-							}
-							return false;
-						});
-				
-				}
-				// if user types a comma, create a new tag
-				$(data.fake_input).bind('keypress',data,function(event) {
-					if (event.which==event.data.delimiter.charCodeAt(0) || event.which==13 ) {
-					    event.preventDefault();
-						if( (event.data.minChars <= $(event.data.fake_input).val().length) && (!event.data.maxChars || (event.data.maxChars >= $(event.data.fake_input).val().length)) )
-							$(event.data.real_input).addTag($(event.data.fake_input).val(),{focus:true,unique:(settings.unique)});
-					  	$(event.data.fake_input).resetAutosize(settings);
-						return false;
-					} else if (event.data.autosize) {
-			            $(event.data.fake_input).doAutosize(settings);
-            
-          			}
-				});
-				//Delete last tag on backspace
-				data.removeWithBackspace && $(data.fake_input).bind('keydown', function(event)
-				{
-					if(event.keyCode == 8 && $(this).val() == '')
-					{
-						 event.preventDefault();
-						 var last_tag = $(this).closest('.tagsinput').find('.tag:last').text();
-						 var id = $(this).attr('id').replace(/_tag$/, '');
-						 last_tag = last_tag.replace(/[\s]+x$/, '');
-						 $('#' + id).removeTag(escape(last_tag));
-						 $(this).trigger('focus');
-					}
-				});
-				$(data.fake_input).blur();
-				
-				//Removes the not_valid class when user changes the value of the fake input
-				if(data.unique) {
-				    $(data.fake_input).keydown(function(event){
-				        if(event.keyCode == 8 || String.fromCharCode(event.which).match(/\w+|[áéíóúÁÉÍÓÚñÑ,/]+/)) {
-				            $(this).removeClass('not_valid');
-				        }
-				    });
-				}
-			} // if settings.interactive
-			return false;
-		});
-			
-		return this;
-	
-	};
-	
-	$.fn.tagsInput.updateTagsField = function(obj,tagslist) { 
-		var id = $(obj).attr('id');
-		$(obj).val(tagslist.join(delimiter[id]));
-	};
-	
-	$.fn.tagsInput.importTags = function(obj,val) {			
-		$(obj).val('');
-		var id = $(obj).attr('id');
-		var tags = val.split(delimiter[id]);
-		for (i=0; i<tags.length; i++) { 
-			$(obj).addTag(tags[i],{focus:false,callback:false});
-		}
-		if(tags_callbacks[id] && tags_callbacks[id]['onChange'])
-		{
-			var f = tags_callbacks[id]['onChange'];
-			f.call(obj, obj, tags[i]);
-		}
-	};
-
-})(jQuery);

+ 1 - 1
app/assets/stylesheets/application.css

@@ -7,7 +7,7 @@
  *= require ./fineuploader.css
  *= require ./ui-lightness/jquery-ui-1.8.23.custom.css
  *= require ./jquery.noty.css
- *= require ./jquery.tagsinput.css
+ *= require ./bootstrap-tokenfield.css
  *= require ./noty_theme_twitter.css
  *= require ./sew.css
  *= require ./zzz.css

+ 209 - 0
app/assets/stylesheets/bootstrap-tokenfield.css

@@ -0,0 +1,209 @@
+/*!
+ * bootstrap-tokenfield
+ * https://github.com/sliptree/bootstrap-tokenfield
+ * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
+ */
+@-webkit-keyframes 'blink' {
+  0% {
+    border-color: #ededed;
+  }
+  100% {
+    border-color: #b94a48;
+  }
+}
+@-moz-keyframes 'blink' {
+  0% {
+    border-color: #ededed;
+  }
+  100% {
+    border-color: #b94a48;
+  }
+}
+@keyframes 'blink' {
+  0% {
+    border-color: #ededed;
+  }
+  100% {
+    border-color: #b94a48;
+  }
+}
+.tokenfield {
+  height: auto;
+  min-height: 34px;
+  padding-bottom: 0px;
+}
+.tokenfield.focus {
+  border-color: #66afe9;
+  outline: 0;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+.tokenfield .token {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  display: inline-block;
+  border: 1px solid #d9d9d9;
+  background-color: #ededed;
+  white-space: nowrap;
+  margin: -1px 5px 5px 0;
+  height: 22px;
+  vertical-align: top;
+  cursor: default;
+}
+.tokenfield .token:hover {
+  border-color: #b9b9b9;
+}
+.tokenfield .token.active {
+  border-color: #52a8ec;
+  border-color: rgba(82, 168, 236, 0.8);
+}
+.tokenfield .token.duplicate {
+  border-color: #ebccd1;
+  -webkit-animation-name: blink;
+  animation-name: blink;
+  -webkit-animation-duration: 0.1s;
+  animation-duration: 0.1s;
+  -webkit-animation-direction: normal;
+  animation-direction: normal;
+  -webkit-animation-timing-function: ease;
+  animation-timing-function: ease;
+  -webkit-animation-iteration-count: infinite;
+  animation-iteration-count: infinite;
+}
+.tokenfield .token.invalid {
+  background: none;
+  border: 1px solid transparent;
+  -webkit-border-radius: 0;
+  -moz-border-radius: 0;
+  border-radius: 0;
+  border-bottom: 1px dotted #d9534f;
+}
+.tokenfield .token.invalid.active {
+  background: #ededed;
+  border: 1px solid #ededed;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+}
+.tokenfield .token .token-label {
+  display: inline-block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  padding-left: 4px;
+  vertical-align: top;
+}
+.tokenfield .token .close {
+  font-family: Arial;
+  display: inline-block;
+  line-height: 100%;
+  font-size: 1.1em;
+  line-height: 1.49em;
+  margin-left: 5px;
+  float: none;
+  height: 100%;
+  vertical-align: top;
+  padding-right: 4px;
+}
+.tokenfield .token-input {
+  background: none;
+  width: 60px;
+  min-width: 60px;
+  border: 0;
+  height: 20px;
+  padding: 0;
+  margin-bottom: 6px;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+.tokenfield .token-input:focus {
+  border-color: transparent;
+  outline: 0;
+  /* IE6-9 */
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+.tokenfield.disabled {
+  cursor: not-allowed;
+  background-color: #eeeeee;
+}
+.tokenfield.disabled .token-input {
+  cursor: not-allowed;
+}
+.tokenfield.disabled .token:hover {
+  cursor: not-allowed;
+  border-color: #d9d9d9;
+}
+.tokenfield.disabled .token:hover .close {
+  cursor: not-allowed;
+  opacity: 0.2;
+  filter: alpha(opacity=20);
+}
+.has-warning .tokenfield.focus {
+  border-color: #66512c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+}
+.has-error .tokenfield.focus {
+  border-color: #843534;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+}
+.has-success .tokenfield.focus {
+  border-color: #2b542c;
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+}
+.tokenfield.input-sm,
+.input-group-sm .tokenfield {
+  min-height: 30px;
+  padding-bottom: 0px;
+}
+.input-group-sm .token,
+.tokenfield.input-sm .token {
+  height: 20px;
+  margin-bottom: 4px;
+}
+.input-group-sm .token-input,
+.tokenfield.input-sm .token-input {
+  height: 18px;
+  margin-bottom: 5px;
+}
+.tokenfield.input-lg,
+.input-group-lg .tokenfield {
+  min-height: 45px;
+  padding-bottom: 4px;
+}
+.input-group-lg .token,
+.tokenfield.input-lg .token {
+  height: 25px;
+}
+.input-group-lg .token-label,
+.tokenfield.input-lg .token-label {
+  line-height: 23px;
+}
+.input-group-lg .token .close,
+.tokenfield.input-lg .token .close {
+  line-height: 1.3em;
+}
+.input-group-lg .token-input,
+.tokenfield.input-lg .token-input {
+  height: 23px;
+  line-height: 23px;
+  margin-bottom: 6px;
+  vertical-align: top;
+}
+.tokenfield.rtl {
+  direction: rtl;
+  text-align: right;
+}
+.tokenfield.rtl .token {
+  margin: -1px 0 5px 5px;
+}
+.tokenfield.rtl .token .token-label {
+  padding-left: 0px;
+  padding-right: 4px;
+}

+ 0 - 7
app/assets/stylesheets/jquery.tagsinput.css

@@ -1,7 +0,0 @@
-div.tagsinput { border:1px solid #CCC; background: #FFF; padding:5px; width:300px; height:100px; overflow-y: auto;}
-div.tagsinput span.tag { border: 1px solid #a5d24a; -moz-border-radius:2px; -webkit-border-radius:2px; display: block; float: left; padding: 3px; text-decoration:none; background: #cde69c; color: #638421; margin-right: 5px; margin-bottom:5px;font-family: helvetica;  font-size:13px;}
-div.tagsinput span.tag a { font-weight: bold; color: #82ad2b; text-decoration:none; font-size: 11px;  } 
-div.tagsinput input { width:380px; margin:0px; font-family: helvetica; font-size: 13px; border:1px solid transparent; padding:3px; background: transparent; color: #000; outline:0px;  margin-right:5px; margin-bottom:5px; }
-div.tagsinput div { display:block; float: left; } 
-.tags_clear { clear: both; width: 100%; height: 0px; }
-.not_valid {background: #FBD8DB !important; color: #90111A !important;}