Commit 6977150f04be1bfe365e1cd86e3f8143c34a51bf
1 parent
5d526717
Exists in
master
and in
4 other branches
integrate tags plugins
Showing
6 changed files
with
212 additions
and
0 deletions
Show diff stats
Gemfile
| @@ -21,6 +21,8 @@ gem "git" | @@ -21,6 +21,8 @@ gem "git" | ||
| 21 | gem "acts_as_list" | 21 | gem "acts_as_list" |
| 22 | gem 'rdiscount' | 22 | gem 'rdiscount' |
| 23 | 23 | ||
| 24 | +gem 'acts-as-taggable-on', '~>2.1.0' | ||
| 25 | + | ||
| 24 | group :assets do | 26 | group :assets do |
| 25 | gem 'sass-rails', " ~> 3.1.0" | 27 | gem 'sass-rails', " ~> 3.1.0" |
| 26 | gem 'coffee-rails', "~> 3.1.0" | 28 | gem 'coffee-rails', "~> 3.1.0" |
Gemfile.lock
| @@ -54,6 +54,8 @@ GEM | @@ -54,6 +54,8 @@ GEM | ||
| 54 | activesupport (= 3.1.0) | 54 | activesupport (= 3.1.0) |
| 55 | activesupport (3.1.0) | 55 | activesupport (3.1.0) |
| 56 | multi_json (~> 1.0) | 56 | multi_json (~> 1.0) |
| 57 | + acts-as-taggable-on (2.1.1) | ||
| 58 | + rails | ||
| 57 | acts_as_list (0.1.4) | 59 | acts_as_list (0.1.4) |
| 58 | addressable (2.2.6) | 60 | addressable (2.2.6) |
| 59 | ansi (1.3.0) | 61 | ansi (1.3.0) |
| @@ -246,6 +248,7 @@ PLATFORMS | @@ -246,6 +248,7 @@ PLATFORMS | ||
| 246 | ruby | 248 | ruby |
| 247 | 249 | ||
| 248 | DEPENDENCIES | 250 | DEPENDENCIES |
| 251 | + acts-as-taggable-on (~> 2.1.0) | ||
| 249 | acts_as_list | 252 | acts_as_list |
| 250 | annotate! | 253 | annotate! |
| 251 | autotest | 254 | autotest |
app/models/project.rb
| @@ -9,6 +9,8 @@ class Project < ActiveRecord::Base | @@ -9,6 +9,8 @@ class Project < ActiveRecord::Base | ||
| 9 | has_many :notes, :dependent => :destroy | 9 | has_many :notes, :dependent => :destroy |
| 10 | has_many :snippets, :dependent => :destroy | 10 | has_many :snippets, :dependent => :destroy |
| 11 | 11 | ||
| 12 | + acts_as_taggable | ||
| 13 | + | ||
| 12 | validates :name, | 14 | validates :name, |
| 13 | :uniqueness => true, | 15 | :uniqueness => true, |
| 14 | :presence => true, | 16 | :presence => true, |
db/migrate/20111101222453_acts_as_taggable_on_migration.rb
0 → 100644
| @@ -0,0 +1,28 @@ | @@ -0,0 +1,28 @@ | ||
| 1 | +class ActsAsTaggableOnMigration < ActiveRecord::Migration | ||
| 2 | + def self.up | ||
| 3 | + create_table :tags do |t| | ||
| 4 | + t.string :name | ||
| 5 | + end | ||
| 6 | + | ||
| 7 | + create_table :taggings do |t| | ||
| 8 | + t.references :tag | ||
| 9 | + | ||
| 10 | + # You should make sure that the column created is | ||
| 11 | + # long enough to store the required class names. | ||
| 12 | + t.references :taggable, :polymorphic => true | ||
| 13 | + t.references :tagger, :polymorphic => true | ||
| 14 | + | ||
| 15 | + t.string :context | ||
| 16 | + | ||
| 17 | + t.datetime :created_at | ||
| 18 | + end | ||
| 19 | + | ||
| 20 | + add_index :taggings, :tag_id | ||
| 21 | + add_index :taggings, [:taggable_id, :taggable_type, :context] | ||
| 22 | + end | ||
| 23 | + | ||
| 24 | + def self.down | ||
| 25 | + drop_table :taggings | ||
| 26 | + drop_table :tags | ||
| 27 | + end | ||
| 28 | +end |
| @@ -0,0 +1,143 @@ | @@ -0,0 +1,143 @@ | ||
| 1 | +/* Author: Alicia Liu */ | ||
| 2 | + | ||
| 3 | +(function ($) { | ||
| 4 | + | ||
| 5 | + $.widget("ui.tagify", { | ||
| 6 | + options: { | ||
| 7 | + delimiters: [13, 188], // what user can type to complete a tag in char codes: [enter], [comma] | ||
| 8 | + outputDelimiter: ',', // delimiter for tags in original input field | ||
| 9 | + cssClass: 'tagify-container', // CSS class to style the tagify div and tags, see stylesheet | ||
| 10 | + addTagPrompt: 'add tags' // placeholder text | ||
| 11 | + }, | ||
| 12 | + | ||
| 13 | + _create: function() { | ||
| 14 | + var self = this, | ||
| 15 | + el = self.element, | ||
| 16 | + opts = self.options; | ||
| 17 | + | ||
| 18 | + this.tags = []; | ||
| 19 | + | ||
| 20 | + // hide text field and replace with a div that contains it's own input field for entering tags | ||
| 21 | + this.tagInput = $("<input type='text'>") | ||
| 22 | + .attr( 'placeholder', opts.addTagPrompt ) | ||
| 23 | + .keypress( function(e) { | ||
| 24 | + var $this = $(this), | ||
| 25 | + pressed = e.which; | ||
| 26 | + | ||
| 27 | + for ( i in opts.delimiters ) { | ||
| 28 | + | ||
| 29 | + if (pressed == opts.delimiters[i]) { | ||
| 30 | + self.add( $this.val() ); | ||
| 31 | + e.preventDefault(); | ||
| 32 | + return false; | ||
| 33 | + } | ||
| 34 | + } | ||
| 35 | + }) | ||
| 36 | + // for some reason, in Safari, backspace is only recognized on keyup | ||
| 37 | + .keyup( function(e) { | ||
| 38 | + var $this = $(this), | ||
| 39 | + pressed = e.which; | ||
| 40 | + | ||
| 41 | + // if backspace is hit with no input, remove the last tag | ||
| 42 | + if (pressed == 8) { // backspace | ||
| 43 | + if ( $this.val() == "" ) { | ||
| 44 | + self.remove(); | ||
| 45 | + return false; | ||
| 46 | + } | ||
| 47 | + return; | ||
| 48 | + } | ||
| 49 | + }); | ||
| 50 | + | ||
| 51 | + this.tagDiv = $("<div></div>") | ||
| 52 | + .addClass( opts.cssClass ) | ||
| 53 | + .click( function() { | ||
| 54 | + $(this).children('input').focus(); | ||
| 55 | + }) | ||
| 56 | + .append( this.tagInput ) | ||
| 57 | + .insertAfter( el.hide() ); | ||
| 58 | + | ||
| 59 | + // if the field isn't empty, parse the field for tags, and prepopulate existing tags | ||
| 60 | + var initVal = $.trim( el.val() ); | ||
| 61 | + | ||
| 62 | + if ( initVal ) { | ||
| 63 | + var initTags = initVal.split( opts.outputDelimiter ); | ||
| 64 | + $.each( initTags, function(i, tag) { | ||
| 65 | + self.add( tag ); | ||
| 66 | + }); | ||
| 67 | + } | ||
| 68 | + }, | ||
| 69 | + | ||
| 70 | + _setOption: function( key, value ) { | ||
| 71 | + options.key = value; | ||
| 72 | + }, | ||
| 73 | + | ||
| 74 | + // add a tag, public function | ||
| 75 | + add: function(text) { | ||
| 76 | + var self = this; | ||
| 77 | + text = text || self.tagInput.val(); | ||
| 78 | + if (text) { | ||
| 79 | + var tagIndex = self.tags.length; | ||
| 80 | + | ||
| 81 | + var removeButton = $("<a href='#'>x</a>") | ||
| 82 | + .click( function() { | ||
| 83 | + self.remove( tagIndex ); | ||
| 84 | + return false; | ||
| 85 | + }); | ||
| 86 | + var newTag = $("<span></span>") | ||
| 87 | + .text( text ) | ||
| 88 | + .append( removeButton ); | ||
| 89 | + | ||
| 90 | + self.tagInput.before( newTag ); | ||
| 91 | + self.tags.push( text ); | ||
| 92 | + self.tagInput.val(''); | ||
| 93 | + } | ||
| 94 | + }, | ||
| 95 | + | ||
| 96 | + // remove a tag by index, public function | ||
| 97 | + // if index is blank, remove the last tag | ||
| 98 | + remove: function( tagIndex ) { | ||
| 99 | + var self = this; | ||
| 100 | + if ( tagIndex == null || tagIndex === (self.tags.length - 1) ) { | ||
| 101 | + this.tagDiv.children("span").last().remove(); | ||
| 102 | + self.tags.pop(); | ||
| 103 | + } | ||
| 104 | + if ( typeof(tagIndex) == 'number' ) { | ||
| 105 | + // otherwise just hide this tag, and we don't mess up the index | ||
| 106 | + this.tagDiv.children( "span:eq(" + tagIndex + ")" ).hide(); | ||
| 107 | + // we rely on the serialize function to remove null values | ||
| 108 | + delete( self.tags[tagIndex] ); | ||
| 109 | + } | ||
| 110 | + }, | ||
| 111 | + | ||
| 112 | + // serialize the tags with the given delimiter, and write it back into the tagified field | ||
| 113 | + serialize: function() { | ||
| 114 | + var self = this; | ||
| 115 | + var delim = self.options.outputDelimiter; | ||
| 116 | + var tagsStr = self.tags.join( delim ); | ||
| 117 | + | ||
| 118 | + // our tags might have deleted entries, remove them here | ||
| 119 | + var dupes = new RegExp(delim + delim + '+', 'g'); // regex: /,,+/g | ||
| 120 | + var ends = new RegExp('^' + delim + '|' + delim + '$', 'g'); // regex: /^,|,$/g | ||
| 121 | + var outputStr = tagsStr.replace( dupes, delim ).replace(ends, ''); | ||
| 122 | + | ||
| 123 | + self.element.val(outputStr); | ||
| 124 | + return outputStr; | ||
| 125 | + }, | ||
| 126 | + | ||
| 127 | + inputField: function() { | ||
| 128 | + return this.tagInput; | ||
| 129 | + }, | ||
| 130 | + | ||
| 131 | + containerDiv: function() { | ||
| 132 | + return this.tagDiv; | ||
| 133 | + }, | ||
| 134 | + | ||
| 135 | + // remove the div, and show original input | ||
| 136 | + destroy: function() { | ||
| 137 | + $.Widget.prototype.destroy.apply(this); | ||
| 138 | + this.tagDiv.remove(); | ||
| 139 | + this.element.show(); | ||
| 140 | + } | ||
| 141 | + }); | ||
| 142 | + | ||
| 143 | +})(jQuery); | ||
| 0 | \ No newline at end of file | 144 | \ No newline at end of file |
| @@ -0,0 +1,34 @@ | @@ -0,0 +1,34 @@ | ||
| 1 | +/* Tagify styles | ||
| 2 | +Author: Alicia Liu test | ||
| 3 | +*/ | ||
| 4 | + | ||
| 5 | +.tagify-container { | ||
| 6 | +} | ||
| 7 | + | ||
| 8 | +.tagify-container > span { | ||
| 9 | + display: inline-block; | ||
| 10 | + padding: 8px 11px 8px 11px; | ||
| 11 | + margin: 1px 5px 0px 0px; | ||
| 12 | + border-radius: 4px; | ||
| 13 | + border: 1px solid #d0e1ff; | ||
| 14 | + background-color: #d0e1ff; | ||
| 15 | + color: #0f326d; | ||
| 16 | + font-weight: bold; | ||
| 17 | + font-size: 14px; | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +.tagify-container > span > a { | ||
| 21 | + padding-left: 5px !important; | ||
| 22 | + color: #83a5e1; | ||
| 23 | + text-decoration: none; | ||
| 24 | + font-weight: bold; | ||
| 25 | +} | ||
| 26 | + | ||
| 27 | +.tagify-container > input { | ||
| 28 | + border: 0 none; | ||
| 29 | + width: 100px !important; | ||
| 30 | +} | ||
| 31 | + | ||
| 32 | +.tagify-container > input:focus { | ||
| 33 | + outline: none; | ||
| 34 | +} | ||
| 0 | \ No newline at end of file | 35 | \ No newline at end of file |