e9_tags 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+