From 591fab6d79717eb20e72f1ea7d8e4b2443d73aea Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Thu, 3 Apr 2014 16:23:58 +0200 Subject: [PATCH] Moved to new tag lib. --- .../_application_controller_form.js.coffee | 32 +- .../app/controllers/widget/tag.js.coffee | 30 +- .../app/lib/base/bootstrap-tokenfield.js | 1020 +++++++++++++++++ .../app/lib/base/jquery.tagsinput.js | 353 ------ app/assets/stylesheets/application.css | 2 +- .../stylesheets/bootstrap-tokenfield.css | 209 ++++ app/assets/stylesheets/jquery.tagsinput.css | 7 - 7 files changed, 1242 insertions(+), 411 deletions(-) create mode 100755 app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js delete mode 100644 app/assets/javascripts/app/lib/base/jquery.tagsinput.js create mode 100755 app/assets/stylesheets/bootstrap-tokenfield.css delete mode 100644 app/assets/stylesheets/jquery.tagsinput.css diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee index 7e138c7d2..10674c2ff 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee +++ b/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' diff --git a/app/assets/javascripts/app/controllers/widget/tag.js.coffee b/app/assets/javascripts/app/controllers/widget/tag.js.coffee index e67b48a0b..431828d48 100644 --- a/app/assets/javascripts/app/controllers/widget/tag.js.coffee +++ b/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 ) diff --git a/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js b/app/assets/javascripts/app/lib/base/bootstrap-tokenfield.js new file mode 100755 index 000000000..196cc0d0b --- /dev/null +++ b/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 = $('
') + 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 = $('') + .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 = $('').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 = $(''); + 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 = $('
') + .attr('data-value', value) + .append('') + .append('×') + + // 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; + +})); diff --git a/app/assets/javascripts/app/lib/base/jquery.tagsinput.js b/app/assets/javascripts/app/lib/base/jquery.tagsinput.js deleted file mode 100644 index 5dd95c142..000000000 --- a/app/assets/javascripts/app/lib/base/jquery.tagsinput.js +++ /dev/null @@ -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, '&').replace(/\s/g,' ').replace(//g, '>'); - 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 = $('').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) { - $('').addClass('tag').append( - $('').text(value).append('  '), - $('', { - 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 = '
'; - - if (settings.interactive) { - markup = markup + ''; - } - - markup = markup + '
'; - - $(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