diff options
| author | mo k <mo@mokhan.ca> | 2013-02-09 09:16:59 -0700 |
|---|---|---|
| committer | mo k <mo@mokhan.ca> | 2013-02-09 09:16:59 -0700 |
| commit | 3f211d5f29435b693a952cf8bf886884decfbf23 (patch) | |
| tree | 5389a431cbf59560f57c5ac2acec8375b492c7e2 | |
| parent | 8c090b7f5fc8f5b349e108e78695b389051fb084 (diff) | |
add tags to creations
| -rw-r--r-- | app/assets/javascripts/application.js | 1 | ||||
| -rw-r--r-- | app/assets/javascripts/users_edit.js | 1 | ||||
| -rw-r--r-- | app/controllers/creations_controller.rb | 2 | ||||
| -rw-r--r-- | app/models/creation.rb | 1 | ||||
| -rw-r--r-- | app/views/creations/_form.html.erb | 16 | ||||
| -rw-r--r-- | app/views/creations/new.html.erb | 17 | ||||
| -rw-r--r-- | app/views/creations/show.html.erb | 3 | ||||
| -rw-r--r-- | vendor/assets/javascripts/tag-it.js | 533 |
8 files changed, 558 insertions, 16 deletions
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 1833d703..1b807df3 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -12,5 +12,6 @@ //= require jquery.validate //= require jquery.masonry //= require jquery-fileupload +//= require tag-it //= require bootstrap //= require_tree . diff --git a/app/assets/javascripts/users_edit.js b/app/assets/javascripts/users_edit.js index e2c3cfa6..adaa0353 100644 --- a/app/assets/javascripts/users_edit.js +++ b/app/assets/javascripts/users_edit.js @@ -1,4 +1,5 @@ var DeviseUsers = DeviseUsers || {}; + DeviseUsers.Edit = { initialize: function($){ $('#edit_user').validate(); diff --git a/app/controllers/creations_controller.rb b/app/controllers/creations_controller.rb index 68d6d155..72fc28d3 100644 --- a/app/controllers/creations_controller.rb +++ b/app/controllers/creations_controller.rb @@ -25,6 +25,7 @@ class CreationsController < ApplicationController def create @creation = current_user.creations.create(params[:creation]) @creation.category_ids = params[:creation][:category_ids] ||= [] + current_user.tag(@creation, :with => params[:creation_tags], :on => :tags) if @creation.save redirect_to new_creation_photo_url(@creation) @@ -38,6 +39,7 @@ class CreationsController < ApplicationController def update @creation = current_user.creations.find(params[:id]) @creation.category_ids = params[:creation][:category_ids] ||= [] + current_user.tag(@creation, :with => params[:creation_tags], :on => :tags) if @creation.update_attributes(params[:creation]) redirect_to new_creation_photo_url(@creation) diff --git a/app/models/creation.rb b/app/models/creation.rb index d7ab98b9..112435a0 100644 --- a/app/models/creation.rb +++ b/app/models/creation.rb @@ -6,6 +6,7 @@ class Creation < ActiveRecord::Base has_and_belongs_to_many :categories, :join_table => 'creations_categories', :uniq => true, :autosave => true has_many :photos, :dependent => :destroy has_many :favorites, :dependent => :destroy + acts_as_taggable default_scope order("created_at DESC") diff --git a/app/views/creations/_form.html.erb b/app/views/creations/_form.html.erb index 5433db93..306b3d22 100644 --- a/app/views/creations/_form.html.erb +++ b/app/views/creations/_form.html.erb @@ -1,7 +1,13 @@ <% content_for :javascript do %> <script type="text/javascript" charset="utf-8"> $(function(){ + var all_tags = [ <% ActsAsTaggableOn::Tag.all.map(&:name).each { |item| %> '<%= item %>', <% } %> ]; $('.tooltip-item').tooltip(); + $('#tags').tagit({ + availableTags: all_tags, + singleField: true, + singleFieldNode: $('#tag-fields') + }); }); </script> <% end %> @@ -52,6 +58,16 @@ </label> </div> </div> + <div class="control-group"> + <div class="controls"> + <% if @creation.present? %> + <input name="creation_tags" id="tag-fields" value="<% @creation.tags.map(&:name).each { |item| %><%= item %>,<% } %>" type="hidden" /> + <% else %> + <input name="creation_tags" id="tag-fields" value="" type="hidden" /> + <% end %> + <ul id="tags"></ul> + </div> + </div> <div class="form-actions"> <button type="submit" class="btn btn-primary">NEXT STEP</button> <a href="<%= url_for @creation %>" class="btn">Cancel</a> diff --git a/app/views/creations/new.html.erb b/app/views/creations/new.html.erb index da5b22f7..7968c6d7 100644 --- a/app/views/creations/new.html.erb +++ b/app/views/creations/new.html.erb @@ -3,22 +3,7 @@ <h1>Share a new creation <small>(Step 1 of 2)</small></h1> <% end -%> <div class="row"> - <div class="span3"> - <div class="thumbnail"> - <%= avatar_for(current_user) %> - <div class="caption"> - <h5><%= current_user.name %></h5> - </div> - </div> - <div style="padding: 8px 0;" class="well"> - <ul class="nav nav-list"> - <li><a href="<%= url_for profiles_mine_path %>"><i class="icon-user"></i> Profile</a></li> - <li><a href="<%= url_for profiles_favorites_path -%>"><i class="icon-book"></i> Favorites</a></li> - <li><a href="<%= url_for edit_user_registration_path %>"><i class="icon-cog"></i> Settings</a></li> - </ul> - </div> - </div> - <div class="span9"> + <div class="span12"> <%= render 'form' %> </div> </div> diff --git a/app/views/creations/show.html.erb b/app/views/creations/show.html.erb index 85d962f4..98c73475 100644 --- a/app/views/creations/show.html.erb +++ b/app/views/creations/show.html.erb @@ -20,6 +20,9 @@ <% @creation.categories.each do |category| %> <a href="/categories/<%= category.slug %>"><span class="label"><%= category.name %></span></a> <% end %> + <% @creation.tags.each do |tag| -%> + <a href="/tags/<%= tag.name %>"><span class="label"><%= tag.name %></span></a> + <% end -%> <% end -%> <hr /> <% if @creation.user == current_user %> diff --git a/vendor/assets/javascripts/tag-it.js b/vendor/assets/javascripts/tag-it.js new file mode 100644 index 00000000..9d231dfe --- /dev/null +++ b/vendor/assets/javascripts/tag-it.js @@ -0,0 +1,533 @@ +/* +* jQuery UI Tag-it! +* +* @version v2.0 (06/2011) +* +* Copyright 2011, Levy Carneiro Jr. +* Released under the MIT license. +* http://aehlke.github.com/tag-it/LICENSE +* +* Homepage: +* http://aehlke.github.com/tag-it/ +* +* Authors: +* Levy Carneiro Jr. +* Martin Rehfeld +* Tobias Schmidt +* Skylar Challand +* Alex Ehlke +* +* Maintainer: +* Alex Ehlke - Twitter: @aehlke +* +* Dependencies: +* jQuery v1.4+ +* jQuery UI v1.8+ +*/ +(function($) { + + $.widget('ui.tagit', { + options: { + allowDuplicates : false, + caseSensitive : true, + fieldName : 'tags', + placeholderText : null, // Sets `placeholder` attr on input field. + readOnly : false, // Disables editing. + removeConfirmation: false, // Require confirmation to remove tags. + tagLimit : null, // Max number of tags allowed (null for unlimited). + + // Used for autocomplete, unless you override `autocomplete.source`. + availableTags : [], + + // Use to override or add any options to the autocomplete widget. + // + // By default, autocomplete.source will map to availableTags, + // unless overridden. + autocomplete: {}, + + // Shows autocomplete before the user even types anything. + showAutocompleteOnFocus: false, + + // When enabled, quotes are unneccesary for inputting multi-word tags. + allowSpaces: false, + + // The below options are for using a single field instead of several + // for our form values. + // + // When enabled, will use a single hidden field for the form, + // rather than one per tag. It will delimit tags in the field + // with singleFieldDelimiter. + // + // The easiest way to use singleField is to just instantiate tag-it + // on an INPUT element, in which case singleField is automatically + // set to true, and singleFieldNode is set to that element. This + // way, you don't need to fiddle with these options. + singleField: false, + + // This is just used when preloading data from the field, and for + // populating the field with delimited tags as the user adds them. + singleFieldDelimiter: ',', + + // Set this to an input DOM node to use an existing form field. + // Any text in it will be erased on init. But it will be + // populated with the text of tags as they are created, + // delimited by singleFieldDelimiter. + // + // If this is not set, we create an input node for it, + // with the name given in settings.fieldName. + singleFieldNode: null, + + // Whether to animate tag removals or not. + animate: true, + + // Optionally set a tabindex attribute on the input that gets + // created for tag-it. + tabIndex: null, + + // Event callbacks. + beforeTagAdded : null, + afterTagAdded : null, + + beforeTagRemoved : null, + afterTagRemoved : null, + + onTagClicked : null, + onTagLimitExceeded : null, + + + // DEPRECATED: + // + // /!\ These event callbacks are deprecated and WILL BE REMOVED at some + // point in the future. They're here for backwards-compatibility. + // Use the above before/after event callbacks instead. + onTagAdded : null, + onTagRemoved: null, + // `autocomplete.source` is the replacement for tagSource. + tagSource: null + // Do not use the above deprecated options. + }, + + _create: function() { + // for handling static scoping inside callbacks + var that = this; + + // There are 2 kinds of DOM nodes this widget can be instantiated on: + // 1. UL, OL, or some element containing either of these. + // 2. INPUT, in which case 'singleField' is overridden to true, + // a UL is created and the INPUT is hidden. + if (this.element.is('input')) { + this.tagList = $('<ul></ul>').insertAfter(this.element); + this.options.singleField = true; + this.options.singleFieldNode = this.element; + this.element.css('display', 'none'); + } else { + this.tagList = this.element.find('ul, ol').andSelf().last(); + } + + this.tagInput = $('<input type="text" />').addClass('ui-widget-content'); + + if (this.options.readOnly) this.tagInput.attr('disabled', 'disabled'); + + if (this.options.tabIndex) { + this.tagInput.attr('tabindex', this.options.tabIndex); + } + + if (this.options.placeholderText) { + this.tagInput.attr('placeholder', this.options.placeholderText); + } + + if (!this.options.autocomplete.source) { + this.options.autocomplete.source = function(search, showChoices) { + var filter = search.term.toLowerCase(); + var choices = $.grep(this.options.availableTags, function(element) { + // Only match autocomplete options that begin with the search term. + // (Case insensitive.) + return (element.toLowerCase().indexOf(filter) === 0); + }); + showChoices(this._subtractArray(choices, this.assignedTags())); + }; + } + + if (this.options.showAutocompleteOnFocus) { + this.tagInput.focus(function(event, ui) { + that._showAutocomplete(); + }); + + if (typeof this.options.autocomplete.minLength === 'undefined') { + this.options.autocomplete.minLength = 0; + } + } + + // Bind autocomplete.source callback functions to this context. + if ($.isFunction(this.options.autocomplete.source)) { + this.options.autocomplete.source = $.proxy(this.options.autocomplete.source, this); + } + + // DEPRECATED. + if ($.isFunction(this.options.tagSource)) { + this.options.tagSource = $.proxy(this.options.tagSource, this); + } + + this.tagList + .addClass('tagit') + .addClass('ui-widget ui-widget-content ui-corner-all') + // Create the input field. + .append($('<li class="tagit-new"></li>').append(this.tagInput)) + .click(function(e) { + var target = $(e.target); + if (target.hasClass('tagit-label')) { + var tag = target.closest('.tagit-choice'); + if (!tag.hasClass('removed')) { + that._trigger('onTagClicked', e, {tag: tag, tagLabel: that.tagLabel(tag)}); + } + } else { + // Sets the focus() to the input field, if the user + // clicks anywhere inside the UL. This is needed + // because the input field needs to be of a small size. + that.tagInput.focus(); + } + }); + + // Single field support. + var addedExistingFromSingleFieldNode = false; + if (this.options.singleField) { + if (this.options.singleFieldNode) { + // Add existing tags from the input field. + var node = $(this.options.singleFieldNode); + var tags = node.val().split(this.options.singleFieldDelimiter); + node.val(''); + $.each(tags, function(index, tag) { + that.createTag(tag, null, true); + addedExistingFromSingleFieldNode = true; + }); + } else { + // Create our single field input after our list. + this.options.singleFieldNode = $('<input type="hidden" style="display:none;" value="" name="' + this.options.fieldName + '" />'); + this.tagList.after(this.options.singleFieldNode); + } + } + + // Add existing tags from the list, if any. + if (!addedExistingFromSingleFieldNode) { + this.tagList.children('li').each(function() { + if (!$(this).hasClass('tagit-new')) { + that.createTag($(this).text(), $(this).attr('class'), true); + $(this).remove(); + } + }); + } + + // Events. + this.tagInput + .keydown(function(event) { + // Backspace is not detected within a keypress, so it must use keydown. + if (event.which == $.ui.keyCode.BACKSPACE && that.tagInput.val() === '') { + var tag = that._lastTag(); + if (!that.options.removeConfirmation || tag.hasClass('remove')) { + // When backspace is pressed, the last tag is deleted. + that.removeTag(tag); + } else if (that.options.removeConfirmation) { + tag.addClass('remove ui-state-highlight'); + } + } else if (that.options.removeConfirmation) { + that._lastTag().removeClass('remove ui-state-highlight'); + } + + // Comma/Space/Enter are all valid delimiters for new tags, + // except when there is an open quote or if setting allowSpaces = true. + // Tab will also create a tag, unless the tag input is empty, + // in which case it isn't caught. + if ( + event.which === $.ui.keyCode.COMMA || + event.which === $.ui.keyCode.ENTER || + ( + event.which == $.ui.keyCode.TAB && + that.tagInput.val() !== '' + ) || + ( + event.which == $.ui.keyCode.SPACE && + that.options.allowSpaces !== true && + ( + $.trim(that.tagInput.val()).replace( /^s*/, '' ).charAt(0) != '"' || + ( + $.trim(that.tagInput.val()).charAt(0) == '"' && + $.trim(that.tagInput.val()).charAt($.trim(that.tagInput.val()).length - 1) == '"' && + $.trim(that.tagInput.val()).length - 1 !== 0 + ) + ) + ) + ) { + // Enter submits the form if there's no text in the input. + if (!(event.which === $.ui.keyCode.ENTER && that.tagInput.val() === '')) { + event.preventDefault(); + } + + that.createTag(that._cleanedInput()); + + // The autocomplete doesn't close automatically when TAB is pressed. + // So let's ensure that it closes. + that.tagInput.autocomplete('close'); + } + }).blur(function(e){ + // Create a tag when the element loses focus. + // If autocomplete is enabled and suggestion was clicked, don't add it. + if (!that.tagInput.data('autocomplete-open')) { + that.createTag(that._cleanedInput()); + } + }); + + // Autocomplete. + if (this.options.availableTags || this.options.tagSource || this.options.autocomplete.source) { + var autocompleteOptions = { + select: function(event, ui) { + that.createTag(ui.item.value); + // Preventing the tag input to be updated with the chosen value. + return false; + } + }; + $.extend(autocompleteOptions, this.options.autocomplete); + + // tagSource is deprecated, but takes precedence here since autocomplete.source is set by default, + // while tagSource is left null by default. + autocompleteOptions.source = this.options.tagSource || autocompleteOptions.source; + + this.tagInput.autocomplete(autocompleteOptions).bind('autocompleteopen', function(event, ui) { + that.tagInput.data('autocomplete-open', true); + }).bind('autocompleteclose', function(event, ui) { + that.tagInput.data('autocomplete-open', false) + }); + } + }, + + _cleanedInput: function() { + // Returns the contents of the tag input, cleaned and ready to be passed to createTag + return $.trim(this.tagInput.val().replace(/^"(.*)"$/, '$1')); + }, + + _lastTag: function() { + return this.tagList.find('.tagit-choice:last:not(.removed)'); + }, + + _tags: function() { + return this.tagList.find('.tagit-choice:not(.removed)'); + }, + + assignedTags: function() { + // Returns an array of tag string values + var that = this; + var tags = []; + if (this.options.singleField) { + tags = $(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter); + if (tags[0] === '') { + tags = []; + } + } else { + this._tags().each(function() { + tags.push(that.tagLabel(this)); + }); + } + return tags; + }, + + _updateSingleTagsField: function(tags) { + // Takes a list of tag string values, updates this.options.singleFieldNode.val to the tags delimited by this.options.singleFieldDelimiter + $(this.options.singleFieldNode).val(tags.join(this.options.singleFieldDelimiter)).trigger('change'); + }, + + _subtractArray: function(a1, a2) { + var result = []; + for (var i = 0; i < a1.length; i++) { + if ($.inArray(a1[i], a2) == -1) { + result.push(a1[i]); + } + } + return result; + }, + + tagLabel: function(tag) { + // Returns the tag's string label. + if (this.options.singleField) { + return $(tag).find('.tagit-label:first').text(); + } else { + return $(tag).find('input:first').val(); + } + }, + + _showAutocomplete: function() { + this.tagInput.autocomplete('search', ''); + }, + + _findTagByLabel: function(name) { + var that = this; + var tag = null; + this._tags().each(function(i) { + if (that._formatStr(name) == that._formatStr(that.tagLabel(this))) { + tag = $(this); + return false; + } + }); + return tag; + }, + + _isNew: function(name) { + return !this._findTagByLabel(name); + }, + + _formatStr: function(str) { + if (this.options.caseSensitive) { + return str; + } + return $.trim(str.toLowerCase()); + }, + + _effectExists: function(name) { + return Boolean($.effects && ($.effects[name] || ($.effects.effect && $.effects.effect[name]))); + }, + + createTag: function(value, additionalClass, duringInitialization) { + var that = this; + + value = $.trim(value); + + if (value === '') { + return false; + } + + if (!this.options.allowDuplicates && !this._isNew(value)) { + var existingTag = this._findTagByLabel(value); + if (this._trigger('onTagExists', null, { + existingTag: existingTag, + duringInitialization: duringInitialization + }) !== false) { + if (this._effectExists('highlight')) { + existingTag.effect('highlight'); + } + } + return false; + } + + if (this.options.tagLimit && this._tags().length >= this.options.tagLimit) { + this._trigger('onTagLimitExceeded', null, {duringInitialization: duringInitialization}); + return false; + } + + var label = $(this.options.onTagClicked ? '<a class="tagit-label"></a>' : '<span class="tagit-label"></span>').text(value); + + // Create tag. + var tag = $('<li></li>') + .addClass('tagit-choice ui-widget-content ui-state-default ui-corner-all') + .addClass(additionalClass) + .append(label); + + if (this.options.readOnly){ + tag.addClass('tagit-choice-read-only'); + } else { + tag.addClass('tagit-choice-editable'); + // Button for removing the tag. + var removeTagIcon = $('<span></span>') + .addClass('ui-icon ui-icon-close'); + var removeTag = $('<a><span class="text-icon">\xd7</span></a>') // \xd7 is an X + .addClass('tagit-close') + .append(removeTagIcon) + .click(function(e) { + // Removes a tag when the little 'x' is clicked. + that.removeTag(tag); + }); + tag.append(removeTag); + } + + // Unless options.singleField is set, each tag has a hidden input field inline. + if (!this.options.singleField) { + var escapedValue = label.html(); + tag.append('<input type="hidden" style="display:none;" value="' + escapedValue + '" name="' + this.options.fieldName + '" />'); + } + + if (this._trigger('beforeTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + tags.push(value); + this._updateSingleTagsField(tags); + } + + // DEPRECATED. + this._trigger('onTagAdded', null, tag); + + this.tagInput.val(''); + + // Insert tag. + this.tagInput.parent().before(tag); + + this._trigger('afterTagAdded', null, { + tag: tag, + tagLabel: this.tagLabel(tag), + duringInitialization: duringInitialization + }); + + if (this.options.showAutocompleteOnFocus && !duringInitialization) { + setTimeout(function () { that._showAutocomplete(); }, 0); + } + }, + + removeTag: function(tag, animate) { + animate = typeof animate === 'undefined' ? this.options.animate : animate; + + tag = $(tag); + + // DEPRECATED. + this._trigger('onTagRemoved', null, tag); + + if (this._trigger('beforeTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}) === false) { + return; + } + + if (this.options.singleField) { + var tags = this.assignedTags(); + var removedTagLabel = this.tagLabel(tag); + tags = $.grep(tags, function(el){ + return el != removedTagLabel; + }); + this._updateSingleTagsField(tags); + } + + if (animate) { + tag.addClass('removed'); // Excludes this tag from _tags. + var hide_args = this._effectExists('blind') ? ['blind', {direction: 'horizontal'}, 'fast'] : ['fast']; + + hide_args.push(function() { + tag.remove(); + }); + + tag.fadeOut('fast').hide.apply(tag, hide_args).dequeue(); + } else { + tag.remove(); + } + + this._trigger('afterTagRemoved', null, {tag: tag, tagLabel: this.tagLabel(tag)}); + }, + + removeTagByLabel: function(tagLabel, animate) { + var toRemove = this._findTagByLabel(tagLabel); + if (!toRemove) { + throw "No such tag exists with the name '" + tagLabel + "'"; + } + this.removeTag(toRemove, animate); + }, + + removeAll: function() { + // Removes all tags. + var that = this; + this._tags().each(function(index, tag) { + that.removeTag(tag, false); + }); + } + + }); +})(jQuery); + |
