e9_tags 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in e9_tags.gemspec
4
+ gemspec
data/README.rdoc ADDED
@@ -0,0 +1,61 @@
1
+ == E9Tags
2
+
3
+ An extension to ActsAsTaggableOn[http://github.com/mbleigh/acts-as-taggable-on] which "improves" on custom tagging, or at least makes it more dynamic. Additionally it
4
+ provides some autocomplete rack apps and the corresponding javascript.
5
+
6
+ == Installation
7
+
8
+ 1. E9Tags requires jquery and jquery-ui for the autocompletion and tag-adding form, be sure they're loaded in your
9
+ pages where the tags form will be rendered.
10
+
11
+ 2. E9Tags extends ActsAsTaggableOn and requires it. Run it's generator if you have not.
12
+
13
+ 3. Run the E9Tags install script to copy over the required JS
14
+
15
+ rails g e9_tags:install
16
+
17
+ 4. Then make sure it is loaded, how you do that doesn't matter, e.g.
18
+
19
+ <%= javascript_include_tag 'e9_tags' %>
20
+
21
+ 5. Create an initializer for that sets up the taggable models and their controllers. This gives the models the tag
22
+ associations and methods and prepares their controller to handle the otherwise unexpected tag params.
23
+
24
+ require 'e9_tags'
25
+ require 'contacts_controller'
26
+ require 'contact'
27
+
28
+ E9Tags.controllers << ContactsController
29
+ E9Tags.models << Contact
30
+
31
+ OR
32
+
33
+ You can just include the modules in your classes yourself. The first way really exists for the case where the
34
+ classes you wish to extend are part of another plugin/gem.
35
+
36
+ # in contact.rb
37
+ include E9Tags:Model
38
+
39
+ # in contacts_controller.rb
40
+ include E9Tags::Controller
41
+
42
+ 6. Render the tags form partial in whatever model forms require it.
43
+
44
+ = render 'e9_tags/form', :f => f
45
+
46
+ If you pass a context, it will be locked and no longer possible to change/add the contexts on the form (and as
47
+ a side effect, the tags autocompletion will be restricted to that context).
48
+
49
+ = render 'e9_tags/form', :f => f, :context => :users
50
+
51
+ Finally if you pass a 2nd arg to :context you can set a tag context to be "private" (default is false). In this
52
+ case the tag context will be locked as private (typically suffixed with *), meaning that the tags will not be
53
+ publicly searchable/visible. This is useful for organizational tags tags, say if you wanted to arbitrarily
54
+ group records, or create a custom search based on a tag context.
55
+
56
+ = render 'e9_tags/form', :f => f, :context => [:users, true]
57
+
58
+ NOTE: The form and javascript are intended to work out of the box, but the certainly aren't going to look pretty.
59
+ If you do intend to use the forms, you'll no doubt need to style them.
60
+
61
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,31 @@
1
+ <!--BEGIN e9_tags_js-->
2
+ :javascript
3
+ var tag_template = "#{escape_javascript(tag_template(f.object).html_safe)}";
4
+ <!--END e9_tags_js-->
5
+
6
+ %fieldset.tags
7
+ %legend= label_tag 'add-tag-fld', t(:tags_name, :scope => :e9_tags)
8
+ #tag-fields
9
+ - if local_assigns[:context]
10
+ = hidden_field_tag 'tag-context', humanize_context(*local_assigns[:context])
11
+ - else
12
+ .field
13
+ = label_tag 'tag-context', t(:tag_context_name, :scope => :e9_tags).singularize
14
+ = text_field_tag 'tag-context', nil
15
+ .field
16
+ = label_tag 'add-tag-fld', t(:tags_name, :scope => :e9_tags).singularize
17
+ = text_field_tag 'add-tag-fld', nil
18
+ .actions
19
+ %button#add-tag-btn= t(:add_tag_link, :scope => :e9_tags)
20
+ -# NOTE this escaping bit doesn't work without a tooltips plugin (browser does not render HTML in default title tooltips)
21
+ - t = raw t(:tag_instructions, :scope => :e9_tags).gsub('"', '&quot;')
22
+ <button title="#{t}" rel="tooltip">?</button>
23
+
24
+ #tag-contexts{:class => local_assigns[:context] ? 'locked-context' : nil }
25
+ - f.object.tagging_contexts(:show_all => true).each do |context|
26
+ - h_context = humanize_context(context)
27
+ %div{:id => "#{context}-context-list", :class => 'context-list'}
28
+ %span.context-header= h_context unless local_assigns[:context]
29
+ %ul
30
+ - f.object.tag_list_on(context).each do |tag_word|
31
+ = tag_template(f.object, tag_word, h_context, context)
@@ -0,0 +1,3 @@
1
+ .tag-list
2
+ %span.heading #{ e9_t(:tags_name) }:
3
+ = tag_list(taggable)
@@ -0,0 +1,2 @@
1
+ .tag-list-with-context
2
+ = tag_list_with_context(taggable, local_assigns[:highlight])
@@ -0,0 +1,5 @@
1
+ %li
2
+ %span.admin-tag
3
+ %span= tag
4
+ = link_to t(:remove_tag), 'javascript:;', :class => 'delete-tag', :title => t(:delete_tag_link_instructions, :scope => :e9_tags)
5
+ = hidden_field_tag "#{class_name}[#{u_context}_tag_list][]", tag, :id => "#{class_name}_#{u_context}_tag_list"
@@ -0,0 +1,10 @@
1
+ en:
2
+ e9_tags:
3
+ tags_name: Tags
4
+ add_tag_link: Add
5
+ delete_tag: Delete
6
+ tag_context_name: Context
7
+ tag_context_visible: "Tags created with this context will be searchable and visible on their tagged content."
8
+ tag_context_hidden: "Tags created with this context will be unsearchable and will not be visible on their tagged content."
9
+ tag_instructions: "<p>Tags are words or phrases that describe or classify the content on this page.</p><p>Context allows you to contextualize each tag you enter. For a tag of \"coca cola\" you might give it a context of \"brand\", or you could give it a context of \"soda\".</p><p>Enter a tag, click Add. Repeat to add more.</p><p>Tags suffixed with a * are not publically visible or searchable, making them suitable for organizational purposes.</p>"
10
+ delete_tag_link_instructions: Click to remove this tag
data/config/routes.rb ADDED
@@ -0,0 +1,6 @@
1
+ Rails.application.routes.draw do
2
+ # TODO fix the fact that we route to apps, then the apps themselves check those paths
3
+ get '/js/tags' => E9Tags::Rack::TagsJs
4
+ get '/autocomplete/tags' => E9Tags::Rack::TagAutoCompleter
5
+ get '/autocomplete/tag-contexts' => E9Tags::Rack::TagContextAutoCompleter
6
+ end
data/e9_tags.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "e9_tags/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "e9_tags"
7
+ s.version = E9Tags::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Travis Cox"]
10
+ s.email = ["travis@e9digital.com"]
11
+ s.homepage = "http://github.com/e9digital/e9_tags"
12
+ s.summary = %q{Extension to ActsAsTaggableOn used in e9 Rails 3 projects}
13
+ s.description = File.open('README.rdoc').read rescue nil
14
+
15
+ s.rubyforge_project = "e9_tags"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency("rails", "~> 3.0.0")
23
+ s.add_dependency("acts-as-taggable-on", "~> 2.0.6")
24
+ end
@@ -0,0 +1,21 @@
1
+ module E9Tags
2
+ module Controller
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ prepend_before_filter :extract_tag_lists_from_params, :only => [:create, :update]
7
+ end
8
+
9
+ def extract_tag_lists_from_params
10
+ params[resource_instance_name] ||= {}
11
+
12
+ list_keys = params[resource_instance_name].keys.select {|k| k.to_s =~ /_tag_list$/ }
13
+ tags = params[resource_instance_name].slice(*list_keys)
14
+
15
+ params[resource_instance_name].except!(*list_keys)
16
+ params[resource_instance_name][:tag_lists] = tags
17
+ end
18
+
19
+ protected :extract_tag_lists_from_params
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ module E9Tags
2
+ module Helper
3
+ def tag_template(object, tag = "__TAG__", context = "__CONTEXT__", u_context = "__UCONTEXT__")
4
+ render('e9_tags/template', {
5
+ :tag => tag,
6
+ :context => context,
7
+ :u_context => u_context,
8
+ :class_name => object.class.name.underscore
9
+ })
10
+ end
11
+
12
+ def humanize_context(*context_and_is_private)
13
+ context, is_private = context_and_is_private
14
+
15
+ context = E9Tags.unescape_context(context)
16
+
17
+ if is_private && context !~ /#{E9Tags::PRIVATE_TAG_SUFFIX_REGEX}$/
18
+ context << E9Tags::PRIVATE_TAG_SUFFIX
19
+ end
20
+
21
+ context
22
+ end
23
+
24
+ def tag_list(resource, highlighted_tag = nil, options = {})
25
+ _tag_list(resource.tags(options), highlighted_tag) if resource.respond_to?(:tags)
26
+ end
27
+
28
+ def tag_list_with_context(resource, highlighted_tag = nil, options = {})
29
+ if resource.respond_to?(:tagging_contexts)
30
+
31
+ # by default, we don't want to show all tags
32
+ options[:show_all] = false if options.blank?
33
+
34
+ content_tag(:div, :class => 'tag-lists') do
35
+ ''.html_safe.tap do |html|
36
+ resource.tagging_contexts(options).each do |context|
37
+ str = ''.html_safe
38
+ str.safe_concat content_tag(:div, "#{humanize_context(E9Tags.unescape_context(context))}:", :class => 'heading')
39
+ str.safe_concat _tag_list(resource.tag_list_on(context), highlighted_tag)
40
+
41
+ html.safe_concat content_tag(:div, str.html_safe, :class => 'tag-context-list')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # NOTE this subroutine is kinda useless, should be rewritten to handle both tag list types
51
+ def _tag_list(tags, highlighted_tag = nil)
52
+ content_tag(:div, :class => 'tag-list') do
53
+ tags.each_with_index.map do |tag, index|
54
+ css_class = index == tags.length - 1 ? 'last' : nil
55
+ link_class = 'tag'
56
+ link_class << ' highlight' if highlighted_tag && highlighted_tag == tag
57
+ link_class << ' last' if index == tags.length - 1
58
+ link_to(tag, searches_path(:query => tag), :class => link_class)
59
+ end.join(', ').html_safe
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,151 @@
1
+ module E9Tags
2
+ module Model
3
+ extend ActiveSupport::Concern
4
+
5
+ #
6
+ # It should be noted that our usage of tag contexts is apparently
7
+ # incongruent with acts_as_taggable_on's intentions. It goes to lengths
8
+ # to write class methods for each context with the apparent intention that
9
+ # these be used separately, whereas we needed contexts to be more dynamic
10
+ # (all contexts but "tags" are user defined).
11
+ #
12
+ # The gem provides custom contexts but they're not very well supported
13
+ # and somewhat broken (see #tagging_contexts below).
14
+ #
15
+ # Because of the way AATO expects contexts to be used, it makes some
16
+ # assumptions, and writes a bunch of helper methods to the class for each.
17
+ # These are useless to us really and only serve to add some confusion to
18
+ # the scope. E.g. for acts_as_taggable_on :tags (the default), the
19
+ # following methods are created:
20
+ #
21
+ # tag_taggings (has_many)
22
+ # tags (has_many)
23
+ # tags_list
24
+ # tags_list=
25
+ # all_tags_list
26
+ #
27
+ # It's important to note that these methods all only deal with the "tags"
28
+ # context and won't return any tag which does not have it.
29
+ #
30
+ # The actual associations on the taggable record are "taggings" and
31
+ # "base_tags", which ignores context, returning all tags.
32
+ #
33
+
34
+ def has_tags?
35
+ !base_tags.empty?
36
+ end
37
+
38
+ # Override ActsAsTaggableOn's broken handling of tagging_contexts.
39
+ #
40
+ # This method by default reads:
41
+ #
42
+ # def tagging_contexts
43
+ # custom_contexts + self.class.tag_types.map(&:to_s)
44
+ # end
45
+ #
46
+ # ...where custom_contexts is an non-persistent instance variable
47
+ # on the record and tag_types are the tagging contexts defined
48
+ # on the class itself, e.g. acts_as_taggable_on :some_context.
49
+ #
50
+ # This means that proper custom contexts aren't reflected in
51
+ # tagging_contexts on retrieved records.
52
+ #
53
+ # For this reason we override/monkeypatch a few methods
54
+ # after including acts_as_taggable, making them more useful. The concept
55
+ # of "hidden" tags is also added, with the default "tagging_contexts" association
56
+ # overridden to return only "visible" tags. "Hidden" tags are denoted
57
+ # by having a context ending in __H__, a contrivance created in the javascript
58
+ # that handles adding/reading tags.
59
+ #
60
+ # Additionally contexts are further escaped by subsituting dashes and spaces with
61
+ # __D__ and __S__, respectively. This allows for those character in custom contexts,
62
+ # which is otherwise impossible due to the way that AATO caches contexts on the
63
+ # taggable object by dynamically writing context-named instance variables.
64
+ #
65
+
66
+ #
67
+ # this won't take unless the record is saved successfully
68
+ #
69
+ def clear_all_tags
70
+ tagging_contexts.each {|context| set_tag_list_on(context, '') }
71
+ end
72
+
73
+ def tag_lists=(hash)
74
+ self.clear_all_tags
75
+ hash.each do |context, tags|
76
+ c = context.to_s.sub(/_tag_list$/, '')
77
+ c = E9Tags.escape_context(c)
78
+
79
+ self.set_tag_list_on(c, tags)
80
+ end
81
+ end
82
+
83
+ module DelayedClassMethods
84
+ def tagged_with(tags, options = {})
85
+ retv = super
86
+
87
+ # for whatever reason, tagged_with by default returns an empty Hash (??) if there are no results
88
+ return where("1=0") if retv.blank?
89
+
90
+ # if :show_all is true, just return super with no modification
91
+ #
92
+ if !options[:show_all] && retv.joins_values.present? && retv.to_sql =~ /JOIN taggings (\S+)/
93
+ #
94
+ # otherwise we'll show hidden OR visible tags based on whether :show_hidden was passed
95
+ #
96
+ retv = retv.where(
97
+ %Q[#{$1}.context #{options[:show_hidden] ? '' : ' NOT '} like '%__H__']
98
+ )
99
+ end
100
+
101
+ retv
102
+ end
103
+ end
104
+
105
+ included do
106
+ acts_as_taggable
107
+
108
+ extend DelayedClassMethods
109
+
110
+ # TODO Tagged_with unfortunately does not handle context-only searches. Rewriting it to do so would save this unnecessary subquery
111
+ scope :tagged_with_context, lambda {|context|
112
+ where(:id => select(Arel::Distinct.new(arel_table[:id])).joins(:taggings).where(Tagging.arel_table[:context].eq(E9Tags.escape_context(context))).map(&:id))
113
+ }
114
+
115
+ def filtered_taggings(*args)
116
+ options = args.extract_options!
117
+
118
+ retv = taggings(*args << options.slice!(:show_all, :show_hidden))
119
+
120
+ if !options[:show_all]
121
+ retv = retv.where(
122
+ %Q[taggings.context #{options[:show_hidden] ? '' : ' NOT '} like '%__H__']
123
+ )
124
+ end
125
+
126
+ retv
127
+ end
128
+
129
+ def tags(options = {})
130
+ filtered_taggings(options).map {|tagging| tagging.tag.name if tagging }.compact.uniq
131
+ end
132
+
133
+ #
134
+ # rewrite tagging_contexts as mentioned above
135
+ #
136
+ # NOTE unlike tags & taggings, tagging_contexts returns all contexts
137
+ # This is crucial because AATO uses tagging_contexts internally
138
+ # and it needs to behave like the default method
139
+ #
140
+ def tagging_contexts(options = {})
141
+
142
+ # NOTE this takes options but it only affects taggings.
143
+ # if there are custom_contexts that are hidden they will
144
+ # be returned
145
+ options[:show_all] = true if options.blank?
146
+
147
+ (custom_contexts + filtered_taggings(options).map(&:context) + self.class.tag_types.map(&:to_s)).uniq
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,39 @@
1
+ module E9Tags::Rack
2
+ class TagAutoCompleter
3
+ DEFAULT_LIMIT = 10
4
+
5
+ def self.call(env)
6
+ if env["PATH_INFO"] =~ /^\/autocomplete\/tags/
7
+ terms = []
8
+ @params = Rack::Request.new(env).params
9
+
10
+ if @term = @params['term']
11
+ tags, taggings = ::Tag.arel_table, ::Tagging.arel_table
12
+
13
+ relation = tags.join(taggings).on(tags[:id].eq(taggings[:tag_id])).
14
+ where(tags[:name].matches("#{@term}%")).
15
+ group(tags[:id]).
16
+ take(@params['limit'] ? @params['limit'].to_i : DEFAULT_LIMIT).
17
+ project(tags[:name], tags[:name].count.as('count')).
18
+ #having('count > 0'). # having count > 0 is unnecessary because of the join type
19
+ order('name ASC')
20
+
21
+ if @context = @params['context']
22
+ relation = relation.where(taggings[:context].matches(E9Tags.escape_context(@context)))
23
+ end
24
+
25
+ # NOTE this select is stolen from Arel::SelectManager's deprecated to_a method, but since Arel has been re-written
26
+ # (and even before that) it'd probably be smarter here to avoid arel tables and just use AR and to_json
27
+ #
28
+ terms = ::ActiveRecord::Base.connection.send(:select, relation.to_sql, 'Tag Autocomplete').map do |row|
29
+ { :label => "#{row['name']} - #{row['count']}", :value => row['name'], :count => row['count'] }
30
+ end
31
+ end
32
+
33
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "max-age=3600, must-revalidate"}, [terms.to_json]]
34
+ else
35
+ [404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ module E9Tags::Rack
2
+ class TagContextAutoCompleter
3
+ DEFAULT_LIMIT = 10
4
+
5
+ def self.call(env)
6
+ if env["PATH_INFO"] =~ /^\/autocomplete\/tag\-contexts/
7
+ terms = []
8
+
9
+ if @term = Rack::Request.new(env).params['term']
10
+
11
+ taggings = ::Tagging.arel_table
12
+
13
+ relation = taggings.
14
+ group(taggings[:context]).
15
+ where(taggings[:context].matches("#{@term}%")).
16
+ project(taggings[:context], taggings[:context].count.as('count')).
17
+ take(DEFAULT_LIMIT).
18
+ order('context ASC')
19
+
20
+ # NOTE this select is stolen from Arel::SelectManager's deprecated to_a method, but since Arel has been re-written
21
+ # (and even before that) it'd probably be smarter here to avoid arel tables and just use AR and to_json
22
+ #
23
+ terms = ::ActiveRecord::Base.send(:select, relation.to_sql, 'Tag Context Autocomplete').map do |row|
24
+ unescaped_context = E9Tags.unescape_context(row['context'])
25
+
26
+ { :label => "#{unescaped_context} - #{row['count']}", :value => unescaped_context, :count => row['count'] }
27
+ end
28
+ end
29
+
30
+ [200, {"Content-Type" => "application/json", "Cache-Control" => "max-age=3600, must-revalidate"}, [terms.to_json]]
31
+ else
32
+ [404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,14 @@
1
+ module E9Tags::Rack
2
+ class TagsJs
3
+ def self.call(env)
4
+ if env["PATH_INFO"] =~ /^\/js\/tags/
5
+ tags = ::Tagging.joins(:tag).order('tags.name').group_by(&:context).to_json
6
+ js = "window.e9=window.e9||{};window.e9.tags=#{tags};"
7
+
8
+ [200, {"Content-Type" => "text/javascript", "Cache-Control" => "max-age=3600, must-revalidate"}, [js]]
9
+ else
10
+ [404, {"Content-Type" => "text/html", "X-Cascade" => "pass"}, ["Not Found"]]
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_support/ordered_hash'
2
+
3
+ module ActiveSupport
4
+ class OrderedHash
5
+ alias :zero? :blank?
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module E9Tags
2
+ module TaggingExtension
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias :content :taggable
7
+
8
+ delegate :name, :to => :tag
9
+
10
+ scope :hidden, lambda {
11
+ where(arel_table[:contexts].matches('%__H__').not)
12
+ }
13
+
14
+ scope :excluding_tags, lambda {|*tags|
15
+ joins(:tag).where(Tag.arel_table[:name].in(tags.flatten).not)
16
+ }
17
+
18
+ scope :including_tags, lambda {|*tags|
19
+ joins(:tag).where(Tag.arel_table[:name].in(tags.flatten))
20
+ }
21
+
22
+ def as_json(options = {})
23
+ tag.try(:name)
24
+ end
25
+ end
26
+
27
+ module ClassMethods
28
+ def contexts(options = {})
29
+ scope = arel_table.project(:context)
30
+
31
+ if !options[:show_all]
32
+ condition = arel_table[:context].matches('%__H__')
33
+ condition = condition.not unless options[:show_hidden]
34
+ scope = scope.where(condition)
35
+ end
36
+
37
+ scope.map {|row| row.tuple.first }.uniq
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module E9Tags
2
+ VERSION = "0.0.8"
3
+ end
data/lib/e9_tags.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'rails'
2
+ require 'acts-as-taggable-on'
3
+ require 'e9_tags/tagging_extension'
4
+ require 'e9_tags/rails_extensions'
5
+
6
+ module E9Tags
7
+ autoload :Controller, 'e9_tags/controller'
8
+ autoload :Model, 'e9_tags/model'
9
+ autoload :Helper, 'e9_tags/helper'
10
+
11
+ module Rack
12
+ autoload :TagAutoCompleter, 'e9_tags/rack/tag_auto_completer'
13
+ autoload :TagContextAutoCompleter, 'e9_tags/rack/tag_context_auto_completer'
14
+ autoload :TagsJs, 'e9_tags/rack/tags_js'
15
+ end
16
+
17
+ #
18
+ # Controllers that are prepared to handle taggable models
19
+ #
20
+ mattr_accessor :controllers
21
+ @@controllers = []
22
+
23
+ #
24
+ # Models that are taggable
25
+ #
26
+ mattr_accessor :models
27
+ @@models = []
28
+
29
+ ESCAPED_DASH = '__d__'
30
+ ESCAPED_DASH_REGEX = Regexp.new(Regexp.escape(ESCAPED_DASH), true)
31
+
32
+ ESCAPED_SPACE = '__s__'
33
+ ESCAPED_SPACE_REGEX = Regexp.new(Regexp.escape(ESCAPED_SPACE), true)
34
+
35
+ ESCAPED_PRIVATE = '__h__'
36
+ ESCAPED_PRIVATE_REGEX = Regexp.new(Regexp.escape(ESCAPED_PRIVATE), true)
37
+
38
+ PRIVATE_TAG_SUFFIX = '*'
39
+ PRIVATE_TAG_SUFFIX_REGEX = Regexp.new(Regexp.escape(PRIVATE_TAG_SUFFIX), true)
40
+
41
+ def E9Tags.escape_context(context)
42
+ context.to_s.strip.
43
+ downcase.
44
+ gsub(/\s+/, ESCAPED_SPACE).
45
+ gsub(/-/, ESCAPED_DASH).
46
+ sub(PRIVATE_TAG_SUFFIX_REGEX, ESCAPED_PRIVATE)
47
+ end
48
+
49
+ def E9Tags.unescape_context(context)
50
+ context.to_s.strip.
51
+ gsub(ESCAPED_SPACE_REGEX, ' ').
52
+ gsub(ESCAPED_DASH_REGEX, '-').
53
+ sub(ESCAPED_PRIVATE_REGEX, PRIVATE_TAG_SUFFIX).
54
+ downcase.
55
+ titleize
56
+ end
57
+
58
+ def E9Tags.setup!
59
+ ActsAsTaggableOn::Tagging.send(:include, E9Tags::TaggingExtension)
60
+
61
+ E9Tags.models.each {|m| m.send(:include, E9Tags::Model) }
62
+ E9Tags.controllers.each {|m| m.send(:include, E9Tags::Controller) }
63
+ end
64
+
65
+ class Engine < ::Rails::Engine
66
+ config.e9_tags = E9Tags
67
+
68
+ config.to_prepare { E9Tags.setup! }
69
+
70
+ initializer 'e9_tags.include_helper' do
71
+ ActiveSupport.on_load(:action_view) do
72
+ include E9Tags::Helper
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ # Alias ActsAsTaggableOn classes
79
+ Tag = ActsAsTaggableOn::Tag
80
+ Tagging = ActsAsTaggableOn::Tagging
@@ -0,0 +1,13 @@
1
+ require 'rails/generators/base'
2
+
3
+ module E9Tags
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def install_javascript
9
+ copy_file 'e9_tags.js', 'public/javascripts/e9_tags.js'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,211 @@
1
+ jQuery(function($) {
2
+ var
3
+ $tag_context = $('#tag-context'),
4
+ tcv = $.trim($tag_context.val()),
5
+
6
+ /*
7
+ * If tag-context is blank, default it to Tags. This is important also in that
8
+ * if the tag-context is pre-set, it will considered hidden and not used as
9
+ * a label in the form.
10
+ */
11
+ default_context_value = tcv.length ? tcv : "Tags",
12
+
13
+ /*
14
+ * tag contexts in acts-as-taggable-on must be legal instance variable names.
15
+ * Replace spaces with __S__ and dashes with __D__ to solve the issue. As a side effect
16
+ * it safe to always use the escaped context names as dom ids
17
+ */
18
+ escape_context = function(context) {
19
+ return $.trim(context)
20
+ .toLowerCase()
21
+ .replace(/\s+/g, '__s__')
22
+ .replace(/-/, '__d__')
23
+ .replace(/\*/, '__h__');
24
+ },
25
+
26
+ unescape_context = function(context) {
27
+ return $.trim(context)
28
+ .replace(/__s__/g, ' ')
29
+ .replace(/__d__/g, '-')
30
+ .replace(/__h__/g, '*')
31
+ .toLowerCase()
32
+ .replace(/(^|\s)([a-z])/g, function(match, m1, m2) { return m1 + m2.toUpperCase(); });
33
+ };
34
+
35
+ // hide empty context-lists
36
+ $('.context-list ul').each(function(ele) {
37
+ var ct = $(this).find('li').length;
38
+ if(ct == 0) $(this).parent().hide();
39
+ });
40
+
41
+ var context_cache = {};
42
+ var has_clicked_context = false;
43
+
44
+ $tag_context
45
+ /*
46
+ * initial setting of context and context blur use this:
47
+ * set context value to default value if context is blank
48
+ */
49
+ .blur(function() {
50
+ $(this).val(function(i, cVal) {
51
+ return $.trim(cVal).length ? cVal : default_context_value
52
+ });
53
+ })
54
+
55
+ /*
56
+ * focus does the reverse and set's the value to blank if
57
+ * the context is the default context
58
+ */
59
+ .focus(function() {
60
+ $(this).val(function(i, cVal) {
61
+ return cVal == default_context_value ? "" : cVal;
62
+ });
63
+ })
64
+
65
+ .autocomplete({
66
+ delay: 350,
67
+ source: function(request, response) {
68
+ var request_str = "term=" + request.term;
69
+ //if (context_cache.term == request.term && context_cache.content) {
70
+ //response(context_cache.content);
71
+ //return;
72
+ //}
73
+ //if (new RegExp(context_cache.term).test(request.term) && context_cache.content && context_cache.content.length < 13) {
74
+ //response($.ui.autocomplete.filter(context_cache.content, request.term));
75
+ //return;
76
+ //}
77
+ $.ajax({
78
+ url: "/autocomplete/tag-contexts",
79
+ dataType: "json",
80
+ data: request_str,
81
+ success: function(data) {
82
+ context_cache.term = request.term;
83
+ context_cache.content = data;
84
+ response(data);
85
+ }
86
+ });
87
+ },
88
+ focus: function(evt, ui) {
89
+ $("#tag-context").val(ui.item.label);
90
+ return false;
91
+ }
92
+ })
93
+
94
+ // blur on load (set to default if blank)
95
+ .blur()
96
+ ;
97
+
98
+ var $addtf = $("#add-tag-fld"), cache = {};
99
+
100
+ $addtf.autocomplete({
101
+ delay: 350,
102
+ source: function(request, response) {
103
+ var request_str = "term=" + request.term;
104
+ var context = $("#tag-context-select").val() || $("#tag-context").val();
105
+
106
+ if (context != undefined && context != '') {
107
+ request_str += "&context=" + context;
108
+ }
109
+
110
+ // TODO Caching tag autocomplete with context
111
+ //if (cache.term == request.term && cache.content) {
112
+ //response(cache.content);
113
+ //return;
114
+ //}
115
+ //if (new RegExp(cache.term).test(request.term) && cache.content && cache.content.length < 13) {
116
+ //response($.ui.autocomplete.filter(cache.content, request.term));
117
+ //return;
118
+ //}
119
+
120
+ $.ajax({
121
+ url: "/autocomplete/tags",
122
+ dataType: "json",
123
+ data: request_str,
124
+ success: function(data) {
125
+ //cache.term = request.term;
126
+ //cache.content = data;
127
+ response(data);
128
+ }
129
+ });
130
+ },
131
+ focus: function(evt, ui) {
132
+ $addtf.val(ui.item.label);
133
+ return false;
134
+ }
135
+ });
136
+
137
+ // some variation of this one is probably better as it leaves a blank field in the form?
138
+ $(".admin-tag").live("click", function(e) {
139
+ e.preventDefault();
140
+ var $li = $(this).closest('li');
141
+ if (!$li.siblings().length) $li.closest('.context-list').hide();
142
+ $li.remove();
143
+ });
144
+
145
+ $("#tag-context-select").change(function(e) {
146
+ $addtf.autocomplete('search');
147
+ });
148
+
149
+ $("#add-tag-btn").click(function(evt) {
150
+ evt.preventDefault();
151
+
152
+ if (!$.trim($addtf.val()).length) return;
153
+
154
+ var
155
+ $tcf = $("#tag-context"),
156
+ regex = /^[a-zA-Z][a-zA-Z0-9- ]*\*?$/,
157
+ message = " must begin with a letter and contain only letters, numbers, spaces, and hyphens",
158
+ tag,
159
+ context,
160
+ u_context;
161
+
162
+ tag = $addtf.val();
163
+ if (!tag.match(regex)) {
164
+ alert("Tags" + message);
165
+ return;
166
+ }
167
+
168
+ context = $.trim($tcf.val());
169
+ if (!context.match(regex)) {
170
+ alert("Tag contexts" + message);
171
+ return;
172
+ }
173
+
174
+ context = unescape_context(context);
175
+ u_context = escape_context(context);
176
+
177
+ var list = $("#"+u_context+"-context-list ul");
178
+ if(list.length == 0) {
179
+ var
180
+ html = '<div id="'+ u_context +'-context-list" class="context-list">';
181
+
182
+ // if the tag context value was not preset then we use the humanized
183
+ // context value as a label for the separate context lists
184
+ if (!tcv.length) {
185
+ html += '<span>'+context+'</span>';
186
+ }
187
+
188
+ html += '<ul></ul></div>';
189
+
190
+ $('#tag-contexts').append(html);
191
+ }
192
+
193
+ list = $("#"+u_context+"-context-list ul");
194
+
195
+ if ($.trim(list.find("input[value='"+tag+"']").val()).length) {
196
+ alert("That label exists for this content. You can't create a duplicate record.");
197
+ return;
198
+ }
199
+
200
+ var ele = tag_template;
201
+ ele = ele.replace(/__TAG__/g, tag);
202
+ ele = ele.replace(/__CONTEXT__/g, context);
203
+ ele = ele.replace(/__UCONTEXT__/g, u_context);
204
+
205
+ list.append(ele).parent().show();
206
+
207
+ $addtf.val('');
208
+ $tcf.val('').blur();
209
+ });
210
+
211
+ });
metadata ADDED
@@ -0,0 +1,178 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: e9_tags
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 8
9
+ version: 0.0.8
10
+ platform: ruby
11
+ authors:
12
+ - Travis Cox
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-04-15 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rails
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 3
30
+ - 0
31
+ - 0
32
+ version: 3.0.0
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: acts-as-taggable-on
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 2
45
+ - 0
46
+ - 6
47
+ version: 2.0.6
48
+ type: :runtime
49
+ version_requirements: *id002
50
+ description: |
51
+ == E9Tags
52
+
53
+ An extension to ActsAsTaggableOn[http://github.com/mbleigh/acts-as-taggable-on] which "improves" on custom tagging, or at least makes it more dynamic. Additionally it
54
+ provides some autocomplete rack apps and the corresponding javascript.
55
+
56
+ == Installation
57
+
58
+ 1. E9Tags requires jquery and jquery-ui for the autocompletion and tag-adding form, be sure they're loaded in your
59
+ pages where the tags form will be rendered.
60
+
61
+ 2. E9Tags extends ActsAsTaggableOn and requires it. Run it's generator if you have not.
62
+
63
+ 3. Run the E9Tags install script to copy over the required JS
64
+
65
+ rails g e9_tags:install
66
+
67
+ 4. Then make sure it is loaded, how you do that doesn't matter, e.g.
68
+
69
+ <%= javascript_include_tag 'e9_tags' %>
70
+
71
+ 5. Create an initializer for that sets up the taggable models and their controllers. This gives the models the tag
72
+ associations and methods and prepares their controller to handle the otherwise unexpected tag params.
73
+
74
+ require 'e9_tags'
75
+ require 'contacts_controller'
76
+ require 'contact'
77
+
78
+ E9Tags.controllers << ContactsController
79
+ E9Tags.models << Contact
80
+
81
+ OR
82
+
83
+ You can just include the modules in your classes yourself. The first way really exists for the case where the
84
+ classes you wish to extend are part of another plugin/gem.
85
+
86
+ # in contact.rb
87
+ include E9Tags:Model
88
+
89
+ # in contacts_controller.rb
90
+ include E9Tags::Controller
91
+
92
+ 6. Render the tags form partial in whatever model forms require it.
93
+
94
+ = render 'e9_tags/form', :f => f
95
+
96
+ If you pass a context, it will be locked and no longer possible to change/add the contexts on the form (and as
97
+ a side effect, the tags autocompletion will be restricted to that context).
98
+
99
+ = render 'e9_tags/form', :f => f, :context => :users
100
+
101
+ Finally if you pass a 2nd arg to :context you can set a tag context to be "private" (default is false). In this
102
+ case the tag context will be locked as private (typically suffixed with *), meaning that the tags will not be
103
+ publicly searchable/visible. This is useful for organizational tags tags, say if you wanted to arbitrarily
104
+ group records, or create a custom search based on a tag context.
105
+
106
+ = render 'e9_tags/form', :f => f, :context => [:users, true]
107
+
108
+ NOTE: The form and javascript are intended to work out of the box, but the certainly aren't going to look pretty.
109
+ If you do intend to use the forms, you'll no doubt need to style them.
110
+
111
+
112
+
113
+ email:
114
+ - travis@e9digital.com
115
+ executables: []
116
+
117
+ extensions: []
118
+
119
+ extra_rdoc_files: []
120
+
121
+ files:
122
+ - .gitignore
123
+ - Gemfile
124
+ - README.rdoc
125
+ - Rakefile
126
+ - app/views/e9_tags/_form.html.haml
127
+ - app/views/e9_tags/_tag_list.html.haml
128
+ - app/views/e9_tags/_tag_list_with_context.html.haml
129
+ - app/views/e9_tags/_template.html.haml
130
+ - config/locales/en.yml
131
+ - config/routes.rb
132
+ - e9_tags.gemspec
133
+ - lib/e9_tags.rb
134
+ - lib/e9_tags/controller.rb
135
+ - lib/e9_tags/helper.rb
136
+ - lib/e9_tags/model.rb
137
+ - lib/e9_tags/rack/tag_auto_completer.rb
138
+ - lib/e9_tags/rack/tag_context_auto_completer.rb
139
+ - lib/e9_tags/rack/tags_js.rb
140
+ - lib/e9_tags/rails_extensions.rb
141
+ - lib/e9_tags/tagging_extension.rb
142
+ - lib/e9_tags/version.rb
143
+ - lib/generators/e9_tags/install_generator.rb
144
+ - lib/generators/e9_tags/templates/e9_tags.js
145
+ has_rdoc: true
146
+ homepage: http://github.com/e9digital/e9_tags
147
+ licenses: []
148
+
149
+ post_install_message:
150
+ rdoc_options: []
151
+
152
+ require_paths:
153
+ - lib
154
+ required_ruby_version: !ruby/object:Gem::Requirement
155
+ none: false
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ segments:
160
+ - 0
161
+ version: "0"
162
+ required_rubygems_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ segments:
168
+ - 0
169
+ version: "0"
170
+ requirements: []
171
+
172
+ rubyforge_project: e9_tags
173
+ rubygems_version: 1.3.7
174
+ signing_key:
175
+ specification_version: 3
176
+ summary: Extension to ActsAsTaggableOn used in e9 Rails 3 projects
177
+ test_files: []
178
+