rails-autocomplete 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +51 -0
- data/LICENSE +20 -0
- data/README.md +453 -0
- data/Rakefile +21 -0
- data/lib/assets/javascripts/autocomplete-rails-uncompressed.js +197 -0
- data/lib/assets/javascripts/autocomplete-rails.js +1 -0
- data/lib/cucumber/autocomplete.rb +6 -0
- data/lib/generators/autocomplete/install_generator.rb +14 -0
- data/lib/generators/autocomplete/uncompressed_generator.rb +14 -0
- data/lib/rails-jquery-autocomplete.rb +23 -0
- data/lib/rails-jquery-autocomplete/autocomplete.rb +112 -0
- data/lib/rails-jquery-autocomplete/form_helper.rb +49 -0
- data/lib/rails-jquery-autocomplete/formtastic.rb +41 -0
- data/lib/rails-jquery-autocomplete/formtastic_plugin.rb +44 -0
- data/lib/rails-jquery-autocomplete/orm.rb +6 -0
- data/lib/rails-jquery-autocomplete/orm/active_record.rb +83 -0
- data/lib/rails-jquery-autocomplete/rails/engine.rb +5 -0
- data/lib/rails-jquery-autocomplete/simple_form_plugin.rb +100 -0
- data/lib/rails-jquery-autocomplete/version.rb +3 -0
- data/lib/steak/autocomplete.rb +12 -0
- data/test/form_helper_test.rb +25 -0
- data/test/generators/autocomplete/install_generator_test.rb +25 -0
- data/test/generators/autocomplete/uncompressed_generator_test.rb +25 -0
- data/test/lib/rails-jquery-autocomplete/autocomplete_test.rb +93 -0
- data/test/lib/rails-jquery-autocomplete/orm/active_record_test.rb +190 -0
- data/test/lib/rails-jquery-autocomplete/simple_form_plugin_test.rb +33 -0
- data/test/lib/rails-jquery-autocomplete_test.rb +38 -0
- data/test/test_helper.rb +35 -0
- data/test/view_test_helper.rb +108 -0
- metadata +196 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
#
|
2
|
+
# Load the formtastic plugin if using Formtastic
|
3
|
+
#
|
4
|
+
begin
|
5
|
+
require 'formtastic'
|
6
|
+
begin
|
7
|
+
require "formtastic/version"
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
if defined?(Formtastic::VERSION)
|
12
|
+
#
|
13
|
+
# Formtastic 2.x
|
14
|
+
#
|
15
|
+
|
16
|
+
module Formtastic
|
17
|
+
module Inputs
|
18
|
+
class AutocompleteInput
|
19
|
+
include Base
|
20
|
+
include Base::Stringish
|
21
|
+
|
22
|
+
def to_html
|
23
|
+
input_wrapping do
|
24
|
+
label_html <<
|
25
|
+
builder.autocomplete_field(method, options.delete(:url), input_html_options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
else
|
32
|
+
|
33
|
+
#
|
34
|
+
# Formtastic 1.x
|
35
|
+
#
|
36
|
+
class Formtastic::SemanticFormBuilder < ActionView::Helpers::FormBuilder
|
37
|
+
include RailsJQueryAutocomplete::FormtasticPlugin
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RailsJQueryAutocomplete
|
2
|
+
module FormtasticPlugin
|
3
|
+
def autocomplete_input(method, options = {})
|
4
|
+
if options.key?(:selected) || options.key?(:checked) || options.key?(:default)
|
5
|
+
::ActiveSupport::Deprecation.warn(
|
6
|
+
"The :selected, :checked (and :default) options are deprecated in Formtastic and will be removed from 1.0. " <<
|
7
|
+
"Please set default values in your models (using an after_initialize callback) or in your controller set-up. " <<
|
8
|
+
"See http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html for more information.", caller)
|
9
|
+
end
|
10
|
+
|
11
|
+
options[:required] = method_required?(method) unless options.key?(:required)
|
12
|
+
options[:as] ||= "autocompleted_string"
|
13
|
+
|
14
|
+
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
|
15
|
+
html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
|
16
|
+
|
17
|
+
wrapper_html = options.delete(:wrapper_html) || {}
|
18
|
+
wrapper_html[:id] ||= generate_html_id(method)
|
19
|
+
wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
|
20
|
+
|
21
|
+
if options[:input_html] && options[:input_html][:id]
|
22
|
+
options[:label_html] ||= {}
|
23
|
+
options[:label_html][:for] ||= options[:input_html][:id]
|
24
|
+
end
|
25
|
+
|
26
|
+
input_parts = self.class.inline_order.dup
|
27
|
+
input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden
|
28
|
+
|
29
|
+
list_item_content = input_parts.map do |type|
|
30
|
+
send(:"inline_#{type}_for", method, options)
|
31
|
+
end.compact.join("\n")
|
32
|
+
|
33
|
+
return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)
|
34
|
+
end
|
35
|
+
|
36
|
+
alias_method :autocompleted_input, :autocomplete_input
|
37
|
+
|
38
|
+
|
39
|
+
protected
|
40
|
+
def autocompleted_string_input(method, options)
|
41
|
+
self.label(method, options_for_label(options)) << autocomplete_field(method, options.delete(:url), options)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module RailsJQueryAutocomplete
|
2
|
+
module Orm
|
3
|
+
module ActiveRecord
|
4
|
+
def active_record_get_autocomplete_order(method, options, model=nil)
|
5
|
+
order = options[:order]
|
6
|
+
|
7
|
+
table_prefix = model ? "#{model.table_name}." : ""
|
8
|
+
if sqlite?
|
9
|
+
order || "LOWER(#{method}) ASC"
|
10
|
+
else
|
11
|
+
order || "LOWER(#{table_prefix}#{method}) ASC"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def active_record_get_autocomplete_items(parameters)
|
16
|
+
model = parameters[:model]
|
17
|
+
term = parameters[:term]
|
18
|
+
options = parameters[:options]
|
19
|
+
method = options[:hstore] ? options[:hstore][:method] : parameters[:method]
|
20
|
+
scopes = Array(options[:scopes])
|
21
|
+
where = options[:where]
|
22
|
+
limit = get_autocomplete_limit(options)
|
23
|
+
order = active_record_get_autocomplete_order(method, options, model)
|
24
|
+
|
25
|
+
items = (::Rails::VERSION::MAJOR * 10 + ::Rails::VERSION::MINOR) >= 40 ? model.where(nil) : model.scoped
|
26
|
+
|
27
|
+
scopes.each { |scope| items = items.send(scope) } unless scopes.empty?
|
28
|
+
|
29
|
+
items = items.select(get_autocomplete_select_clause(model, method, options)) unless options[:full_model]
|
30
|
+
items = items.where(get_autocomplete_where_clause(model, term, method, options)).
|
31
|
+
limit(limit).order(order)
|
32
|
+
items = items.where(where) unless where.blank?
|
33
|
+
|
34
|
+
items
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_autocomplete_select_clause(model, method, options)
|
38
|
+
if sqlite?
|
39
|
+
table_name = model.quoted_table_name
|
40
|
+
([
|
41
|
+
"#{table_name}.#{model.connection.quote_column_name(model.primary_key)} as #{model.primary_key}",
|
42
|
+
"#{table_name}.#{model.connection.quote_column_name(method)} as #{method}"
|
43
|
+
] + (options[:extra_data].blank? ? [] : options[:extra_data]))
|
44
|
+
else
|
45
|
+
table_name = model.table_name
|
46
|
+
(["#{table_name}.#{model.primary_key}", "#{table_name}.#{method}"] + (options[:extra_data].blank? ? [] : options[:extra_data]))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def get_autocomplete_where_clause(model, term, method, options)
|
51
|
+
table_name = model.table_name
|
52
|
+
is_full_search = options[:full]
|
53
|
+
is_case_sensitive_search = options[:case_sensitive]
|
54
|
+
like_clause = (postgres?(model) && !is_case_sensitive_search ? 'ILIKE' : 'LIKE')
|
55
|
+
column_transform = is_case_sensitive_search ? '' : 'LOWER'
|
56
|
+
term = "#{(is_full_search ? '%' : '')}#{term.gsub(/([_%\\])/, '\\\\\1')}%"
|
57
|
+
if options[:hstore]
|
58
|
+
["#{column_transform}(#{table_name}.#{method} -> '#{options[:hstore][:key]}') LIKE #{column_transform}(?)", term]
|
59
|
+
elsif sqlite?
|
60
|
+
["#{column_transform}(#{method}) #{like_clause} #{column_transform}(?)", term]
|
61
|
+
else
|
62
|
+
["#{column_transform}(#{table_name}.#{method}) #{like_clause} #{column_transform}(?)", term]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
def sqlite?
|
69
|
+
begin
|
70
|
+
return ::ActiveRecord::Base.connection.to_s.match(/SQLite/)
|
71
|
+
rescue ::ActiveRecord::ConnectionNotEstablished
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
def postgres?(model)
|
78
|
+
# Figure out if this particular model uses the PostgreSQL adapter
|
79
|
+
model.connection.class.to_s.match(/PostgreSQLAdapter/)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module SimpleForm
|
2
|
+
module Inputs
|
3
|
+
module Autocomplete
|
4
|
+
#
|
5
|
+
# Method used to rename the autocomplete key to a more standard
|
6
|
+
# data-autocomplete key
|
7
|
+
#
|
8
|
+
def rewrite_autocomplete_option
|
9
|
+
new_options = {}
|
10
|
+
new_options["data-autocomplete-fields"] = JSON.generate(options.delete :fields) if options[:fields]
|
11
|
+
new_options["data-update-elements"] = JSON.generate(options.delete :update_elements) if options[:update_elements]
|
12
|
+
new_options["data-id-element"] = options.delete :id_element if options[:id_element]
|
13
|
+
input_html_options.merge new_options
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class AutocompleteInput < Base
|
18
|
+
include Autocomplete
|
19
|
+
|
20
|
+
protected
|
21
|
+
def limit
|
22
|
+
column && column.limit
|
23
|
+
end
|
24
|
+
|
25
|
+
def has_placeholder?
|
26
|
+
placeholder_present?
|
27
|
+
end
|
28
|
+
|
29
|
+
def input(args = nil)
|
30
|
+
# This branching is to deal with a change beginning in simple_form 3.0.2 and above to ensure backwards compatibility
|
31
|
+
if args.nil?
|
32
|
+
@builder.autocomplete_field(
|
33
|
+
attribute_name,
|
34
|
+
options[:url],
|
35
|
+
rewrite_autocomplete_option
|
36
|
+
)
|
37
|
+
else
|
38
|
+
@builder.autocomplete_field(
|
39
|
+
attribute_name,
|
40
|
+
options[:url],
|
41
|
+
merge_wrapper_options(rewrite_autocomplete_option, args)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class AutocompleteCollectionInput < CollectionInput
|
48
|
+
include Autocomplete
|
49
|
+
|
50
|
+
def input(opts)
|
51
|
+
# http://www.codeofficer.com/blog/entry/form_builders_in_rails_discovering_field_names_and_ids_for_javascript/
|
52
|
+
hidden_id = "#{object_name}_#{attribute_name}_hidden".gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
|
53
|
+
id_element = options[:id_element]
|
54
|
+
if id_element
|
55
|
+
id_element << ", #" << hidden_id
|
56
|
+
else
|
57
|
+
id_element = "#" + hidden_id
|
58
|
+
end
|
59
|
+
options[:id_element] = id_element
|
60
|
+
|
61
|
+
# This branching is to deal with a change beginning in simple_form 3.0.2 and above to ensure backwards compatibility
|
62
|
+
if opts.nil?
|
63
|
+
autocomplete_options = rewrite_autocomplete_option
|
64
|
+
else
|
65
|
+
merge_wrapper_options(rewrite_autocomplete_option, args)
|
66
|
+
end
|
67
|
+
|
68
|
+
label_method, value_method = detect_collection_methods
|
69
|
+
association = object.send(reflection.name)
|
70
|
+
if association && association.respond_to?(label_method)
|
71
|
+
autocomplete_options[:value] = association.send(label_method)
|
72
|
+
end
|
73
|
+
out = @builder.autocomplete_field(
|
74
|
+
attribute_name,
|
75
|
+
options[:url],
|
76
|
+
autocomplete_options
|
77
|
+
)
|
78
|
+
hidden_options = if association && association.respond_to?(value_method)
|
79
|
+
new_options = {}
|
80
|
+
new_options[:value] = association.send(value_method)
|
81
|
+
input_html_options.merge new_options
|
82
|
+
else
|
83
|
+
input_html_options
|
84
|
+
end
|
85
|
+
hidden_options[:id] = hidden_id
|
86
|
+
out << @builder.hidden_field(
|
87
|
+
attribute_name,
|
88
|
+
hidden_options
|
89
|
+
)
|
90
|
+
out.html_safe
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class FormBuilder
|
96
|
+
map_type :autocomplete, :to => SimpleForm::Inputs::AutocompleteInput
|
97
|
+
map_type :autocomplete_collection, :to => SimpleForm::Inputs::AutocompleteCollectionInput
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Steak
|
2
|
+
module Autocomplete
|
3
|
+
def choose_autocomplete_result(text, input_id="input[data-autocomplete]")
|
4
|
+
page.execute_script %Q{ $('#{input_id}').trigger("focus") }
|
5
|
+
page.execute_script %Q{ $('#{input_id}').trigger("keydown") }
|
6
|
+
sleep 1
|
7
|
+
page.execute_script %Q{ $('.ui-menu-item a:contains("#{text}")').trigger("mouseenter").trigger("click"); }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec.configuration.include Steak::Autocomplete, :type => :acceptance
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class Post
|
4
|
+
attr_accessor :author
|
5
|
+
end
|
6
|
+
|
7
|
+
class FormHelperTest < ActionView::TestCase
|
8
|
+
def test_text_field_tag
|
9
|
+
assert_match(/autocomplete=\"some\/path\"/, text_field_tag('field_name', '', :autocomplete => 'some/path'))
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_text_field
|
13
|
+
post = Post.new
|
14
|
+
assert_match(/autocomplete=\"some\/path\"/, text_field(:post, :author, :autocomplete => 'some/path'))
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_autocomplete_field_tag
|
18
|
+
assert_match(/data-autocomplete=\"some\/path\"/, autocomplete_field_tag('field_name', '', 'some/path'))
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_autocomplete_field
|
22
|
+
post= Post.new
|
23
|
+
assert_match(/data-autocomplete=\"some\/path\"/, autocomplete_field(:post, :author, 'some/path'))
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'generators/autocomplete/install_generator'
|
3
|
+
|
4
|
+
module Autocomplete
|
5
|
+
class InstallGeneratorTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@destination = File.join('tmp', 'test_app')
|
8
|
+
@source = InstallGenerator.source_root
|
9
|
+
@filename = File.join(@destination, 'public', 'javascripts', 'autocomplete-rails.js')
|
10
|
+
|
11
|
+
File.unlink(@filename) if File.exists?(@filename)
|
12
|
+
|
13
|
+
InstallGenerator.start([], :destination_root => @destination)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_install
|
17
|
+
assert File.exists?(@filename)
|
18
|
+
|
19
|
+
assert_equal(
|
20
|
+
File.read(File.join(@source, 'autocomplete-rails.js')),
|
21
|
+
File.read(@filename)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'generators/autocomplete/uncompressed_generator'
|
3
|
+
|
4
|
+
module Autocomplete
|
5
|
+
class UncompressedGeneratorTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@destination = File.join('tmp', 'test_app')
|
8
|
+
@source = UncompressedGenerator.source_root
|
9
|
+
@filename = File.join(@destination, 'public', 'javascripts', 'autocomplete-rails.js')
|
10
|
+
|
11
|
+
File.unlink(@filename) if File.exists?(@filename)
|
12
|
+
|
13
|
+
UncompressedGenerator.start([], :destination_root => @destination)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_install
|
17
|
+
assert File.exists?(@filename)
|
18
|
+
|
19
|
+
assert_equal(
|
20
|
+
File.read(File.join(@source, 'autocomplete-rails-uncompressed.js')),
|
21
|
+
File.read(@filename)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module RailsJQueryAutocomplete
|
4
|
+
class AutocompleteTest < Test::Unit::TestCase
|
5
|
+
include RailsJQueryAutocomplete::Autocomplete
|
6
|
+
|
7
|
+
context 'ClassMethods' do
|
8
|
+
context '#autocomplete' do
|
9
|
+
context '#get_prefix' do
|
10
|
+
context 'Mongoid and MongoMapper is not defined' do
|
11
|
+
setup do
|
12
|
+
ActorsController = Class.new(ActionController::Base)
|
13
|
+
ActorsController.autocomplete(:movie, :name)
|
14
|
+
@controller = ActorsController.new
|
15
|
+
|
16
|
+
@model = Class.new(ActiveRecord::Base)
|
17
|
+
|
18
|
+
Object.send(:remove_const, :Mongoid)
|
19
|
+
Object.send(:remove_const, :MongoMapper)
|
20
|
+
end
|
21
|
+
|
22
|
+
should 'not raise exception' do
|
23
|
+
@controller.get_prefix(@model)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context '#get_autocomplete_limit' do
|
31
|
+
context 'the limit option was specified' do
|
32
|
+
should "return the limit option" do
|
33
|
+
assert_equal 99, get_autocomplete_limit({:limit => 99})
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'the limit option is not specified' do
|
38
|
+
should 'return 10' do
|
39
|
+
assert_equal 10, get_autocomplete_limit({})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context '#get_object' do
|
45
|
+
should 'return the specified sym as a class name' do
|
46
|
+
symbol = Object.new
|
47
|
+
class_object = Class.new
|
48
|
+
mock(symbol).to_s.mock!.camelize.mock!.constantize { class_object }
|
49
|
+
assert_equal class_object, get_object(symbol)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
context '#json_for_autocomplete' do
|
54
|
+
should 'parse items to JSON' do
|
55
|
+
item = mock(Object)
|
56
|
+
mock(item).send(:name).times(2) { 'Object Name' }
|
57
|
+
mock(item).id { 1 }
|
58
|
+
items = [item]
|
59
|
+
response = self.json_for_autocomplete(items, :name).first
|
60
|
+
assert_equal response["id"], "1"
|
61
|
+
assert_equal response["value"], "Object Name"
|
62
|
+
assert_equal response["label"], "Object Name"
|
63
|
+
end
|
64
|
+
|
65
|
+
should 'return an instance of HashWithIndifferentAccess' do
|
66
|
+
item = mock(Object)
|
67
|
+
mock(item).send(:name).times(2) { 'Object Name' }
|
68
|
+
mock(item).id { 1 }
|
69
|
+
items = [item]
|
70
|
+
response = self.json_for_autocomplete(items, :name).first
|
71
|
+
assert_equal response.is_a?(HashWithIndifferentAccess), true
|
72
|
+
assert_equal response["id"], "1"
|
73
|
+
assert_equal response[:id], "1"
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with extra data' do
|
77
|
+
should 'add that extra data to result' do
|
78
|
+
item = mock(Object)
|
79
|
+
mock(item).send(:name).times(2) { 'Object Name' }
|
80
|
+
mock(item).id { 1 }
|
81
|
+
mock(item).send("extra") { 'Object Extra ' }
|
82
|
+
|
83
|
+
items = [item]
|
84
|
+
response = self.json_for_autocomplete(items, :name, ["extra"]).first
|
85
|
+
|
86
|
+
assert_equal "1" , response["id"]
|
87
|
+
assert_equal "Object Name" , response["value"]
|
88
|
+
assert_equal "Object Name" , response["label"]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|