ransack_ui 0.1.7 → 1.0.0

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