repeated_auto_complete 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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