ransack_ui 0.1.7 → 1.0.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.
@@ -0,0 +1,26 @@
1
+ #
2
+ # Converts a select list into a bootstrap button-group with radio button behaviour
3
+ #
4
+ (($) ->
5
+ $.widget 'ransack.button_group_select',
6
+ options: {}
7
+
8
+ _create: ->
9
+ el = @element
10
+ val = el.val()
11
+ el.hide()
12
+
13
+ html = '<div class="btn-group btn-group-select" data-toggle="buttons-radio">'
14
+ el.find('option').each (i, o) ->
15
+ html += "<button class=\"btn#{if o.value == val then ' active' else ''}\" type=\"button\" value=\"#{o.value}\">#{o.text}</button>"
16
+
17
+ # Insert HTML after hidden select
18
+ el.after html
19
+
20
+ # Update select val when button is clicked
21
+ btn_group = el.next()
22
+ btn_group.on 'click', 'button.btn', (e) =>
23
+ @element.val $(e.currentTarget).val()
24
+ true
25
+
26
+ ) jQuery
@@ -0,0 +1,2 @@
1
+ #= require ransack_ui_jquery
2
+ #= require_tree .
@@ -1,5 +1,5 @@
1
1
  (($) ->
2
- $.widget 'ransack.search_form',
2
+ $.widget 'ransack.ransack_search_form',
3
3
  options: {}
4
4
 
5
5
  _create: ->
@@ -15,6 +15,9 @@
15
15
  @init_select2(containers)
16
16
  @store_initial_predicates(containers)
17
17
 
18
+ if $.ransack.button_group_select?
19
+ @init_button_group_select(@element)
20
+
18
21
  # show spinner and disable the form when the search is underway
19
22
  el.find("form input:submit").click $.proxy(@form_submit, this)
20
23
 
@@ -37,12 +40,7 @@
37
40
  # Handle association columns with AJAX autocomplete
38
41
  if selected.data('ajax-url') and Select2?
39
42
  controller = selected.data('controller')
40
-
41
- # Hide predicate Select2
42
- predicate_select2.hide()
43
- # Clear predicates, and set 'eq' predicate
44
- predicate_select.find('option').each (i, o) -> $(o).remove()
45
- predicate_select.append $('<option selected="selected" value="eq">is</option>')
43
+ @force_eq_predicate(base_id)
46
44
 
47
45
  # Set up Select2 for query input
48
46
  query_input.val('')
@@ -61,9 +59,18 @@
61
59
  results: (data, page) ->
62
60
  {results: $.map(data, (text, id) -> {id: id, text: text}) }
63
61
 
62
+ # Handle columns with options detected from validates :inclusion
63
+ else if selected.data('select-options') and Select2?
64
+ @force_eq_predicate(base_id)
65
+ query_input.val('')
66
+ query_input.select2
67
+ data: selected.data('select-options')
68
+ placeholder: "Please select a #{selected.val()}"
69
+ allowClear: true
70
+
64
71
  # Handle regular columns
65
72
  else
66
- predicate_select2.show()
73
+ predicate_select2.select2("enable")
67
74
  # If Select2 is on query input, remove and set defaults
68
75
  if (query_select2 = @element.find("#s2id_#{base_id}v_0_value")).length > 0
69
76
  query_input.select2('destroy')
@@ -139,6 +146,17 @@
139
146
  query_input.parent().find('.ui-datepicker-trigger').show()
140
147
 
141
148
 
149
+ # Dsiables predicate choices and sets it to 'eq'
150
+ force_eq_predicate: (base_id) ->
151
+ predicate_select = @element.find("select##{base_id}p")
152
+ predicate_select2 = @element.find("#s2id_#{base_id}p")
153
+ # Disable predicate Select2
154
+ predicate_select2.select2('disable')
155
+ # Clear predicates, and set 'eq' predicate
156
+ predicate_select.find('option').each (i, o) -> $(o).remove()
157
+ predicate_select.append $('<option selected="selected" value="eq">is</option>')
158
+ predicate_select2.select2('val', 'eq')
159
+
142
160
  # Attempts to find a predicate translation for the specific column type,
143
161
  # or returns the default label.
144
162
  # For example, 'lt' on an integer column will be translated to 'is less than',
@@ -208,9 +226,7 @@
208
226
  'value="'+value+'" style="display:none;" />')
209
227
 
210
228
  form_submit: (e) ->
211
- $("#loading").show()
212
229
  @element.css({ opacity: 0.4 })
213
- $('div.list').html('')
214
230
  true
215
231
 
216
232
  add_fields: (e) ->
@@ -222,7 +238,12 @@
222
238
  container = target.closest('p')
223
239
  container.before content.replace(regexp, new_id)
224
240
  prev_container = container.prev()
241
+
225
242
  @init_select2(prev_container)
243
+
244
+ if $.ransack.button_group_select?
245
+ @init_button_group_select(prev_container)
246
+
226
247
  @store_initial_predicates(prev_container)
227
248
  # Fire change event on any new selects.
228
249
  container.prev().find("select").change()
@@ -239,7 +260,7 @@
239
260
 
240
261
  store_initial_predicates: (container) ->
241
262
  # Store current predicates in data attribute
242
- predicate_select = container.find('select.ransack_predicate')
263
+ predicate_select = container.find('select.ransack_predicate:first')
243
264
  unless predicate_select.data['predicates']
244
265
  predicates = []
245
266
  predicate_select.find('option').each (i, o) ->
@@ -267,8 +288,17 @@
267
288
  # Avoid labels like 'Contact: Contact'
268
289
  if group_label == object.text
269
290
  object.text
270
- else
291
+ else if group_label?
271
292
  group_label + ': ' + object.text
293
+ else
294
+ object.text
295
+
296
+ @element.find('select.ransack_sort').select2
297
+ width: '230px'
298
+ placeholder: "Select a Field"
299
+
300
+ init_button_group_select: (containers) ->
301
+ containers.find('select.ransack_combinator, select.ransack_sort_order').button_group_select()
272
302
 
273
303
  init_datetimepicker: (base_id) ->
274
304
  if $.ui?.timepicker?
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= require_tree .
3
+ */
@@ -0,0 +1,32 @@
1
+ #ransack_search {
2
+ .remove_fields, .ransack_attribute, .ransack_predicate,
3
+ .ransack_query, .ransack_sort_order, .ransack_sort,
4
+ .btn-group-select {
5
+ margin: 0 5px;
6
+ vertical-align: middle;
7
+ margin-bottom: 0;
8
+ }
9
+
10
+ .btn-group-select { display: inline-block; }
11
+
12
+ .remove_fields {
13
+ margin-right: 4px;
14
+ img { margin-bottom: 4px; }
15
+ &:hover { opacity: 0.6; }
16
+ }
17
+ .remove_fields.btn {
18
+ padding: 0px 3px;
19
+ &:hover { opacity: 1; }
20
+ }
21
+
22
+ a.btn {
23
+ i { margin-bottom: 2px; }
24
+ span { margin-left: 5px; }
25
+ }
26
+
27
+ .combinator { margin-bottom: 20px; }
28
+
29
+ .filters > p {
30
+ margin-top: 20px;
31
+ }
32
+ }
@@ -1,6 +1,6 @@
1
1
  .fields.condition{ "data-object-name" => f.object_name }
2
2
  %p
3
- = link_to_remove_fields t(:advanced_search_remove_condition), f
3
+ = link_to_remove_fields t('ransack.remove_condition'), f, options
4
4
 
5
5
  = f.attribute_fields do |a|
6
6
  %span.fields{ "data-object-name" => f.object_name }
@@ -1,12 +1,13 @@
1
- .fields{ 'data-object-name' => f.object_name }
2
- %p
3
- - key = (f.object_name =~ /[0]/) ? :advanced_search_group_first : :advanced_search_group_rest
4
- = t(key, :combinator => f.combinator_select).html_safe
1
+ .fields.well{ 'data-object-name' => f.object_name }
2
+ .combinator
3
+ - key = (f.object_name =~ /[0]/) ? :group_first : :group_rest
4
+ - combinator = f.combinator_select({}, :class => 'ransack_combinator')
5
+ = t("ransack.#{key}", :combinator => combinator).html_safe
5
6
 
6
7
  .filters
7
8
  - f.object.build_condition unless f.object.conditions.any?
8
9
  = f.condition_fields do |c|
9
- = render 'ransack_ui/condition_fields', :f => c
10
+ = render 'ransack_ui/condition_fields', :f => c, :options => options
10
11
 
11
12
  %p
12
- = link_to_add_fields t(:advanced_search_add_condition), f, :condition
13
+ = link_to_add_fields t('ransack.add_condition'), f, :condition, options
@@ -1,16 +1,25 @@
1
- = search_form_for @ransack_search, :url => url_for(:action => :index), :html => {:method => :get, :class => "ransack_search"}, :remote => true do |f|
1
+ = search_form_for @ransack_search, :url => (options[:url] || url_for(:action => :index)),
2
+ :html => {:method => :get, :class => "ransack_search"}, :remote => !!options[:remote] do |f|
2
3
 
3
4
  :javascript
4
5
  if (window.Ransack == null) { window.Ransack = {}; }
5
6
  Ransack.alt_predicates_i18n = #{I18n.translate(:"ransack.predicates.alt", :default => {}).to_json}
6
7
 
7
- = f.grouping_fields do |g|
8
- = render 'ransack_ui/grouping_fields', :f => g
8
+ .row
9
+ .span12.well
10
+ = f.grouping_fields do |g|
11
+ = render 'ransack_ui/grouping_fields', :f => g, :options => options
9
12
 
10
- %p
11
- = link_to_add_fields t(:advanced_search_add_group), f, :grouping
13
+ %p
14
+ = link_to_add_fields t('ransack.add_group'), f, :grouping, options
12
15
 
13
- %p
14
- = hidden_field_tag :distinct, '1'
15
- = hidden_field_tag :page, '1'
16
- = f.submit t(:advanced_search_submit)
16
+ %hr
17
+
18
+ .pull-left
19
+ = f.sort_fields do |s|
20
+ = render 'ransack_ui/sort_fields', :f => s
21
+
22
+ .pull-right
23
+ = hidden_field_tag :distinct, '1'
24
+ = hidden_field_tag :page, '1'
25
+ = f.submit t('ransack.submit'), :class => 'btn btn-primary btn-large'
@@ -0,0 +1,3 @@
1
+ .fields.sort
2
+ %span.sort_by Sort by:
3
+ = f.sort_select
@@ -14,4 +14,11 @@ en:
14
14
  gt_all: "is after all"
15
15
  gteq: "is after or on"
16
16
  gteq_any: "is after or on any"
17
- gteq_all: "is after or on all"
17
+ gteq_all: "is after or on all"
18
+
19
+ submit: Search
20
+ add_group: Add a filter group
21
+ group_first: "Show results where %{combinator} of the following match:"
22
+ group_rest: "...and where %{combinator} of the following match:"
23
+ add_condition: Add a filter
24
+ remove_condition: Remove filter
@@ -11,7 +11,7 @@ module RansackUI
11
11
  def load_ransack_search(klass = nil)
12
12
  klass ||= controller_name.classify.constantize
13
13
  @ransack_search = klass.search(params[:q])
14
- @ransack_search.build_grouping unless @ransack_search.groupings.any?
14
+ @ransack_search.build_grouping if @ransack_search.groupings.empty?
15
15
  @ransack_search
16
16
  end
17
17
  end
@@ -20,7 +20,29 @@ module Ransack
20
20
  objectify_options(options), @default_options.merge(html_options)
21
21
  )
22
22
  else
23
- collection = object.context.searchable_attributes(bases.first).map do |c|
23
+ @template.select(
24
+ @object_name, :name, attribute_collection_for_base(bases.first),
25
+ objectify_options(options), @default_options.merge(html_options)
26
+ )
27
+ end
28
+ end
29
+
30
+ def sort_select(options = {}, html_options = {})
31
+ raise ArgumentError, "sort_select must be called inside a search FormBuilder!" unless object.respond_to?(:context)
32
+ options[:include_blank] = true unless options.has_key?(:include_blank)
33
+ bases = [''] + association_array(options[:associations])
34
+ if bases.size > 1
35
+ @template.select(
36
+ @object_name, :name,
37
+ @template.grouped_options_for_select(attribute_collection_for_bases(bases), object.name),
38
+ objectify_options(options), @default_options.merge({class: 'ransack_sort'}).merge(html_options)
39
+ ) + @template.collection_select(
40
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
41
+ objectify_options(options.except(:include_blank)), @default_options.merge({class: 'ransack_sort_order'}).merge(html_options)
42
+ )
43
+ else
44
+ # searchable_attributes now returns [c, type]
45
+ collection = object.context.searchable_attributes(bases.first).map do |c, type|
24
46
  [
25
47
  attr_from_base_and_column(bases.first, c),
26
48
  Translate.attribute(attr_from_base_and_column(bases.first, c), :context => object.context)
@@ -28,7 +50,10 @@ module Ransack
28
50
  end
29
51
  @template.collection_select(
30
52
  @object_name, :name, collection, :first, :last,
31
- objectify_options(options), @default_options.merge(html_options)
53
+ objectify_options(options), @default_options.merge({class: 'ransack_sort'}).merge(html_options)
54
+ ) + @template.collection_select(
55
+ @object_name, :dir, [['asc', object.translate('asc')], ['desc', object.translate('desc')]], :first, :last,
56
+ objectify_options(options.except(:include_blank)), @default_options.merge({class: 'ransack_sort_order'}).merge(html_options)
32
57
  )
33
58
  end
34
59
  end
@@ -49,63 +74,84 @@ module Ransack
49
74
 
50
75
  def attribute_collection_for_bases(bases)
51
76
  bases.map do |base|
52
- klass = object.context.traverse(base)
53
- foreign_keys = klass.reflect_on_all_associations.select(&:belongs_to?).
54
- map_to({}) {|r, h| h[r.foreign_key.to_sym] = r.class_name }
55
- ajax_options = Ransack.options[:ajax_options] || {}
56
-
57
- if base.present?
58
- model = object.context.traverse(base).model_name
77
+ if collection = attribute_collection_for_base(base)
78
+ [
79
+ Translate.association(base, :context => object.context),
80
+ collection
81
+ ]
59
82
  end
83
+ end.compact
84
+ end
60
85
 
61
- begin
62
- [
63
- Translate.association(base, :context => object.context),
64
- object.context.searchable_attributes(base).map do |c, type|
65
- # Don't show 'id' column for base model
66
- next nil if base.blank? && c == 'id'
67
-
68
- attribute = attr_from_base_and_column(base, c)
69
- attribute_label = Translate.attribute(attribute, :context => object.context)
70
-
71
- # Set model name as label for 'id' column on that model's table.
72
- if c == 'id'
73
- foreign_klass = object.context.traverse(base).model_name
74
- # Check that model can autocomplete. If not, skip this id column.
75
- next nil unless foreign_klass.constantize._ransack_can_autocomplete
76
- attribute_label = I18n.translate(foreign_klass, :default => foreign_klass)
77
- else
78
- foreign_klass = foreign_keys[c.to_sym]
79
- end
86
+ def attribute_collection_for_base(base)
87
+ klass = object.context.traverse(base)
88
+ foreign_keys = klass.reflect_on_all_associations.select(&:belongs_to?).
89
+ map_to({}) {|r, h| h[r.foreign_key.to_sym] = r.class_name }
90
+
91
+ ajax_options = Ransack.options[:ajax_options] || {}
80
92
 
81
- # Add column type as data attribute
82
- html_options = {:'data-type' => type}
83
- # Set 'base' attribute if attribute is on base model
84
- html_options[:'data-root-model'] = true if base.blank?
85
-
86
- if foreign_klass
87
- # If field is a foreign key, set up 'data-ajax-*' attributes for auto-complete
88
- controller = foreign_klass.tableize
89
- html_options[:'data-ajax-entity'] = I18n.translate(controller, :default => controller)
90
- if ajax_options[:url]
91
- html_options[:'data-ajax-url'] = ajax_options[:url].sub(':controller', controller)
92
- else
93
- html_options[:'data-ajax-url'] = "/#{controller}.json"
94
- end
95
- html_options[:'data-ajax-type'] = ajax_options[:type] || 'GET'
96
- html_options[:'data-ajax-key'] = ajax_options[:key] || 'query'
93
+ # Detect any inclusion validators to build list of options for a column
94
+ column_select_options = klass.validators.each_with_object({}) do |v, hash|
95
+ if v.is_a? ActiveModel::Validations::InclusionValidator
96
+ v.attributes.each do |a|
97
+ # Try to translate options from activerecord.attribute_options.<model>.<attribute>
98
+ hash[a.to_s] = v.send(:delimiter).each_with_object({}) do |o, options|
99
+ options[o] = I18n.translate("activerecord.attribute_options.#{klass.to_s.downcase}.#{a}.#{o}", :default => o)
97
100
  end
98
- [
99
- attribute_label,
100
- attribute,
101
- html_options
102
- ]
103
- end.compact
104
- ]
105
- rescue UntraversableAssociationError => e
106
- nil
101
+ end
102
+ end
103
+ end
104
+
105
+ object.context.searchable_attributes(base).map do |c, type|
106
+ # Don't show 'id' column for base model
107
+ next nil if base.blank? && c == 'id'
108
+
109
+ attribute = attr_from_base_and_column(base, c)
110
+ attribute_label = Translate.attribute(attribute, :context => object.context)
111
+
112
+ # Set model name as label for 'id' column on that model's table.
113
+ if c == 'id'
114
+ foreign_klass = object.context.traverse(base).model_name
115
+ # Check that model can autocomplete. If not, skip this id column.
116
+ next nil unless foreign_klass.constantize._ransack_can_autocomplete
117
+ attribute_label = I18n.translate(foreign_klass, :default => foreign_klass)
118
+ else
119
+ foreign_klass = foreign_keys[c.to_sym]
107
120
  end
121
+
122
+ # Add column type as data attribute
123
+ html_options = {:'data-type' => type}
124
+ # Set 'base' attribute if attribute is on base model
125
+ html_options[:'data-root-model'] = true if base.blank?
126
+ # Set column options if detected from inclusion validator
127
+ if column_select_options[c]
128
+ # Format options as an array of hashes with id and text columns, for Select2
129
+ html_options[:'data-select-options'] = column_select_options[c].map {|id, text|
130
+ {:id => id, :text => text}
131
+ }.to_json
132
+ end
133
+
134
+ if foreign_klass
135
+ # If field is a foreign key, set up 'data-ajax-*' attributes for auto-complete
136
+ controller = foreign_klass.tableize
137
+ html_options[:'data-ajax-entity'] = I18n.translate(controller, :default => controller)
138
+ if ajax_options[:url]
139
+ html_options[:'data-ajax-url'] = ajax_options[:url].sub(':controller', controller)
140
+ else
141
+ html_options[:'data-ajax-url'] = "/#{controller}.json"
142
+ end
143
+ html_options[:'data-ajax-type'] = ajax_options[:type] || 'GET'
144
+ html_options[:'data-ajax-key'] = ajax_options[:key] || 'query'
145
+ end
146
+
147
+ [
148
+ attribute_label,
149
+ attribute,
150
+ html_options
151
+ ]
108
152
  end.compact
153
+ rescue UntraversableAssociationError => e
154
+ nil
109
155
  end
110
156
  end
111
157
  end
@@ -1,3 +1,3 @@
1
1
  module RansackUI
2
- VERSION = "0.1.7"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,19 +1,30 @@
1
1
  module RansackUI
2
2
  module ViewHelpers
3
- def ransack_ui_search
4
- render 'ransack_ui/search'
3
+ def ransack_ui_search(options = {})
4
+ render 'ransack_ui/search', :options => options
5
5
  end
6
6
 
7
- def link_to_add_fields(name, f, type)
7
+ def link_to_add_fields(name, f, type, options)
8
8
  new_object = f.object.send "build_#{type}"
9
9
  fields = f.send("#{type}_fields", new_object, :child_index => "new_#{type}") do |builder|
10
- render "ransack_ui/#{type.to_s}_fields", :f => builder
10
+ render "ransack_ui/#{type.to_s}_fields", :f => builder, :options => options
11
+ end
12
+
13
+ if options[:theme].to_s == 'bootstrap'
14
+ link_to nil, :class => "add_fields btn btn-small btn-primary", "data-field-type" => type, "data-content" => "#{fields}" do
15
+ "<i class=\"icon-plus icon-white\"></i><span>#{name}</span>".html_safe
16
+ end
17
+ else
18
+ link_to name, nil, :class => "add_fields", "data-field-type" => type, "data-content" => "#{fields}"
11
19
  end
12
- link_to name, nil, :class => "add_fields", "data-field-type" => type, "data-content" => "#{fields}"
13
20
  end
14
21
 
15
- def link_to_remove_fields(name, f)
16
- link_to image_tag('ransack_ui/delete.png', :size => '16x16', :alt => name), nil, :class => "remove_fields"
22
+ def link_to_remove_fields(name, f, options)
23
+ if options[:theme].to_s == 'bootstrap'
24
+ link_to '<i class="icon-remove icon-white"></i>'.html_safe, nil, :class => "remove_fields btn btn-mini btn-danger"
25
+ else
26
+ link_to image_tag('ransack_ui/delete.png', :size => '16x16', :alt => name), nil, :class => "remove_fields"
27
+ end
17
28
  end
18
29
  end
19
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ransack_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 1.0.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-17 00:00:00.000000000 Z
12
+ date: 2012-12-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ransack_chronic
@@ -42,11 +42,16 @@ files:
42
42
  - app/assets/images/ransack_ui/calendar.png
43
43
  - app/assets/images/ransack_ui/delete.png
44
44
  - app/assets/javascripts/ransack/predicates.js.coffee
45
- - app/assets/javascripts/ransack_ui_jquery.js
45
+ - app/assets/javascripts/ransack_ui_bootstrap/button_group_select.js.coffee
46
+ - app/assets/javascripts/ransack_ui_bootstrap/index.js.coffee
47
+ - app/assets/javascripts/ransack_ui_jquery/index.js
46
48
  - app/assets/javascripts/ransack_ui_jquery/search_form.js.coffee.erb
49
+ - app/assets/stylesheets/ransack_ui_bootstrap/index.css
50
+ - app/assets/stylesheets/ransack_ui_bootstrap/search.css.scss
47
51
  - app/views/ransack_ui/_condition_fields.html.haml
48
52
  - app/views/ransack_ui/_grouping_fields.html.haml
49
53
  - app/views/ransack_ui/_search.html.haml
54
+ - app/views/ransack_ui/_sort_fields.html.haml
50
55
  - config/locales/en.yml
51
56
  - lib/core_ext/enumerable.rb
52
57
  - lib/ransack_ui.rb
@@ -76,7 +81,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
76
81
  version: '0'
77
82
  segments:
78
83
  - 0
79
- hash: 1445871146542449388
84
+ hash: -2240639429364373770
80
85
  required_rubygems_version: !ruby/object:Gem::Requirement
81
86
  none: false
82
87
  requirements:
@@ -85,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
85
90
  version: '0'
86
91
  segments:
87
92
  - 0
88
- hash: 1445871146542449388
93
+ hash: -2240639429364373770
89
94
  requirements: []
90
95
  rubyforge_project:
91
96
  rubygems_version: 1.8.24