From 3d9edbbbb1f897685de1caf92f8b9d03c5e48171 Mon Sep 17 00:00:00 2001 From: Francisco Marcelo de Araújo Lima Júnior Date: Fri, 8 Aug 2014 18:57:45 -0300 Subject: [PATCH] change tagging ui --- app/controllers/my_profile/cms_controller.rb | 6 ++++++ app/helpers/article_helper.rb | 4 ++++ app/views/cms/edit.html.erb | 12 +++++++++++- app/views/layouts/_javascript.html.erb | 2 +- public/javascripts/inputosaurus.js | 523 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ public/stylesheets/inputosaurus.css | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 public/javascripts/inputosaurus.js create mode 100644 public/stylesheets/inputosaurus.css diff --git a/app/controllers/my_profile/cms_controller.rb b/app/controllers/my_profile/cms_controller.rb index 3fcee17..3992d63 100644 --- a/app/controllers/my_profile/cms_controller.rb +++ b/app/controllers/my_profile/cms_controller.rb @@ -4,6 +4,12 @@ class CmsController < MyProfileController include ArticleHelper + def search_tags + arg = params[:term].downcase + result = ActsAsTaggableOn::Tag.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"]) + render :text => prepare_to_token_input2(result).to_json + end + def self.protect_if(*args) before_filter(*args) do |c| user, profile = c.send(:user), c.send(:profile) diff --git a/app/helpers/article_helper.rb b/app/helpers/article_helper.rb index 26bc18d..afcc572 100644 --- a/app/helpers/article_helper.rb +++ b/app/helpers/article_helper.rb @@ -83,6 +83,10 @@ module ArticleHelper array.map { |object| {:id => object.id, :name => object.name} } end + def prepare_to_token_input2(array) + array.map { |object| {:label => object.name, :value => object.name} } + end + def cms_label_for_new_children _('New article') end diff --git a/app/views/cms/edit.html.erb b/app/views/cms/edit.html.erb index c38af1c..ba4ed80 100644 --- a/app/views/cms/edit.html.erb +++ b/app/views/cms/edit.html.erb @@ -31,8 +31,18 @@ <%= select_categories(:article, _('Categorize your article')) %> +
+ + <%= stylesheet_link_tag('inputosaurus') %> + <%= f.text_field('tag_list', :size => 64) %> - <%= content_tag( 'small', _('Separate tags with commas') ) %> + +
<%= options_for_article(@article, @tokenized_children) %> diff --git a/app/views/layouts/_javascript.html.erb b/app/views/layouts/_javascript.html.erb index 33c05d4..f33bdc4 100644 --- a/app/views/layouts/_javascript.html.erb +++ b/app/views/layouts/_javascript.html.erb @@ -4,7 +4,7 @@ 'jquery-ui-1.10.4/js/jquery-ui-1.10.4.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate', 'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'add-and-join', 'report-abuse', 'catalog', 'manage-products', 'autogrow', -'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', :cache => 'cache/application' %> +'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', 'inputosaurus.js', :cache => 'cache/application' %> <% language = FastGettext.locale %> <% %w{messages methods}.each do |type| %> diff --git a/public/javascripts/inputosaurus.js b/public/javascripts/inputosaurus.js new file mode 100644 index 0000000..999d1a0 --- /dev/null +++ b/public/javascripts/inputosaurus.js @@ -0,0 +1,523 @@ +/** + * Inputosaurus Text + * + * Must be instantiated on an element + * Allows multiple input items. Each item is represented with a removable tag that appears to be inside the input area. + * + * @requires: + * + * jQuery 1.7+ + * jQueryUI 1.8+ Core + * + * @version 0.1.6 + * @author Dan Kielp + * @created October 3,2012 + * + */ + + +(function($) { + + var inputosaurustext = { + + version: "0.1.6", + + eventprefix: "inputosaurus", + + options: { + + // bindable events + // + // 'change' - triggered whenever a tag is added or removed (should be similar to binding the the change event of the instantiated input + // 'keyup' - keyup event on the newly created input + + // while typing, the user can separate values using these delimiters + // the value tags are created on the fly when an inputDelimiter is detected + inputDelimiters : [',', ';'], + + // this separator is used to rejoin all input items back to the value of the original + outputDelimiter : ',', + + allowDuplicates : false, + + parseOnBlur : false, + + // optional wrapper for widget + wrapperElement : null, + + width : null, + + // simply passing an autoComplete source (array, string or function) will instantiate autocomplete functionality + autoCompleteSource : '', + + // When forcing users to select from the autocomplete list, allow them to press 'Enter' to select an item if it's the only option left. + activateFinalResult : false, + + // manipulate and return the input value after parseInput() parsing + // the array of tag names is passed and expected to be returned as an array after manipulation + parseHook : null, + + // define a placeholder to display when the input is empty + placeholder: null, + + // when you check for duplicates it check for the case + caseSensitiveDuplicates: false + }, + + _create: function() { + var widget = this, + els = {}, + o = widget.options, + placeholder = o.placeholder || this.element.attr('placeholder') || null; + + this._chosenValues = []; + + // Create the elements + els.ul = $('
    '); + els.input = $(''); + els.inputCont = $('
  • '); + els.origInputCont = $('
  • '); + + // define starting placeholder + if (placeholder) { + o.placeholder = placeholder; + els.input.attr('placeholder', o.placeholder); + if (o.width) { + els.input.css('min-width', o.width - 50); + } + } + + o.wrapperElement && o.wrapperElement.append(els.ul); + this.element.replaceWith(o.wrapperElement || els.ul); + els.origInputCont.append(this.element).hide(); + + els.inputCont.append(els.input); + els.ul.append(els.inputCont); + els.ul.append(els.origInputCont); + + o.width && els.ul.css('width', o.width); + + this.elements = els; + + widget._attachEvents(); + + // if instantiated input already contains a value, parse that junk + if($.trim(this.element.val())){ + els.input.val( this.element.val() ); + this.parseInput(); + } + + this._instAutocomplete(); + }, + + _instAutocomplete : function() { + if(this.options.autoCompleteSource){ + var widget = this; + + this.elements.input.autocomplete({ + position : { + of : this.elements.ul + }, + source : this.options.autoCompleteSource, + minLength : 1, + select : function(ev, ui){ + ev.preventDefault(); + widget.elements.input.val(ui.item.value); + widget.parseInput(); + }, + open : function() { + // Older versions of jQueryUI have a different namespace + var auto = $(this).data('ui-autocomplete') || $(this).data('autocomplete'); + var menu = auto.menu, + $menuItems; + + + // zIndex will force the element on top of anything (like a dialog it's in) + menu.element.zIndex && menu.element.zIndex($(this).zIndex() + 1); + menu.element.width(widget.elements.ul.outerWidth()); + + // auto-activate the result if it's the only one + if(widget.options.activateFinalResult){ + $menuItems = menu.element.find('li'); + + // activate single item to allow selection upon pressing 'Enter' + if($menuItems.size() === 1){ + menu[menu.activate ? 'activate' : 'focus']($.Event('click'), $menuItems); + } + } + } + }); + } + }, + + _autoCompleteMenuPosition : function() { + var widget; + if(this.options.autoCompleteSource){ + widget = this.elements.input.data('ui-autocomplete') || this.elements.input.data('autocomplete'); + widget && widget.menu.element.position({ + of: this.elements.ul, + my: 'left top', + at: 'left bottom', + collision: 'none' + }); + } + }, + + /*_closeAutoCompleteMenu : function() { + if(this.options.autoCompleteSource){ + this.elements.input.autocomplete('close'); + } + },*/ + + parseInput : function(ev) { + var widget = (ev && ev.data.widget) || this, + val, + delimiterFound = false, + values = []; + + val = widget.elements.input.val(); + + val && (delimiterFound = widget._containsDelimiter(val)); + + if(delimiterFound !== false){ + values = val.split(delimiterFound); + } else if(!ev || ev.which === $.ui.keyCode.ENTER && !$('.ui-menu-item.ui-state-focus').size() && !$('.ui-menu-item .ui-state-focus').size() && !$('#ui-active-menuitem').size()){ + values.push(val); + ev && ev.preventDefault(); + + // prevent autoComplete menu click from causing a false 'blur' + } else if(ev.type === 'blur' && !$('#ui-active-menuitem').size()){ + values.push(val); + } + + $.isFunction(widget.options.parseHook) && (values = widget.options.parseHook(values)); + + if(values.length){ + widget._setChosen(values); + widget.elements.input.val(''); + widget._resizeInput(); + } + + widget._resetPlaceholder(); + }, + + _inputFocus : function(ev) { + var widget = ev.data.widget || this; + + widget.elements.input.value || (widget.options.autoCompleteSource.length && widget.elements.input.autocomplete('search', '')); + }, + + _inputKeypress : function(ev) { + var widget = ev.data.widget || this; + + ev.type === 'keyup' && widget._trigger('keyup', ev, widget); + + switch(ev.which){ + case $.ui.keyCode.BACKSPACE: + ev.type === 'keydown' && widget._inputBackspace(ev); + break; + + case $.ui.keyCode.LEFT: + ev.type === 'keydown' && widget._inputBackspace(ev); + break; + + default : + widget.parseInput(ev); + widget._resizeInput(ev); + } + + // reposition autoComplete menu as
      grows and shrinks vertically + if(widget.options.autoCompleteSource){ + setTimeout(function(){widget._autoCompleteMenuPosition.call(widget);}, 200); + } + }, + + // the input dynamically resizes based on the length of its value + _resizeInput : function(ev) { + var widget = (ev && ev.data.widget) || this, + maxWidth = widget.elements.ul.width(), + val = widget.elements.input.val(), + txtWidth = 25 + val.length * 8; + + widget.elements.input.width(txtWidth < maxWidth ? txtWidth : maxWidth); + }, + + // resets placeholder on representative input + _resetPlaceholder: function () { + var placeholder = this.options.placeholder, + input = this.elements.input, + width = this.options.width || 'inherit'; + if (placeholder && this.element.val().length === 0) { + input.attr('placeholder', placeholder).css('min-width', width - 50) + }else { + input.attr('placeholder', '').css('min-width', 'inherit') + } + }, + + // if our input contains no value and backspace has been pressed, select the last tag + _inputBackspace : function(ev) { + var widget = (ev && ev.data.widget) || this; + lastTag = widget.elements.ul.find('li:not(.inputosaurus-required):last'); + + // IE goes back in history if the event isn't stopped + ev.stopPropagation(); + + if((!$(ev.currentTarget).val() || (('selectionStart' in ev.currentTarget) && ev.currentTarget.selectionStart === 0 && ev.currentTarget.selectionEnd === 0)) && lastTag.size()){ + ev.preventDefault(); + lastTag.find('a').focus(); + } + + }, + + _editTag : function(ev) { + var widget = (ev && ev.data.widget) || this, + tagName = '', + $closest = $(ev.currentTarget).closest('li'), + tagKey = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus'); + + if(!tagKey){ + return true; + } + + ev.preventDefault(); + + $.each(widget._chosenValues, function(i,v) { + v.key === tagKey && (tagName = v.value); + }); + + widget.elements.input.val(tagName); + + widget._removeTag(ev); + widget._resizeInput(ev); + }, + + _tagKeypress : function(ev) { + var widget = ev.data.widget; + switch(ev.which){ + + case $.ui.keyCode.BACKSPACE: + ev && ev.preventDefault(); + ev && ev.stopPropagation(); + $(ev.currentTarget).trigger('click'); + break; + + // 'e' - edit tag (removes tag and places value into visible input + case 69: + widget._editTag(ev); + break; + + case $.ui.keyCode.LEFT: + ev.type === 'keydown' && widget._prevTag(ev); + break; + + case $.ui.keyCode.RIGHT: + ev.type === 'keydown' && widget._nextTag(ev); + break; + + case $.ui.keyCode.DOWN: + ev.type === 'keydown' && widget._focus(ev); + break; + } + }, + + // select the previous tag or input if no more tags exist + _prevTag : function(ev) { + var widget = (ev && ev.data.widget) || this, + tag = $(ev.currentTarget).closest('li'), + previous = tag.prev(); + + if(previous.is('li')){ + previous.find('a').focus(); + } else { + widget._focus(); + } + }, + + // select the next tag or input if no more tags exist + _nextTag : function(ev) { + var widget = (ev && ev.data.widget) || this, + tag = $(ev.currentTarget).closest('li'), + next = tag.next(); + + if(next.is('li:not(.inputosaurus-input)')){ + next.find('a').focus(); + } else { + widget._focus(); + } + }, + + // return the inputDelimiter that was detected or false if none were found + _containsDelimiter : function(tagStr) { + + var found = false; + + $.each(this.options.inputDelimiters, function(k,v) { + if(tagStr.indexOf(v) !== -1){ + found = v; + } + }); + + return found; + }, + + _setChosen : function(valArr) { + var self = this; + + if(!$.isArray(valArr)){ + return false; + } + + $.each(valArr, function(k,v) { + var exists = false, + obj = { + key : '', + value : '' + }; + + v = $.trim(v); + + $.each(self._chosenValues, function(kk,vv) { + if(!self.options.caseSensitiveDuplicates){ + vv.value.toLowerCase() === v.toLowerCase() && (exists = true); + } + else{ + vv.value === v && (exists = true); + } + }); + + if(v !== '' && (!exists || self.options.allowDuplicates)){ + + obj.key = 'mi_' + Math.random().toString( 16 ).slice( 2, 10 ); + obj.value = v; + self._chosenValues.push(obj); + + self._renderTags(); + } + }); + self._setValue(self._buildValue()); + }, + + _buildValue : function() { + var widget = this, + value = ''; + + $.each(this._chosenValues, function(k,v) { + value += value.length ? widget.options.outputDelimiter + v.value : v.value; + }); + + return value; + }, + + _setValue : function(value) { + var val = this.element.val(); + + if(val !== value){ + this.element.val(value); + this._trigger('change'); + } + }, + + // @name text for tag + // @className optional className for
    • + _createTag : function(name, key, className) { + className = className ? ' class="' + className + '"' : ''; + + if(name !== undefined){ + return $('' + name + '
    • '); + } + }, + + _renderTags : function() { + var self = this; + + this.elements.ul.find('li:not(.inputosaurus-required)').remove(); + + $.each(this._chosenValues, function(k,v) { + var el = self._createTag(v.value, v.key); + self.elements.ul.find('li.inputosaurus-input').before(el); + }); + }, + + _removeTag : function(ev) { + var $closest = $(ev.currentTarget).closest('li'), + key = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus'), + indexFound = false, + widget = (ev && ev.data.widget) || this; + + + $.each(widget._chosenValues, function(k,v) { + if(key === v.key){ + indexFound = k; + } + }); + + indexFound !== false && widget._chosenValues.splice(indexFound, 1); + + widget._setValue(widget._buildValue()); + + $(ev.currentTarget).closest('li').remove(); + widget.elements.input.focus(); + }, + + _focus : function(ev) { + var widget = (ev && ev.data.widget) || this, + $closest = $(ev.target).closest('li'), + $data = $closest.data('ui-inputosaurus') || $closest.data('inputosaurus'); + + if(!ev || !$data){ + widget.elements.input.focus(); + } + }, + + _tagFocus : function(ev) { + $(ev.currentTarget).parent()[ev.type === 'focusout' ? 'removeClass' : 'addClass']('inputosaurus-selected'); + }, + + refresh : function() { + var delim = this.options.outputDelimiter, + val = this.element.val(), + values = []; + + values.push(val); + delim && (values = val.split(delim)); + + if(values.length){ + this._chosenValues = []; + + $.isFunction(this.options.parseHook) && (values = this.options.parseHook(values)); + + this._setChosen(values); + this._renderTags(); + this.elements.input.val(''); + this._resizeInput(); + } + }, + + _attachEvents : function() { + var widget = this; + + this.elements.input.on('keyup.inputosaurus', {widget : widget}, this._inputKeypress); + this.elements.input.on('keydown.inputosaurus', {widget : widget}, this._inputKeypress); + this.elements.input.on('change.inputosaurus', {widget : widget}, this._inputKeypress); + this.elements.input.on('focus.inputosaurus', {widget : widget}, this._inputFocus); + this.options.parseOnBlur && this.elements.input.on('blur.inputosaurus', {widget : widget}, this.parseInput); + + this.elements.ul.on('click.inputosaurus', {widget : widget}, this._focus); + this.elements.ul.on('click.inputosaurus', 'a', {widget : widget}, this._removeTag); + this.elements.ul.on('dblclick.inputosaurus', 'li', {widget : widget}, this._editTag); + this.elements.ul.on('focus.inputosaurus', 'a', {widget : widget}, this._tagFocus); + this.elements.ul.on('blur.inputosaurus', 'a', {widget : widget}, this._tagFocus); + this.elements.ul.on('keydown.inputosaurus', 'a', {widget : widget}, this._tagKeypress); + }, + + _destroy: function() { + this.elements.input.unbind('.inputosaurus'); + + this.elements.ul.replaceWith(this.element); + + } + }; + + $.widget("ui.inputosaurus", inputosaurustext); +})(jQuery); + diff --git a/public/stylesheets/inputosaurus.css b/public/stylesheets/inputosaurus.css new file mode 100644 index 0000000..0f407e6 --- /dev/null +++ b/public/stylesheets/inputosaurus.css @@ -0,0 +1,74 @@ +.inputosaurus-container { + /*background-color: #fff;*/ + /*border: 1px solid #bcbec0;*/ + margin: 0; + padding: 0; + display: inline-block; + cursor: text; + /**font-size: 14px;**/ + /**font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;**/ + max-height: 300px; + overflow: hidden; + overflow-y: auto; +} +.inputosaurus-container li { + display: block; + float: left; + overflow: hidden; + margin: 2px 2px 0; + padding: 2px 3px; + white-space: nowrap; + overflow: hidden; + -o-text-overflow: ellipsis; + -ms-text-overflow: ellipsis; + text-overflow: ellipsis; + background-color: #e5eff7; + border: #a9cae4 solid 1px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + color: #5b9bcd; + -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset; + -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset; + box-shadow: 0 1px 0 rgba(255,255,255,0.75) inset; + line-height: 20px; + cursor: default; +} +.inputosaurus-container li.inputosaurus-selected { background-color: #bdd6eb; } +.inputosaurus-container li a { + font-size: 16px; + color: #5b9bcd; + padding: 1px; + text-decoration: none; + outline: none; +} +.inputosaurus-container .inputosaurus-input { + border: none; + box-shadow: none; + background-color: #fff; + margin-top: 3px; +} +.inputosaurus-container .inputosaurus-input input { + border: none; + height: 23px; + font-size: 14px; + line-height: 20px; + color: #555; + margin: 0; + outline: none; + padding: 0 0 1px 1px; + width: 25px; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + background-image: none; + text-indent: 0px; +} + +.inputosaurus-container .inputosaurus-input input:hover { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + border: 1px solid #c0c0c0; +} +.inputosaurus-input-hidden { display: none; } -- libgit2 0.21.2