rails-autocomplete 1.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 (31) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +51 -0
  3. data/LICENSE +20 -0
  4. data/README.md +453 -0
  5. data/Rakefile +21 -0
  6. data/lib/assets/javascripts/autocomplete-rails-uncompressed.js +197 -0
  7. data/lib/assets/javascripts/autocomplete-rails.js +1 -0
  8. data/lib/cucumber/autocomplete.rb +6 -0
  9. data/lib/generators/autocomplete/install_generator.rb +14 -0
  10. data/lib/generators/autocomplete/uncompressed_generator.rb +14 -0
  11. data/lib/rails-jquery-autocomplete.rb +23 -0
  12. data/lib/rails-jquery-autocomplete/autocomplete.rb +112 -0
  13. data/lib/rails-jquery-autocomplete/form_helper.rb +49 -0
  14. data/lib/rails-jquery-autocomplete/formtastic.rb +41 -0
  15. data/lib/rails-jquery-autocomplete/formtastic_plugin.rb +44 -0
  16. data/lib/rails-jquery-autocomplete/orm.rb +6 -0
  17. data/lib/rails-jquery-autocomplete/orm/active_record.rb +83 -0
  18. data/lib/rails-jquery-autocomplete/rails/engine.rb +5 -0
  19. data/lib/rails-jquery-autocomplete/simple_form_plugin.rb +100 -0
  20. data/lib/rails-jquery-autocomplete/version.rb +3 -0
  21. data/lib/steak/autocomplete.rb +12 -0
  22. data/test/form_helper_test.rb +25 -0
  23. data/test/generators/autocomplete/install_generator_test.rb +25 -0
  24. data/test/generators/autocomplete/uncompressed_generator_test.rb +25 -0
  25. data/test/lib/rails-jquery-autocomplete/autocomplete_test.rb +93 -0
  26. data/test/lib/rails-jquery-autocomplete/orm/active_record_test.rb +190 -0
  27. data/test/lib/rails-jquery-autocomplete/simple_form_plugin_test.rb +33 -0
  28. data/test/lib/rails-jquery-autocomplete_test.rb +38 -0
  29. data/test/test_helper.rb +35 -0
  30. data/test/view_test_helper.rb +108 -0
  31. metadata +196 -0
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+
6
+ task :default => [:uglify, :test]
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'lib' << 'test'
10
+ test.pattern = 'test/**/*_test.rb'
11
+ test.verbose = true
12
+ end
13
+
14
+ task :uglify do
15
+ require 'uglifier'
16
+ file_folder = "lib/assets/javascripts"
17
+ File.open("#{file_folder}/autocomplete-rails.js", "w") do |f|
18
+ f << Uglifier.compile(File.read("#{file_folder}/autocomplete-rails-uncompressed.js"))
19
+ end
20
+ end
21
+
@@ -0,0 +1,197 @@
1
+ /*
2
+ * Unobtrusive autocomplete
3
+ *
4
+ * To use it, you just have to include the HTML attribute autocomplete
5
+ * with the autocomplete URL as the value
6
+ *
7
+ * Example:
8
+ * <input type="text" data-autocomplete="/url/to/autocomplete">
9
+ *
10
+ * Optionally, you can use a jQuery selector to specify a field that can
11
+ * be updated with the element id whenever you find a matching value
12
+ *
13
+ * Example:
14
+ * <input type="text" data-autocomplete="/url/to/autocomplete" data-id-element="#id_field">
15
+ */
16
+
17
+ (function(jQuery)
18
+ {
19
+ var self = null;
20
+ jQuery.fn.railsAutocomplete = function(selector) {
21
+ var handler = function() {
22
+ if (!this.railsAutoCompleter) {
23
+ this.railsAutoCompleter = new jQuery.railsAutocomplete(this);
24
+ }
25
+ };
26
+ if (jQuery.fn.on !== undefined) {
27
+ if (!selector) {
28
+ return;
29
+ }
30
+ return jQuery(document).on('focus',selector,handler);
31
+ }
32
+ else {
33
+ return this.live('focus',handler);
34
+ }
35
+ };
36
+
37
+ jQuery.railsAutocomplete = function (e) {
38
+ var _e = e;
39
+ this.init(_e);
40
+ };
41
+ jQuery.railsAutocomplete.options = {
42
+ showNoMatches: true,
43
+ noMatchesLabel: 'no existing match'
44
+ }
45
+
46
+ jQuery.railsAutocomplete.fn = jQuery.railsAutocomplete.prototype = {
47
+ railsAutocomplete: '0.0.1'
48
+ };
49
+
50
+ jQuery.railsAutocomplete.fn.extend = jQuery.railsAutocomplete.extend = jQuery.extend;
51
+ jQuery.railsAutocomplete.fn.extend({
52
+ init: function(e) {
53
+ e.delimiter = jQuery(e).attr('data-delimiter') || null;
54
+ e.min_length = jQuery(e).attr('data-min-length') || jQuery(e).attr('min-length') || 2;
55
+ e.append_to = jQuery(e).attr('data-append-to') || null;
56
+ e.autoFocus = jQuery(e).attr('data-auto-focus') || false;
57
+ function split( val ) {
58
+ return val.split( e.delimiter );
59
+ }
60
+ function extractLast( term ) {
61
+ return split( term ).pop().replace(/^\s+/,"");
62
+ }
63
+
64
+ jQuery(e).autocomplete({
65
+ appendTo: e.append_to,
66
+ autoFocus: e.autoFocus,
67
+ delay: jQuery(e).attr('delay') || 0,
68
+ source: function( request, response ) {
69
+ var firedFrom = this.element[0];
70
+ var params = {term: extractLast( request.term )};
71
+ if (jQuery(e).attr('data-autocomplete-fields')) {
72
+ jQuery.each(jQuery.parseJSON(jQuery(e).attr('data-autocomplete-fields')), function(field, selector) {
73
+ params[field] = jQuery(selector).val();
74
+ });
75
+ }
76
+ jQuery.getJSON( jQuery(e).attr('data-autocomplete'), params, function() {
77
+ var options = {};
78
+ jQuery.extend(options, jQuery.railsAutocomplete.options);
79
+ jQuery.each(options, function(key, value) {
80
+ if(options.hasOwnProperty(key)) {
81
+ var attrVal = jQuery(e).attr('data-' + key);
82
+ options[key] = attrVal ? attrVal : value;
83
+ }
84
+ });
85
+ if(arguments[0].length == 0 && jQuery.inArray(options.showNoMatches, [true, 'true']) >= 0) {
86
+ arguments[0] = [];
87
+ arguments[0][0] = { id: "", label: options.noMatchesLabel };
88
+ }
89
+ jQuery(arguments[0]).each(function(i, el) {
90
+ var obj = {};
91
+ obj[el.id] = el;
92
+ jQuery(e).data(obj);
93
+ });
94
+ response.apply(null, arguments);
95
+ jQuery(firedFrom).trigger('railsAutocomplete.source', arguments);
96
+ });
97
+ },
98
+ change: function( event, ui ) {
99
+ if(!jQuery(this).is('[data-id-element]') ||
100
+ jQuery(jQuery(this).attr('data-id-element')).val() === "") {
101
+ return;
102
+ }
103
+ jQuery(jQuery(this).attr('data-id-element')).val(ui.item ? ui.item.id : "").trigger('change');
104
+
105
+ if (jQuery(this).attr('data-update-elements')) {
106
+ var update_elements = jQuery.parseJSON(jQuery(this).attr("data-update-elements"));
107
+ var data = ui.item ? jQuery(this).data(ui.item.id.toString()) : {};
108
+ if(update_elements && jQuery(update_elements['id']).val() === "") {
109
+ return;
110
+ }
111
+ for (var key in update_elements) {
112
+ var element = jQuery(update_elements[key]);
113
+ if (element.is(':checkbox')) {
114
+ if (data[key] != null) {
115
+ element.prop('checked', data[key]);
116
+ }
117
+ } else {
118
+ element.val(ui.item ? data[key] : "").trigger('change');
119
+ }
120
+ }
121
+ }
122
+ },
123
+ search: function() {
124
+ // custom minLength
125
+ var term = extractLast( this.value );
126
+ if ( term.length < e.min_length ) {
127
+ return false;
128
+ }
129
+ },
130
+ focus: function() {
131
+ // prevent value inserted on focus
132
+ return false;
133
+ },
134
+ select: function( event, ui ) {
135
+ // first ensure value is a string
136
+ ui.item.value = ui.item.value.toString();
137
+ if(ui.item.value.toLowerCase().indexOf('no match') != -1 || ui.item.value.toLowerCase().indexOf('too many results') != -1){
138
+ jQuery(this).trigger('railsAutocomplete.noMatch', ui);
139
+ return false;
140
+ }
141
+ var terms = split( this.value );
142
+ // remove the current input
143
+ terms.pop();
144
+ // add the selected item
145
+ terms.push( ui.item.value );
146
+ // add placeholder to get the comma-and-space at the end
147
+ if (e.delimiter != null) {
148
+ terms.push( "" );
149
+ this.value = terms.join( e.delimiter );
150
+ } else {
151
+ this.value = terms.join("");
152
+ if (jQuery(this).attr('data-id-element')) {
153
+ jQuery(jQuery(this).attr('data-id-element')).val(ui.item.id).trigger('change');
154
+ }
155
+ if (jQuery(this).attr('data-update-elements')) {
156
+ var data = ui.item;
157
+ var new_record = ui.item.value.indexOf('Create New') != -1 ? true : false;
158
+ var update_elements = jQuery.parseJSON(jQuery(this).attr("data-update-elements"));
159
+ for (var key in update_elements) {
160
+ if(jQuery(update_elements[key]).attr("type") === "checkbox"){
161
+ if(data[key] === true || data[key] === 1) {
162
+ jQuery(update_elements[key]).attr("checked","checked");
163
+ }
164
+ else {
165
+ jQuery(update_elements[key]).removeAttr("checked");
166
+ }
167
+ }
168
+ else{
169
+ if((new_record && data[key] && data[key].indexOf('Create New') == -1) || !new_record){
170
+ jQuery(update_elements[key]).val(data[key]).trigger('change');
171
+ }else{
172
+ jQuery(update_elements[key]).val('').trigger('change');
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ var remember_string = this.value;
179
+ jQuery(this).bind('keyup.clearId', function(){
180
+ if(jQuery.trim(jQuery(this).val()) != jQuery.trim(remember_string)){
181
+ jQuery(jQuery(this).attr('data-id-element')).val("").trigger('change');
182
+ jQuery(this).unbind('keyup.clearId');
183
+ }
184
+ });
185
+ jQuery(e).trigger('railsAutocomplete.select', ui);
186
+
187
+ return false;
188
+ }
189
+ });
190
+ jQuery(e).trigger('railsAutocomplete.init');
191
+ }
192
+ });
193
+
194
+ jQuery(document).ready(function(){
195
+ jQuery('input[data-autocomplete]').railsAutocomplete('input[data-autocomplete]');
196
+ });
197
+ })(jQuery);
@@ -0,0 +1 @@
1
+ !function(t){t.fn.railsAutocomplete=function(e){var a=function(){this.railsAutoCompleter||(this.railsAutoCompleter=new t.railsAutocomplete(this))};if(void 0!==t.fn.on){if(!e)return;return t(document).on("focus",e,a)}return this.live("focus",a)},t.railsAutocomplete=function(t){var e=t;this.init(e)},t.railsAutocomplete.options={showNoMatches:!0,noMatchesLabel:"no existing match"},t.railsAutocomplete.fn=t.railsAutocomplete.prototype={railsAutocomplete:"0.0.1"},t.railsAutocomplete.fn.extend=t.railsAutocomplete.extend=t.extend,t.railsAutocomplete.fn.extend({init:function(e){function a(t){return t.split(e.delimiter)}function i(t){return a(t).pop().replace(/^\s+/,"")}e.delimiter=t(e).attr("data-delimiter")||null,e.min_length=t(e).attr("data-min-length")||t(e).attr("min-length")||2,e.append_to=t(e).attr("data-append-to")||null,e.autoFocus=t(e).attr("data-auto-focus")||!1,t(e).autocomplete({appendTo:e.append_to,autoFocus:e.autoFocus,delay:t(e).attr("delay")||0,source:function(a,r){var n=this.element[0],o={term:i(a.term)};t(e).attr("data-autocomplete-fields")&&t.each(t.parseJSON(t(e).attr("data-autocomplete-fields")),function(e,a){o[e]=t(a).val()}),t.getJSON(t(e).attr("data-autocomplete"),o,function(){var a={};t.extend(a,t.railsAutocomplete.options),t.each(a,function(i,r){if(a.hasOwnProperty(i)){var n=t(e).attr("data-"+i);a[i]=n?n:r}}),0==arguments[0].length&&t.inArray(a.showNoMatches,[!0,"true"])>=0&&(arguments[0]=[],arguments[0][0]={id:"",label:a.noMatchesLabel}),t(arguments[0]).each(function(a,i){var r={};r[i.id]=i,t(e).data(r)}),r.apply(null,arguments),t(n).trigger("railsAutocomplete.source",arguments)})},change:function(e,a){if(t(this).is("[data-id-element]")&&""!==t(t(this).attr("data-id-element")).val()&&(t(t(this).attr("data-id-element")).val(a.item?a.item.id:"").trigger("change"),t(this).attr("data-update-elements"))){var i=t.parseJSON(t(this).attr("data-update-elements")),r=a.item?t(this).data(a.item.id.toString()):{};if(i&&""===t(i.id).val())return;for(var n in i){var o=t(i[n]);o.is(":checkbox")?null!=r[n]&&o.prop("checked",r[n]):o.val(a.item?r[n]:"").trigger("change")}}},search:function(){var t=i(this.value);return t.length<e.min_length?!1:void 0},focus:function(){return!1},select:function(i,r){if(r.item.value=r.item.value.toString(),-1!=r.item.value.toLowerCase().indexOf("no match")||-1!=r.item.value.toLowerCase().indexOf("too many results"))return t(this).trigger("railsAutocomplete.noMatch",r),!1;var n=a(this.value);if(n.pop(),n.push(r.item.value),null!=e.delimiter)n.push(""),this.value=n.join(e.delimiter);else if(this.value=n.join(""),t(this).attr("data-id-element")&&t(t(this).attr("data-id-element")).val(r.item.id).trigger("change"),t(this).attr("data-update-elements")){var o=r.item,l=-1!=r.item.value.indexOf("Create New")?!0:!1,u=t.parseJSON(t(this).attr("data-update-elements"));for(var s in u)"checkbox"===t(u[s]).attr("type")?o[s]===!0||1===o[s]?t(u[s]).attr("checked","checked"):t(u[s]).removeAttr("checked"):l&&o[s]&&-1==o[s].indexOf("Create New")||!l?t(u[s]).val(o[s]).trigger("change"):t(u[s]).val("").trigger("change")}var c=this.value;return t(this).bind("keyup.clearId",function(){t.trim(t(this).val())!=t.trim(c)&&(t(t(this).attr("data-id-element")).val("").trigger("change"),t(this).unbind("keyup.clearId"))}),t(e).trigger("railsAutocomplete.select",r),!1}}),t(e).trigger("railsAutocomplete.init")}}),t(document).ready(function(){t("input[data-autocomplete]").railsAutocomplete("input[data-autocomplete]")})}(jQuery);
@@ -0,0 +1,6 @@
1
+ Given /^I choose "([^"]*)" in the autocomplete list$/ do |text|
2
+ page.execute_script %Q{ $('input[data-autocomplete]').trigger("focus") }
3
+ page.execute_script %Q{ $('input[data-autocomplete]').trigger("keydown") }
4
+ sleep 1
5
+ page.execute_script %Q{ $('.ui-menu-item a:contains("#{text}")').trigger("mouseenter").trigger("click"); }
6
+ end
@@ -0,0 +1,14 @@
1
+ require 'rails/generators'
2
+
3
+ module Autocomplete
4
+ class InstallGenerator < Rails::Generators::Base
5
+ def install
6
+ # Copy the unobtrusive JS file
7
+ copy_file('autocomplete-rails.js', 'public/javascripts/autocomplete-rails.js')
8
+ end
9
+
10
+ def self.source_root
11
+ File.join(File.dirname(__FILE__), '..', '..', 'assets', 'javascripts')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require 'rails/generators'
2
+
3
+ module Autocomplete
4
+ class UncompressedGenerator < Rails::Generators::Base
5
+ def install
6
+ # Copy the unobtrusive JS file
7
+ copy_file('autocomplete-rails-uncompressed.js', 'public/javascripts/autocomplete-rails.js')
8
+ end
9
+
10
+ def self.source_root
11
+ File.join(File.dirname(__FILE__), '..', '..', 'assets', 'javascripts')
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,23 @@
1
+ require 'rails-jquery-autocomplete/form_helper'
2
+ require 'rails-jquery-autocomplete/autocomplete'
3
+
4
+ module RailsJQueryAutocomplete
5
+ autoload :Orm , 'rails-jquery-autocomplete/orm'
6
+ autoload :FormtasticPlugin , 'rails-jquery-autocomplete/formtastic_plugin'
7
+
8
+ unless ::Rails.version < "3.1"
9
+ require 'rails-jquery-autocomplete/rails/engine'
10
+ end
11
+ end
12
+
13
+ class ActionController::Base
14
+ include RailsJQueryAutocomplete::Autocomplete
15
+ end
16
+
17
+ require 'rails-jquery-autocomplete/formtastic'
18
+
19
+ begin
20
+ require 'simple_form'
21
+ require 'rails-jquery-autocomplete/simple_form_plugin'
22
+ rescue LoadError
23
+ end
@@ -0,0 +1,112 @@
1
+ module RailsJQueryAutocomplete
2
+ module Autocomplete
3
+ def self.included(target)
4
+ target.extend RailsJQueryAutocomplete::Autocomplete::ClassMethods
5
+
6
+ target.send :include, RailsJQueryAutocomplete::Orm::Mongoid if defined?(Mongoid::Document)
7
+ target.send :include, RailsJQueryAutocomplete::Orm::MongoMapper if defined?(MongoMapper::Document)
8
+ target.send :include, RailsJQueryAutocomplete::Orm::ActiveRecord
9
+
10
+ end
11
+
12
+ #
13
+ # Usage:
14
+ #
15
+ # class ProductsController < Admin::BaseController
16
+ # autocomplete :brand, :name
17
+ # end
18
+ #
19
+ # This will magically generate an action autocomplete_brand_name, so,
20
+ # don't forget to add it on your routes file
21
+ #
22
+ # resources :products do
23
+ # get :autocomplete_brand_name, :on => :collection
24
+ # end
25
+ #
26
+ # Now, on your view, all you have to do is have a text field like:
27
+ #
28
+ # f.text_field :brand_name, :autocomplete => autocomplete_brand_name_products_path
29
+ #
30
+ #
31
+ # Yajl is used by default to encode results, if you want to use a different encoder
32
+ # you can specify your custom encoder via block
33
+ #
34
+ # class ProductsController < Admin::BaseController
35
+ # autocomplete :brand, :name do |items|
36
+ # CustomJSONEncoder.encode(items)
37
+ # end
38
+ # end
39
+ #
40
+ module ClassMethods
41
+ def autocomplete(object, method, options = {}, &block)
42
+
43
+ define_method("get_prefix") do |model|
44
+ if defined?(Mongoid::Document) && model.include?(Mongoid::Document)
45
+ 'active_record'
46
+ end
47
+ end
48
+
49
+ define_method("get_autocomplete_order") do |method, options, model=nil|
50
+ method("#{get_prefix(get_object(options[:class_name] || object))}_get_autocomplete_order").call(method, options, model)
51
+ end
52
+
53
+ define_method("get_autocomplete_items") do |parameters|
54
+ method("#{get_prefix(get_object(options[:class_name] || object))}_get_autocomplete_items").call(parameters)
55
+ end
56
+
57
+ define_method("autocomplete_#{object}_#{method}") do
58
+ method = options[:column_name] if options.has_key?(:column_name)
59
+
60
+ term = params[:term]
61
+
62
+ if term && !term.blank?
63
+ #allow specifying fully qualified class name for model object
64
+ class_name = options[:class_name] || object
65
+ items = get_autocomplete_items(:model => get_object(class_name), \
66
+ :options => options, :term => term, :method => method)
67
+ else
68
+ items = {}
69
+ end
70
+
71
+ render :json => json_for_autocomplete(items, options[:display_value] ||= method, options[:extra_data], &block), root: false
72
+ end
73
+ end
74
+ end
75
+
76
+ # Returns a limit that will be used on the query
77
+ def get_autocomplete_limit(options)
78
+ options[:limit] ||= 10
79
+ end
80
+
81
+ # Returns parameter model_sym as a constant
82
+ #
83
+ # get_object(:actor)
84
+ # # returns a Actor constant supposing it is already defined
85
+ #
86
+ def get_object(model_sym)
87
+ model_sym.to_s.camelize.constantize
88
+ end
89
+
90
+ #
91
+ # Returns a hash with three keys actually used by the Autocomplete jQuery-ui
92
+ # Can be overriden to show whatever you like
93
+ # Hash also includes a key/value pair for each method in extra_data
94
+ #
95
+ def json_for_autocomplete(items, method, extra_data=[])
96
+ items = items.collect do |item|
97
+ hash = HashWithIndifferentAccess.new({"id" => item.id.to_s, "label" => item.send(method), "value" => item.send(method)})
98
+ extra_data.each do |datum|
99
+ hash[datum] = item.send(datum)
100
+ end if extra_data
101
+ # TODO: Come back to remove this if clause when test suite is better
102
+ hash
103
+ end
104
+ if block_given?
105
+ yield(items)
106
+ else
107
+ items
108
+ end
109
+ end
110
+ end
111
+ end
112
+
@@ -0,0 +1,49 @@
1
+ module ActionView
2
+ module Helpers
3
+ module FormHelper
4
+ # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) and
5
+ # that is populated with jQuery's autocomplete plugin.
6
+ #
7
+ # ==== Examples
8
+ # autocomplete_field(:post, :title, author_autocomplete_path, :size => 20)
9
+ # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" data-autocomplete="author/autocomplete"/>
10
+ #
11
+ def autocomplete_field(object_name, method, source, options ={})
12
+ options["data-autocomplete"] = source
13
+ text_field(object_name, method, rewrite_autocomplete_option(options))
14
+ end
15
+ end
16
+
17
+ module FormTagHelper
18
+ # Creates a standard text field that can be populated with jQuery's autocomplete plugin
19
+ #
20
+ # ==== Examples
21
+ # autocomplete_field_tag 'address', '', address_autocomplete_path, :size => 75
22
+ # # => <input id="address" name="address" size="75" type="text" value="" data-autocomplete="address/autocomplete"/>
23
+ #
24
+ def autocomplete_field_tag(name, value, source, options ={})
25
+ options["data-autocomplete"] = source
26
+ text_field_tag(name, value, rewrite_autocomplete_option(options))
27
+ end
28
+ end
29
+
30
+ #
31
+ # Method used to rename the autocomplete key to a more standard
32
+ # data-autocomplete key
33
+ #
34
+ private
35
+ def rewrite_autocomplete_option(options)
36
+ options["data-autocomplete-fields"] = JSON.generate(options.delete :fields) if options[:fields]
37
+ options["data-update-elements"] = JSON.generate(options.delete :update_elements) if options[:update_elements]
38
+ options["data-id-element"] = options.delete :id_element if options[:id_element]
39
+ options["data-append-to"] = options.delete :append_to if options[:append_to]
40
+ options
41
+ end
42
+ end
43
+ end
44
+
45
+ class ActionView::Helpers::FormBuilder #:nodoc:
46
+ def autocomplete_field(method, source, options = {})
47
+ @template.autocomplete_field(@object_name, method, source, objectify_options(options))
48
+ end
49
+ end