repeated_auto_complete 0.1.0

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.
Files changed (33) hide show
  1. data/.gitignore +21 -0
  2. data/README +102 -0
  3. data/Rakefile +52 -0
  4. data/VERSION +1 -0
  5. data/init.rb +3 -0
  6. data/lib/auto_complete.rb +49 -0
  7. data/lib/auto_complete_form_builder_helper.rb +38 -0
  8. data/lib/auto_complete_macros_helper.rb +152 -0
  9. data/lib/repeated_auto_complete.rb +4 -0
  10. data/lib/view_mapper/README +4 -0
  11. data/lib/view_mapper/has_many_auto_complete_view.rb +88 -0
  12. data/lib/view_mapper/templates/controller.rb +90 -0
  13. data/lib/view_mapper/templates/layout.html.erb +19 -0
  14. data/lib/view_mapper/templates/style.css +82 -0
  15. data/lib/view_mapper/templates/view_child_form.html.erb +16 -0
  16. data/lib/view_mapper/templates/view_form.html.erb +24 -0
  17. data/rails/init.rb +3 -0
  18. data/repeated_auto_complete.gemspec +78 -0
  19. data/test/auto_complete_form_builder_helper_test.rb +123 -0
  20. data/test/auto_complete_nested_attributes_test.rb +143 -0
  21. data/test/auto_complete_test.rb +136 -0
  22. data/test/helper.rb +14 -0
  23. data/test/view_mapper/expected_templates/_form.html.erb +26 -0
  24. data/test/view_mapper/expected_templates/_person.html.erb +22 -0
  25. data/test/view_mapper/expected_templates/create_parents.rb +16 -0
  26. data/test/view_mapper/expected_templates/edit.html.erb +11 -0
  27. data/test/view_mapper/expected_templates/index.html.erb +20 -0
  28. data/test/view_mapper/expected_templates/new.html.erb +10 -0
  29. data/test/view_mapper/expected_templates/parent.rb +15 -0
  30. data/test/view_mapper/expected_templates/show.html.erb +37 -0
  31. data/test/view_mapper/expected_templates/testies_controller.rb +92 -0
  32. data/test/view_mapper/has_many_auto_complete_view_test.rb +587 -0
  33. metadata +93 -0
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/README ADDED
@@ -0,0 +1,102 @@
1
+ Fork of the standard auto_complete plugin to support:
2
+
3
+ 1. Creating auto complete text fields that can be repeated more than once a single form.
4
+
5
+ 2. Using auto_complete_for with named scopes to display a customized list
6
+ of autocomplete options.
7
+
8
+ See: http://patshaughnessy.net/repeated_auto_complete for details.
9
+
10
+
11
+ Repeated autocomplete text fields:
12
+ ==================================
13
+
14
+ A "text_field_with_auto_complete" method is made available to the form builder
15
+ yielded by form_for and fields_for that:
16
+ - Insures unique id's for <input> and <div> tags by inserting unique integers into their
17
+ id attributes
18
+ - Uses the object name from the surrounding call to form_for or fields_for so attribute
19
+ mass assignment will work as usual
20
+ - Works with the same server side controller method, auto_complete_for, as usual
21
+ - Supports nested attributes in Rails 2.3
22
+
23
+ form.text_field_with_auto_complete works the same as the original text_field_with_auto_complete macro,
24
+ except it does not take the object as a parameter.
25
+
26
+ Example with nested attributes using Rails 2.3 or later:
27
+
28
+ class Project < ActiveRecord::Base
29
+ has_many :tasks
30
+ accepts_nested_attributes_for :tasks, :allow_destroy => true
31
+ end
32
+
33
+ <% form_for @project do |project_form| %>
34
+ <p>
35
+ <%= project_form.label :name, "Project:" %>
36
+ <%= project_form.text_field_with_auto_complete :name, {}, {:method => :get } %>
37
+ </p>
38
+ <% project_form.fields_for :tasks do |task_form| %>
39
+ <p>
40
+ <%= task_form.label :name, "Task:" %>
41
+ <%= task_form.text_field_with_auto_complete :name, {}, { :method => :get, :skip_style => true } %>
42
+ </p>
43
+ <% end %>
44
+ <% end %>
45
+
46
+
47
+ Rails 2.2 and earlier example:
48
+
49
+ <% for person in @group.people %>
50
+ <% fields_for "group[person_attributes][]", person do |person_form| %>
51
+ <p>
52
+ Person <%= person_form.label :name %><br/>
53
+ <%= person_form.text_field_with_auto_complete :name, {}, {:method => :get } %>
54
+ </p>
55
+ <% end %>
56
+ <% end %>
57
+
58
+
59
+
60
+ Named scopes with auto_complete_for:
61
+ ====================================
62
+
63
+ auto_complete_for now optionally accepts a block that is called with the item list and HTTP parameters
64
+ when the auto complete AJAX request is received. This block can be used to specify that a named scope
65
+ be used to generate a customized list of autocomplete options.
66
+
67
+ Example using anonymous scope:
68
+
69
+ auto_complete_for :some_model, :some_other_field do |items, params|
70
+ items.scoped( { :conditions => [ "a_third_field = ?", params['some_model']['a_third_field'] ] })
71
+ end
72
+
73
+
74
+
75
+ Example using named scope:
76
+
77
+ class Task < ActiveRecord::Base
78
+ belongs_to :project
79
+ named_scope :by_project,
80
+ lambda { |project_name| {
81
+ :include => :project,
82
+ :conditions => [ "projects.name = ?", project_name ]
83
+ } }
84
+ end
85
+
86
+ auto_complete_for :task, :name do | items, params |
87
+ items.by_project(params['project'])
88
+ end
89
+
90
+
91
+
92
+ Autocomplete Scaffolding:
93
+ =========================
94
+
95
+ The "View Mapper" gem will generate scaffolding code that illustrates how to use
96
+ the standard Rails auto_complete plugin in a simple form, or the repeated_auto_complete
97
+ plugin/gem in a complex form.
98
+
99
+ See: http://pats
100
+
101
+
102
+ Copyright (c) 2009 [Pat Shaughnessy], released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "repeated_auto_complete"
8
+ gem.summary = %Q{auto_complete plugin refactored to handle complex forms and named scopes}
9
+ gem.description = %Q{auto_complete plugin refactored to handle complex forms and named scopes}
10
+ gem.email = "pat@patshaughnessy.net"
11
+ gem.homepage = "http://patshaughnessy.net/repeated_auto_complete"
12
+ gem.authors = ["Pat Shaughnessy"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "repeated_auto_complete #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ ActionController::Base.send :include, AutoComplete
2
+ ActionController::Base.helper AutoCompleteMacrosHelper
3
+ ActionView::Helpers::FormBuilder.send :include, AutoCompleteFormBuilderHelper
@@ -0,0 +1,49 @@
1
+ module AutoComplete
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ #
8
+ # Example:
9
+ #
10
+ # # Controller
11
+ # class BlogController < ApplicationController
12
+ # auto_complete_for :post, :title
13
+ # end
14
+ #
15
+ # # View
16
+ # <%= text_field_with_auto_complete :post, title %>
17
+ #
18
+ # By default, auto_complete_for limits the results to 10 entries,
19
+ # and sorts by the given field.
20
+ #
21
+ # auto_complete_for takes a third parameter, an options hash to
22
+ # the find method used to search for the records:
23
+ #
24
+ # auto_complete_for :post, :title, :limit => 15, :order => 'created_at DESC'
25
+ #
26
+ # For help on defining text input fields with autocompletion,
27
+ # see ActionView::Helpers::JavaScriptHelper.
28
+ #
29
+ # For more examples, see script.aculo.us:
30
+ # * http://script.aculo.us/demos/ajax/autocompleter
31
+ # * http://script.aculo.us/demos/ajax/autocompleter_customized
32
+ module ClassMethods
33
+ def auto_complete_for(object, method, options = {})
34
+ define_method("auto_complete_for_#{object}_#{method}") do
35
+ model = object.to_s.camelize.constantize
36
+ find_options = {
37
+ :conditions => [ "LOWER(#{model.quoted_table_name}.#{method}) LIKE ?", '%' + params[object][method].downcase + '%' ],
38
+ :order => "#{model.quoted_table_name}.#{method} ASC",
39
+ :limit => 10 }.merge!(options)
40
+
41
+ @items = model.scoped(find_options)
42
+ @items = yield(@items, params) if block_given?
43
+
44
+ render :inline => "<%= auto_complete_result @items, '#{method}' %>"
45
+ end
46
+ end
47
+ end
48
+
49
+ end
@@ -0,0 +1,38 @@
1
+ module AutoCompleteFormBuilderHelper
2
+
3
+ def class_name
4
+ "#{@object.class.to_s.underscore}"
5
+ end
6
+
7
+ def sanitized_object_name
8
+ @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
9
+ end
10
+
11
+ def is_used_as_nested_attribute?
12
+ /\[#{class_name.pluralize}_attributes\]\[[0-9]+\]/.match @object_name
13
+ end
14
+
15
+ def text_field_with_auto_complete(method, tag_options = {}, completion_options = {})
16
+ if completion_options[:child_index]
17
+ unique_object_name = "#{class_name}_#{completion_options[:child_index]}"
18
+ elsif @options[:child_index]
19
+ unique_object_name = "#{class_name}_#{@options[:child_index]}"
20
+ elsif is_used_as_nested_attribute?
21
+ unique_object_name = sanitized_object_name
22
+ else
23
+ unique_object_name = "#{class_name}_#{Object.new.object_id.abs}"
24
+ end
25
+ completion_options_for_class_name = {
26
+ :url => { :action => "auto_complete_for_#{class_name}_#{method}" },
27
+ :param_name => "#{class_name}[#{method}]"
28
+ }.update(completion_options)
29
+ @template.auto_complete_field_with_style_and_script(unique_object_name,
30
+ method,
31
+ tag_options,
32
+ completion_options_for_class_name
33
+ ) do
34
+ text_field(method, { :id => "#{unique_object_name}_#{method}" }.update(tag_options))
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1,152 @@
1
+ module AutoCompleteMacrosHelper
2
+ # Adds AJAX autocomplete functionality to the text input field with the
3
+ # DOM ID specified by +field_id+.
4
+ #
5
+ # This function expects that the called action returns an HTML <ul> list,
6
+ # or nothing if no entries should be displayed for autocompletion.
7
+ #
8
+ # You'll probably want to turn the browser's built-in autocompletion off,
9
+ # so be sure to include an <tt>autocomplete="off"</tt> attribute with your text
10
+ # input field.
11
+ #
12
+ # The autocompleter object is assigned to a Javascript variable named <tt>field_id</tt>_auto_completer.
13
+ # This object is useful if you for example want to trigger the auto-complete suggestions through
14
+ # other means than user input (for that specific case, call the <tt>activate</tt> method on that object).
15
+ #
16
+ # Required +options+ are:
17
+ # <tt>:url</tt>:: URL to call for autocompletion results
18
+ # in url_for format.
19
+ #
20
+ # Addtional +options+ are:
21
+ # <tt>:update</tt>:: Specifies the DOM ID of the element whose
22
+ # innerHTML should be updated with the autocomplete
23
+ # entries returned by the AJAX request.
24
+ # Defaults to <tt>field_id</tt> + '_auto_complete'
25
+ # <tt>:with</tt>:: A JavaScript expression specifying the
26
+ # parameters for the XMLHttpRequest. This defaults
27
+ # to 'fieldname=value'.
28
+ # <tt>:frequency</tt>:: Determines the time to wait after the last keystroke
29
+ # for the AJAX request to be initiated.
30
+ # <tt>:indicator</tt>:: Specifies the DOM ID of an element which will be
31
+ # displayed while autocomplete is running.
32
+ # <tt>:tokens</tt>:: A string or an array of strings containing
33
+ # separator tokens for tokenized incremental
34
+ # autocompletion. Example: <tt>:tokens => ','</tt> would
35
+ # allow multiple autocompletion entries, separated
36
+ # by commas.
37
+ # <tt>:min_chars</tt>:: The minimum number of characters that should be
38
+ # in the input field before an Ajax call is made
39
+ # to the server.
40
+ # <tt>:on_hide</tt>:: A Javascript expression that is called when the
41
+ # autocompletion div is hidden. The expression
42
+ # should take two variables: element and update.
43
+ # Element is a DOM element for the field, update
44
+ # is a DOM element for the div from which the
45
+ # innerHTML is replaced.
46
+ # <tt>:on_show</tt>:: Like on_hide, only now the expression is called
47
+ # then the div is shown.
48
+ # <tt>:after_update_element</tt>:: A Javascript expression that is called when the
49
+ # user has selected one of the proposed values.
50
+ # The expression should take two variables: element and value.
51
+ # Element is a DOM element for the field, value
52
+ # is the value selected by the user.
53
+ # <tt>:parameters</tt>:: To send additional parameters to the server,
54
+ # add them here in the format: 'field=value&another=value'.
55
+ # <tt>:select</tt>:: Pick the class of the element from which the value for
56
+ # insertion should be extracted. If this is not specified,
57
+ # the entire element is used.
58
+ # <tt>:method</tt>:: Specifies the HTTP verb to use when the autocompletion
59
+ # request is made. Defaults to POST.
60
+ def auto_complete_field(field_id, options = {})
61
+ function = "var #{field_id}_auto_completer = new Ajax.Autocompleter("
62
+ function << "'#{field_id}', "
63
+ function << "'" + (options[:update] || "#{field_id}_auto_complete") + "', "
64
+ function << "'#{url_for(options[:url])}'"
65
+
66
+ js_options = {}
67
+ js_options[:tokens] = array_or_string_for_javascript(options[:tokens]) if options[:tokens]
68
+ js_options[:callback] = "function(element, value) { return #{options[:with]} }" if options[:with]
69
+ js_options[:indicator] = "'#{options[:indicator]}'" if options[:indicator]
70
+ js_options[:select] = "'#{options[:select]}'" if options[:select]
71
+ js_options[:paramName] = "'#{options[:param_name]}'" if options[:param_name]
72
+ js_options[:frequency] = "#{options[:frequency]}" if options[:frequency]
73
+ js_options[:method] = "'#{options[:method].to_s}'" if options[:method]
74
+ js_options[:parameters] = "'#{options[:parameters]}'" if options[:parameters]
75
+
76
+ { :after_update_element => :afterUpdateElement,
77
+ :on_show => :onShow, :on_hide => :onHide, :min_chars => :minChars }.each do |k,v|
78
+ js_options[v] = options[k] if options[k]
79
+ end
80
+
81
+ function << (', ' + options_for_javascript(js_options) + ')')
82
+
83
+ javascript_tag(function)
84
+ end
85
+
86
+ # Use this method in your view to generate a return for the AJAX autocomplete requests.
87
+ #
88
+ # Example action:
89
+ #
90
+ # def auto_complete_for_item_title
91
+ # @items = Item.find(:all,
92
+ # :conditions => [ 'LOWER(description) LIKE ?',
93
+ # '%' + request.raw_post.downcase + '%' ])
94
+ # render :inline => "<%= auto_complete_result(@items, 'description') %>"
95
+ # end
96
+ #
97
+ # The auto_complete_result can of course also be called from a view belonging to the
98
+ # auto_complete action if you need to decorate it further.
99
+ def auto_complete_result(entries, field, phrase = nil)
100
+ return unless entries
101
+ items = entries.map { |entry| content_tag("li", phrase ? highlight(entry[field], phrase) : h(entry[field])) }
102
+ content_tag("ul", items.uniq)
103
+ end
104
+
105
+ # Wrapper for text_field with added AJAX autocompletion functionality.
106
+ #
107
+ # In your controller, you'll need to define an action called
108
+ # auto_complete_for to respond the AJAX calls,
109
+ #
110
+ def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {})
111
+ auto_complete_field_with_style_and_script(object, method, tag_options, completion_options) do
112
+ text_field(object, method, tag_options)
113
+ end
114
+ end
115
+
116
+ def auto_complete_field_with_style_and_script(object, method, tag_options = {}, completion_options = {})
117
+ (completion_options[:skip_style] ? "" : auto_complete_stylesheet) +
118
+ yield +
119
+ content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") +
120
+ auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options))
121
+ end
122
+
123
+ private
124
+ def auto_complete_stylesheet
125
+ content_tag('style', <<-EOT, :type => Mime::CSS)
126
+ div.auto_complete {
127
+ width: 350px;
128
+ background: #fff;
129
+ }
130
+ div.auto_complete ul {
131
+ border:1px solid #888;
132
+ margin:0;
133
+ padding:0;
134
+ width:100%;
135
+ list-style-type:none;
136
+ }
137
+ div.auto_complete ul li {
138
+ margin:0;
139
+ padding:3px;
140
+ }
141
+ div.auto_complete ul li.selected {
142
+ background-color: #ffb;
143
+ }
144
+ div.auto_complete ul strong.highlight {
145
+ color: #800;
146
+ margin:0;
147
+ padding:0;
148
+ }
149
+ EOT
150
+ end
151
+
152
+ end
@@ -0,0 +1,4 @@
1
+ require 'auto_complete.rb'
2
+ require 'auto_complete_form_builder_helper.rb'
3
+ require 'auto_complete_macros_helper.rb'
4
+ require 'view_mapper/has_many_auto_complete_view.rb'
@@ -0,0 +1,4 @@
1
+ The code in this folder is a "View Mapper" module. It will generate scaffolding for a complex form
2
+ that uses auto complete and nested attributes.
3
+
4
+ For details see: http://patshaughnessy.net/view_mapper
@@ -0,0 +1,88 @@
1
+ module ViewMapper
2
+ module HasManyAutoCompleteView
3
+
4
+ def self.extended(base)
5
+ base.extend(ViewMapper::HasManyChildModels)
6
+ end
7
+
8
+ def self.source_root
9
+ File.expand_path(File.dirname(__FILE__) + "/templates")
10
+ end
11
+
12
+ def source_roots_for_view
13
+ [
14
+ HasManyAutoCompleteView.source_root,
15
+ HasManyView.source_root,
16
+ File.expand_path(source_root),
17
+ File.expand_path(File.join(self.class.lookup('model').path, 'templates'))
18
+ ]
19
+ end
20
+
21
+ def manifest
22
+ m = super.edit do |action|
23
+ action unless is_child_model_action?(action)
24
+ end
25
+ add_child_models_manifest(m)
26
+ add_auto_complete_manifest(m)
27
+ m
28
+ end
29
+
30
+ def add_auto_complete_manifest(m)
31
+ if valid
32
+ auto_complete_attributes.each do |attrib|
33
+ add_auto_complete_route(m, attrib[:model_name], attrib[:text_field])
34
+ end
35
+ end
36
+ end
37
+
38
+ def add_auto_complete_route(manifest, model_name, text_field)
39
+ manifest.route :name => 'connect',
40
+ :path => auto_complete_for_method(model_name, text_field),
41
+ :controller => controller_file_name,
42
+ :action => auto_complete_for_method(model_name, text_field)
43
+ end
44
+
45
+ def auto_complete_for_method(model_name, text_field)
46
+ "auto_complete_for_#{model_name}_#{text_field}"
47
+ end
48
+
49
+ def auto_complete_attributes
50
+ @auto_complete_attributes = find_auto_complete_attributes
51
+ end
52
+
53
+ def find_auto_complete_attributes
54
+ attribs = []
55
+ if view_only?
56
+ attribs << auto_complete_attributes_for_model(model)
57
+ else
58
+ attribs << auto_complete_attributes_from_command_line
59
+ end
60
+ attribs << child_models.collect { |child| auto_complete_attributes_for_model(child) }
61
+ attribs.flatten
62
+ end
63
+
64
+ def auto_complete_attributes_for_model(model_info)
65
+ model_info.text_fields.collect do |text_field|
66
+ { :model_name => model_info.name.downcase, :text_field => text_field }
67
+ end
68
+ end
69
+
70
+ def auto_complete_attributes_from_command_line
71
+ attributes.reject do |cmd_line_attrib|
72
+ !ViewMapper::ModelInfo.is_text_field_attrib_type? cmd_line_attrib.type
73
+ end.collect do |cmd_line_attrib|
74
+ { :model_name => singular_name, :text_field => cmd_line_attrib.name }
75
+ end
76
+ end
77
+
78
+ def is_auto_complete_attribute?(model_name, text_field)
79
+ !auto_complete_attributes.detect { |attrib| attrib[:model_name] == model_name && attrib[:text_field] == text_field }.nil?
80
+ end
81
+
82
+ def validate
83
+ super
84
+ @valid &&= validate_child_models
85
+ end
86
+
87
+ end
88
+ end