diff --git a/Gemfile b/Gemfile index 8da55b109562433a1d577939b21555bccfd67776..f32c13f070f1f34148bf58d24c5f9225f7796507 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,8 @@ gem "git" gem "acts_as_list" gem 'rdiscount' +gem 'acts-as-taggable-on', '~>2.1.0' + group :assets do gem 'sass-rails', " ~> 3.1.0" gem 'coffee-rails', "~> 3.1.0" diff --git a/Gemfile.lock b/Gemfile.lock index f66e832e4a6740dac90df83841b1d907f0915062..e557ee432e9b2f8a3b5871da8a837a22ed7b0b51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,6 +54,8 @@ GEM activesupport (= 3.1.0) activesupport (3.1.0) multi_json (~> 1.0) + acts-as-taggable-on (2.1.1) + rails acts_as_list (0.1.4) addressable (2.2.6) ansi (1.3.0) @@ -246,6 +248,7 @@ PLATFORMS ruby DEPENDENCIES + acts-as-taggable-on (~> 2.1.0) acts_as_list annotate! autotest diff --git a/app/models/project.rb b/app/models/project.rb index befa1c6b14e326b7bc38261fc937a68134d41b6a..de68f451e03086177b46ec262690d51aa91564a7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -9,6 +9,8 @@ class Project < ActiveRecord::Base has_many :notes, :dependent => :destroy has_many :snippets, :dependent => :destroy + acts_as_taggable + validates :name, :uniqueness => true, :presence => true, diff --git a/db/migrate/20111101222453_acts_as_taggable_on_migration.rb b/db/migrate/20111101222453_acts_as_taggable_on_migration.rb new file mode 100644 index 0000000000000000000000000000000000000000..16610615f9d6b9bc5ed5b5c78375391f7eb448a8 --- /dev/null +++ b/db/migrate/20111101222453_acts_as_taggable_on_migration.rb @@ -0,0 +1,28 @@ +class ActsAsTaggableOnMigration < ActiveRecord::Migration + def self.up + create_table :tags do |t| + t.string :name + end + + create_table :taggings do |t| + t.references :tag + + # You should make sure that the column created is + # long enough to store the required class names. + t.references :taggable, :polymorphic => true + t.references :tagger, :polymorphic => true + + t.string :context + + t.datetime :created_at + end + + add_index :taggings, :tag_id + add_index :taggings, [:taggable_id, :taggable_type, :context] + end + + def self.down + drop_table :taggings + drop_table :tags + end +end diff --git a/vendor/assets/javascripts/jquery.tagify.js b/vendor/assets/javascripts/jquery.tagify.js new file mode 100644 index 0000000000000000000000000000000000000000..f22d4c711917d5da6a6a3ab74a85dd8547a8be19 --- /dev/null +++ b/vendor/assets/javascripts/jquery.tagify.js @@ -0,0 +1,143 @@ +/* Author: Alicia Liu */ + +(function ($) { + + $.widget("ui.tagify", { + options: { + delimiters: [13, 188], // what user can type to complete a tag in char codes: [enter], [comma] + outputDelimiter: ',', // delimiter for tags in original input field + cssClass: 'tagify-container', // CSS class to style the tagify div and tags, see stylesheet + addTagPrompt: 'add tags' // placeholder text + }, + + _create: function() { + var self = this, + el = self.element, + opts = self.options; + + this.tags = []; + + // hide text field and replace with a div that contains it's own input field for entering tags + this.tagInput = $("") + .attr( 'placeholder', opts.addTagPrompt ) + .keypress( function(e) { + var $this = $(this), + pressed = e.which; + + for ( i in opts.delimiters ) { + + if (pressed == opts.delimiters[i]) { + self.add( $this.val() ); + e.preventDefault(); + return false; + } + } + }) + // for some reason, in Safari, backspace is only recognized on keyup + .keyup( function(e) { + var $this = $(this), + pressed = e.which; + + // if backspace is hit with no input, remove the last tag + if (pressed == 8) { // backspace + if ( $this.val() == "" ) { + self.remove(); + return false; + } + return; + } + }); + + this.tagDiv = $("
") + .addClass( opts.cssClass ) + .click( function() { + $(this).children('input').focus(); + }) + .append( this.tagInput ) + .insertAfter( el.hide() ); + + // if the field isn't empty, parse the field for tags, and prepopulate existing tags + var initVal = $.trim( el.val() ); + + if ( initVal ) { + var initTags = initVal.split( opts.outputDelimiter ); + $.each( initTags, function(i, tag) { + self.add( tag ); + }); + } + }, + + _setOption: function( key, value ) { + options.key = value; + }, + + // add a tag, public function + add: function(text) { + var self = this; + text = text || self.tagInput.val(); + if (text) { + var tagIndex = self.tags.length; + + var removeButton = $("x") + .click( function() { + self.remove( tagIndex ); + return false; + }); + var newTag = $("") + .text( text ) + .append( removeButton ); + + self.tagInput.before( newTag ); + self.tags.push( text ); + self.tagInput.val(''); + } + }, + + // remove a tag by index, public function + // if index is blank, remove the last tag + remove: function( tagIndex ) { + var self = this; + if ( tagIndex == null || tagIndex === (self.tags.length - 1) ) { + this.tagDiv.children("span").last().remove(); + self.tags.pop(); + } + if ( typeof(tagIndex) == 'number' ) { + // otherwise just hide this tag, and we don't mess up the index + this.tagDiv.children( "span:eq(" + tagIndex + ")" ).hide(); + // we rely on the serialize function to remove null values + delete( self.tags[tagIndex] ); + } + }, + + // serialize the tags with the given delimiter, and write it back into the tagified field + serialize: function() { + var self = this; + var delim = self.options.outputDelimiter; + var tagsStr = self.tags.join( delim ); + + // our tags might have deleted entries, remove them here + var dupes = new RegExp(delim + delim + '+', 'g'); // regex: /,,+/g + var ends = new RegExp('^' + delim + '|' + delim + '$', 'g'); // regex: /^,|,$/g + var outputStr = tagsStr.replace( dupes, delim ).replace(ends, ''); + + self.element.val(outputStr); + return outputStr; + }, + + inputField: function() { + return this.tagInput; + }, + + containerDiv: function() { + return this.tagDiv; + }, + + // remove the div, and show original input + destroy: function() { + $.Widget.prototype.destroy.apply(this); + this.tagDiv.remove(); + this.element.show(); + } + }); + +})(jQuery); \ No newline at end of file diff --git a/vendor/assets/stylesheets/jquery-ui/jquery.tagify.css b/vendor/assets/stylesheets/jquery-ui/jquery.tagify.css new file mode 100644 index 0000000000000000000000000000000000000000..d6c178f7132ae045bcf575cac3c70fef6924a958 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-ui/jquery.tagify.css @@ -0,0 +1,34 @@ +/* Tagify styles +Author: Alicia Liu test +*/ + +.tagify-container { +} + +.tagify-container > span { + display: inline-block; + padding: 8px 11px 8px 11px; + margin: 1px 5px 0px 0px; + border-radius: 4px; + border: 1px solid #d0e1ff; + background-color: #d0e1ff; + color: #0f326d; + font-weight: bold; + font-size: 14px; +} + +.tagify-container > span > a { + padding-left: 5px !important; + color: #83a5e1; + text-decoration: none; + font-weight: bold; +} + +.tagify-container > input { + border: 0 none; + width: 100px !important; +} + +.tagify-container > input:focus { + outline: none; +} \ No newline at end of file