effective_datatables 1.4.3 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +25 -2
  3. data/app/assets/images/dataTables/sort_asc.png +0 -0
  4. data/app/assets/images/dataTables/sort_both.png +0 -0
  5. data/app/assets/images/dataTables/sort_desc.png +0 -0
  6. data/app/assets/javascripts/dataTables/bootstrap/2/jquery.dataTables.bootstrap.js +148 -0
  7. data/app/assets/javascripts/dataTables/bootstrap/3/jquery.dataTables.bootstrap.js +185 -0
  8. data/app/assets/javascripts/dataTables/dataTables.colReorder.min.js +26 -0
  9. data/app/assets/javascripts/dataTables/dataTables.colVis.min.js +24 -0
  10. data/app/assets/javascripts/dataTables/dataTables.fixedColumns.min.js +30 -0
  11. data/app/assets/javascripts/dataTables/dataTables.tableTools.min.js +70 -0
  12. data/app/assets/javascripts/dataTables/jquery.dataTables.min.js +160 -0
  13. data/app/assets/javascripts/effective_datatables.bootstrap2.js +6 -5
  14. data/app/assets/javascripts/effective_datatables.js +6 -5
  15. data/app/assets/javascripts/effective_datatables/initialize.js.coffee.erb +52 -39
  16. data/app/assets/javascripts/vendor/jquery.debounce.min.js +9 -0
  17. data/app/assets/stylesheets/dataTables/bootstrap/2/jquery.dataTables.bootstrap.scss +207 -0
  18. data/app/assets/stylesheets/dataTables/bootstrap/3/jquery.dataTables.bootstrap.scss +280 -0
  19. data/app/assets/stylesheets/dataTables/dataTables.colReorder.min.css +1 -0
  20. data/app/assets/stylesheets/dataTables/dataTables.colVis.min.css +1 -0
  21. data/app/assets/stylesheets/dataTables/dataTables.fixedColumns.min.css +1 -0
  22. data/app/assets/stylesheets/dataTables/dataTables.tableTools.min.css +1 -0
  23. data/app/assets/stylesheets/dataTables/jquery.dataTables.min.css +1 -0
  24. data/app/assets/stylesheets/effective_datatables.bootstrap2.scss +4 -3
  25. data/app/assets/stylesheets/effective_datatables.scss +4 -3
  26. data/app/assets/stylesheets/effective_datatables/_overrides.scss.erb +71 -63
  27. data/app/controllers/effective/datatables_controller.rb +4 -4
  28. data/app/helpers/effective_datatables_helper.rb +24 -30
  29. data/app/models/effective/active_record_datatable_tool.rb +6 -6
  30. data/app/models/effective/array_datatable_tool.rb +18 -8
  31. data/app/models/effective/datatable.rb +98 -44
  32. data/app/views/effective/datatables/_datatable.html.haml +41 -15
  33. data/lib/effective_datatables.rb +0 -1
  34. data/lib/effective_datatables/version.rb +1 -1
  35. metadata +20 -17
  36. data/app/assets/javascripts/vendor/jquery.dataTables.columnFilter.js +0 -832
@@ -25,10 +25,10 @@ module Effective
25
25
 
26
26
  def error_json
27
27
  {
28
- :sEcho => params[:sEcho].to_i,
29
- :aaData => [],
30
- :iTotalRecords => 0,
31
- :iTotalDisplayRecords => 0,
28
+ :draw => params[:draw].to_i,
29
+ :data => [],
30
+ :recordsTotal => 0,
31
+ :recordsFiltered => 0,
32
32
  }.to_json
33
33
  end
34
34
 
@@ -1,50 +1,40 @@
1
1
  module EffectiveDatatablesHelper
2
2
  def render_datatable(datatable, opts = {}, &block)
3
3
  datatable.view = self
4
+ locals = {:style => :full, :filterable => true, :sortable => true, :table_class => 'table-bordered table-striped'}.merge(opts)
4
5
 
5
- locals = {:style => :full, :filterable => true, :sortable => true, :table_class => 'table-bordered table-striped'}
6
- locals = locals.merge(opts) if opts.kind_of?(Hash)
7
- locals[:table_class] = 'sorting-hidden ' + locals[:table_class].to_s if locals[:sortable] == false
8
-
9
- # Do we have to look at empty? behaviour
10
- if (block_given? || opts.kind_of?(String) || (opts.kind_of?(Hash) && opts[:empty].present?)) && datatable.empty?
11
- if block_given?
12
- yield; nil
13
- elsif opts.kind_of?(String)
14
- opts
15
- elsif opts.kind_of?(Hash) && opts[:empty].present?
16
- opts[:empty]
17
- end
18
- else
19
- render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
20
- end
6
+ render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
21
7
  end
22
8
 
23
9
  def render_simple_datatable(datatable, opts = {})
24
10
  datatable.view = self
25
- locals = {:style => :simple, :filterable => false, :sortable => false, :table_class => ''}.merge(opts)
26
- locals[:table_class] = 'sorting-hidden ' + locals[:table_class].to_s if locals[:sortable] == false
11
+ datatable.per_page = :all
12
+ locals = {:style => :simple, :filterable => false, :sortable => false, :table_class => 'table-bordered table-striped sorting-hidden'}.merge(opts)
27
13
 
28
14
  render :partial => 'effective/datatables/datatable', :locals => locals.merge(:datatable => datatable)
29
15
  end
30
16
 
31
- def datatable_filter(datatable, filterable = true)
32
- return false unless filterable
33
-
34
- filters = datatable.table_columns.values.map { |options, _| options[:filter] || {:type => 'null'} }
17
+ def render_datatable_header_cell(form, name, opts, filterable = true)
18
+ return content_tag(:p, opts[:label] || name) if filterable == false
35
19
 
36
- # Process any Procs
37
- filters.each do |filter|
38
- if filter[:values].respond_to?(:call)
39
- filter[:values] = filter[:values].call()
20
+ case opts[:filter][:type]
21
+ when :string, :text, :number
22
+ form.input name, :label => false, :required => false, :as => :string, :placeholder => (opts[:label] || name),
23
+ :input_html => { :autocomplete => 'off', :data => {'column-name' => opts[:name], 'column-index' => opts[:index]} }
24
+ when :select, :boolean
25
+ if opts[:filter][:values].respond_to?(:call)
26
+ opts[:filter][:values] = opts[:filter][:values].call()
40
27
 
41
- if filter[:values].kind_of?(ActiveRecord::Relation) || (filter[:values].kind_of?(Array) && filter[:values].first.kind_of?(ActiveRecord::Base))
42
- filter[:values] = filter[:values].map { |obj| [obj.id, obj.to_s] }
28
+ if opts[:filter][:values].kind_of?(ActiveRecord::Relation) || (opts[:filter][:values].kind_of?(Array) && opts[:filter][:values].first.kind_of?(ActiveRecord::Base))
29
+ opts[:filter][:values] = opts[:filter][:values].map { |obj| [obj.to_s, obj.id] }
43
30
  end
44
31
  end
45
- end
46
32
 
47
- filters.to_json()
33
+ form.input name, :label => false, :required => false, :as => :select, :collection => opts[:filter][:values], :include_blank => (opts[:label] || name.titleize),
34
+ :input_html => { :autocomplete => 'off', :data => {'column-name' => opts[:name], 'column-index' => opts[:index]} }
35
+ else
36
+ content_tag(:p, opts[:label] || name)
37
+ end
48
38
  end
49
39
 
50
40
  def datatable_non_sortable(datatable, sortable = true)
@@ -83,6 +73,10 @@ module EffectiveDatatablesHelper
83
73
  end.to_json()
84
74
  end
85
75
 
76
+ def datatable_column_names(datatable)
77
+ datatable.table_columns.values.map { |options| {:name => options[:name], :targets => options[:index] } }.to_json()
78
+ end
79
+
86
80
  def datatables_admin_path?
87
81
  @datatables_admin_path ||= (
88
82
  path = request.path.to_s.downcase.chomp('/') + '/'
@@ -2,25 +2,25 @@ module Effective
2
2
  class ActiveRecordDatatableTool
3
3
  attr_accessor :table_columns
4
4
 
5
- delegate :order_column_index, :order_direction, :page, :per_page, :search_column, :to => :@datatable
5
+ delegate :order_name, :order_direction, :page, :per_page, :search_column, :to => :@datatable
6
6
 
7
7
  def initialize(datatable, table_columns)
8
8
  @datatable = datatable
9
9
  @table_columns = table_columns
10
10
  end
11
11
 
12
- def order_column
13
- @order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
14
- end
15
-
16
12
  def search_terms
17
13
  @search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
18
14
  end
19
15
 
16
+ def order_column
17
+ @order_column ||= table_columns[order_name]
18
+ end
19
+
20
20
  def order(collection)
21
21
  return collection if order_column.blank?
22
22
 
23
- if [:string, :text].include?(order_column[:type])
23
+ if [:string, :text].include?(order_column[:type]) && order_column[:sql_as_column] != true
24
24
  collection.order("COALESCE(#{order_column[:column]}, '') #{order_direction}")
25
25
  else
26
26
  collection.order("#{order_column[:column]} #{order_direction} NULLS #{order_direction == 'ASC' ? 'LAST' : 'FIRST'}")
@@ -3,27 +3,29 @@ module Effective
3
3
  class ArrayDatatableTool
4
4
  attr_accessor :table_columns
5
5
 
6
- delegate :order_column_index, :order_direction, :page, :per_page, :search_column, :to => :@datatable
6
+ delegate :order_name, :order_direction, :page, :per_page, :search_column, :display_table_columns, :to => :@datatable
7
7
 
8
8
  def initialize(datatable, table_columns)
9
9
  @datatable = datatable
10
10
  @table_columns = table_columns
11
11
  end
12
12
 
13
- def order_column
14
- @order_column ||= table_columns.find { |_, values| values[:index] == order_column_index }.try(:second) # This pulls out the values
15
- end
16
-
17
13
  def search_terms
18
14
  @search_terms ||= @datatable.search_terms.select { |name, search_term| table_columns.key?(name) }
19
15
  end
20
16
 
17
+ def order_column
18
+ @order_column ||= table_columns[order_name]
19
+ end
20
+
21
21
  def order(collection)
22
22
  if order_column.present?
23
+ index = display_index(order_column)
24
+
23
25
  if order_direction == 'ASC'
24
- collection.sort! { |x, y| x[order_column[:index]] <=> y[order_column[:index]] }
26
+ collection.sort! { |x, y| x[index] <=> y[index] }
25
27
  else
26
- collection.sort! { |x, y| y[order_column[:index]] <=> x[order_column[:index]] }
28
+ collection.sort! { |x, y| y[index] <=> x[index] }
27
29
  end
28
30
  end
29
31
 
@@ -41,9 +43,10 @@ module Effective
41
43
 
42
44
  def search_column_with_defaults(collection, table_column, search_term)
43
45
  search_term = search_term.downcase
46
+ index = display_index(table_column)
44
47
 
45
48
  collection.select! do |row|
46
- value = row[table_column[:index]].to_s.downcase
49
+ value = row[index].to_s.downcase
47
50
 
48
51
  if table_column[:filter][:type] == :select && table_column[:filter][:fuzzy] != true
49
52
  value == search_term
@@ -56,6 +59,13 @@ module Effective
56
59
  def paginate(collection)
57
60
  Kaminari.paginate_array(collection).page(page).per(per_page)
58
61
  end
62
+
63
+ private
64
+
65
+ def display_index(column)
66
+ (display_table_columns || table_columns).keys.index(column[:name])
67
+ end
68
+
59
69
  end
60
70
  end
61
71
 
@@ -22,6 +22,7 @@ module Effective
22
22
  end
23
23
  raise "You cannot use both :partial => '' and proc => ..." if options[:partial] && options[:proc]
24
24
 
25
+ send(:attr_accessor, name)
25
26
  (@table_columns ||= HashWithIndifferentAccess.new())[name] = options
26
27
  end
27
28
 
@@ -44,17 +45,25 @@ module Effective
44
45
  def default_entries(entries)
45
46
  @default_entries = entries
46
47
  end
48
+
49
+ def model_name # Searching & Filters
50
+ @model_name ||= ActiveModel::Name.new(self)
51
+ end
47
52
  end
48
53
 
54
+
49
55
  def initialize(*args)
56
+ unless active_record_collection? || (collection.kind_of?(Array) && collection.first.kind_of?(Array))
57
+ raise "Unsupported collection type. Should be ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'something'], [2, 'something else']]"
58
+ end
59
+
50
60
  if args.present?
51
61
  raise 'Effective::Datatable.new() can only be called with a Hash like arguments' unless args.first.kind_of?(Hash)
52
62
  args.first.each { |k, v| self.attributes[k] = v }
53
63
  end
54
64
 
55
- unless active_record_collection? || (collection.kind_of?(Array) && collection.first.kind_of?(Array))
56
- raise "Unsupported collection type. Should be ActiveRecord class, ActiveRecord relation, or an Array of Arrays [[1, 'something'], [2, 'something else']]"
57
- end
65
+ # Any pre-selected search terms should be assigned now
66
+ search_terms.each { |column, term| self.send("#{column}=", term) }
58
67
  end
59
68
 
60
69
  # Any attributes set on initialize will be echoed back and available to the class
@@ -62,6 +71,13 @@ module Effective
62
71
  @attributes ||= HashWithIndifferentAccess.new()
63
72
  end
64
73
 
74
+ def to_key; []; end # Searching & Filters
75
+
76
+ # Instance method. In Rails 4.2 this needs to be defined on the instance, before it was on the class
77
+ def model_name # Searching & Filters
78
+ @model_name ||= ActiveModel::Name.new(self.class)
79
+ end
80
+
65
81
  def to_param
66
82
  self.class.name.underscore.parameterize
67
83
  end
@@ -85,14 +101,27 @@ module Effective
85
101
  end.each_with_index { |(_, col), index| col[:index] = index }
86
102
  end
87
103
 
104
+ # This is for the ColReorder plugin
105
+ # It sends us a list of columns that are different than our table_columns order
106
+ # So this method just returns an array of column names, as per ColReorder
107
+ def display_table_columns
108
+ if params[:columns].present?
109
+ HashWithIndifferentAccess.new().tap do |display_columns|
110
+ params[:columns].each do |_, values|
111
+ display_columns[values[:name]] = table_columns[values[:name]]
112
+ end
113
+ end
114
+ end
115
+ end
116
+
88
117
  def to_json
89
118
  raise 'Effective::Datatable to_json called with a nil view. Please call render_datatable(@datatable) or @datatable.view = view before this method' unless view.present?
90
119
 
91
120
  @json ||= {
92
- :sEcho => (params[:sEcho] || 0),
93
- :aaData => (table_data || []),
94
- :iTotalRecords => (total_records || 0),
95
- :iTotalDisplayRecords => (display_records || 0)
121
+ :draw => (params[:draw] || 0),
122
+ :data => (table_data || []),
123
+ :recordsTotal => (total_records || 0),
124
+ :recordsFiltered => (display_records || 0)
96
125
  }
97
126
  end
98
127
 
@@ -104,20 +133,20 @@ module Effective
104
133
  total_records.to_i == 0
105
134
  end
106
135
 
107
- # Wish these were protected
108
- def order_column_index
109
- if params[:iSortCol_0].present?
110
- params[:iSortCol_0].to_i
111
- elsif default_order.present?
112
- (table_columns[default_order.keys.first.to_s] || {}).fetch(:index, 0)
113
- else
114
- 0
136
+ def order_name
137
+ @order_name ||= begin
138
+ if params[:order] && params[:columns]
139
+ order_column_index = (params[:order].first[1][:column] rescue '0')
140
+ (params[:columns][order_column_index] || {})[:name]
141
+ elsif default_order.present?
142
+ default_order.keys.first
143
+ end || table_columns.keys.first
115
144
  end
116
145
  end
117
146
 
118
147
  def order_direction
119
- if params[:sSortDir_0].present?
120
- params[:sSortDir_0].try(:downcase) == 'desc' ? 'DESC' : 'ASC'
148
+ @order_direction ||= if params[:order].present?
149
+ params[:order].first[1][:dir] == 'desc' ? 'DESC' : 'ASC'
121
150
  elsif default_order.present?
122
151
  default_order.values.first.to_s.downcase == 'desc' ? 'DESC' : 'ASC'
123
152
  else
@@ -139,18 +168,15 @@ module Effective
139
168
 
140
169
  def search_terms
141
170
  @search_terms ||= HashWithIndifferentAccess.new().tap do |terms|
142
- if params[:sEcho].present?
143
- table_columns.keys.each_with_index do |col, x|
144
- unless (params["sVisible_#{x}"] == 'false' && table_columns[col][:filter][:when_hidden] != true)
145
- terms[col] = params["sSearch_#{x}"] if params["sSearch_#{x}"].present?
146
- end
171
+ if params[:columns].present? # This is an AJAX request from the DataTable
172
+ (params[:columns] || {}).each do |_, column|
173
+ next if table_columns[column[:name]].blank? || (column[:search] || {})[:value].blank?
174
+
175
+ terms[column[:name]] = column[:search][:value]
147
176
  end
148
- else
149
- # We are in the initial render and have to apply default search terms only
177
+ else # This is the initial render, and we have to apply default search terms only
150
178
  table_columns.each do |name, values|
151
- if (values[:filter][:selected].present?) && (values[:visible] != false || values[:filter][:when_hidden] == true)
152
- terms[name] = values[:filter][:selected]
153
- end
179
+ terms[name] = values[:filter][:selected] if values[:filter][:selected].present?
154
180
  end
155
181
  end
156
182
  end
@@ -166,7 +192,7 @@ module Effective
166
192
  end
167
193
 
168
194
  def per_page
169
- length = (params[:iDisplayLength].presence || default_entries).to_i
195
+ length = (params[:length].presence || default_entries).to_i
170
196
 
171
197
  if length == -1
172
198
  9999999
@@ -177,8 +203,17 @@ module Effective
177
203
  end
178
204
  end
179
205
 
206
+ def per_page=(length)
207
+ case length
208
+ when Integer
209
+ params[:length] = length
210
+ when :all
211
+ params[:length] = -1
212
+ end
213
+ end
214
+
180
215
  def page
181
- params[:iDisplayStart].to_i / per_page + 1
216
+ params[:start].to_i / per_page + 1
182
217
  end
183
218
 
184
219
  def total_records
@@ -206,8 +241,14 @@ module Effective
206
241
  (self.class.instance_methods(false) - [:collection, :search_column]).each do |view_method|
207
242
  @view.class_eval { delegate view_method, :to => :@effective_datatable }
208
243
  end
244
+
245
+ # Clear the search_terms memoization
246
+ @search_terms = nil
247
+ @order_name = nil
248
+ @order_direction = nil
209
249
  end
210
250
 
251
+
211
252
  protected
212
253
 
213
254
  # So the idea here is that we want to do as much as possible on the database in ActiveRecord
@@ -267,7 +308,7 @@ module Effective
267
308
  end
268
309
 
269
310
  collection.each_with_index.map do |obj, index|
270
- table_columns.map do |name, opts|
311
+ (display_table_columns || table_columns).map do |name, opts|
271
312
  value = if opts[:partial]
272
313
  rendered[name][index]
273
314
  elsif opts[:block]
@@ -314,7 +355,11 @@ module Effective
314
355
  end
315
356
 
316
357
  def active_record_collection?
317
- @active_record_collection ||= (collection.ancestors.include?(ActiveRecord::Base) rescue false)
358
+ if @active_record_collection.nil?
359
+ @active_record_collection = (collection.ancestors.include?(ActiveRecord::Base) rescue false)
360
+ else
361
+ @active_record_collection
362
+ end
318
363
  end
319
364
 
320
365
  def table_columns_with_defaults
@@ -334,7 +379,7 @@ module Effective
334
379
  belong_tos = (collection.ancestors.first.reflect_on_all_associations(:belongs_to) rescue []).inject(HashWithIndifferentAccess.new()) do |retval, bt|
335
380
  unless bt.options[:polymorphic]
336
381
  begin
337
- klass = bt.klass || bt.foreign_type.gsub('_type', '').classify.constantize
382
+ klass = bt.klass || bt.foreign_type.sub('_type', '').classify.constantize
338
383
  rescue => e
339
384
  klass = nil
340
385
  end
@@ -370,6 +415,10 @@ module Effective
370
415
  cols[name][:type] = :obfuscated_id
371
416
  end
372
417
 
418
+ if sql_table.present? && sql_column.blank? # This is a SELECT AS column
419
+ cols[name][:sql_as_column] = true
420
+ end
421
+
373
422
  cols[name][:filter] = initialize_table_column_filter(cols[name][:filter], cols[name][:type], belong_tos[name])
374
423
 
375
424
  if cols[name][:partial]
@@ -379,33 +428,38 @@ module Effective
379
428
  end
380
429
 
381
430
  def initialize_table_column_filter(filter, col_type, belongs_to)
382
- return {:type => :null, :when_hidden => false} if filter == false
431
+ return {:type => :null} if filter == false
383
432
 
384
433
  if filter.kind_of?(Symbol)
385
- filter = {:type => filter}
434
+ filter = HashWithIndifferentAccess.new(:type => filter)
386
435
  elsif filter.kind_of?(String)
387
- filter = {:type => filter.to_sym}
436
+ filter = HashWithIndifferentAccess.new(:type => filter.to_sym)
388
437
  elsif filter.kind_of?(Hash) == false
389
- filter = {}
438
+ filter = HashWithIndifferentAccess.new()
390
439
  end
391
440
 
392
441
  # This is a fix for passing filter[:selected] == false, it needs to be 'false'
393
442
  filter[:selected] = filter[:selected].to_s unless filter[:selected].nil?
394
443
 
395
- case col_type # null, number, select, number-range, date-range, checkbox, text(default)
444
+ filter = case col_type
396
445
  when :belongs_to
397
- {
446
+ HashWithIndifferentAccess.new(
398
447
  :type => :select,
399
- :when_hidden => false,
400
- :values => Proc.new { belongs_to[:klass].all.map { |obj| [obj.id, obj.to_s] }.sort { |x, y| x[1] <=> y[1] } }
401
- }.merge(filter)
448
+ :values => Proc.new { belongs_to[:klass].all.map { |obj| [obj.to_s, obj.id] }.sort { |x, y| x[1] <=> y[1] } }
449
+ ).merge(filter)
402
450
  when :integer
403
- {:type => :number, :when_hidden => false}.merge(filter)
451
+ HashWithIndifferentAccess.new(:type => :number).merge(filter)
404
452
  when :boolean
405
- {:type => :select, :when_hidden => false, :values => [true, false]}.merge(filter)
453
+ HashWithIndifferentAccess.new(:type => :boolean, :values => [true, false]).merge(filter)
406
454
  else
407
- {:type => :text, :when_hidden => false}.merge(filter)
455
+ HashWithIndifferentAccess.new(:type => :string).merge(filter)
456
+ end
457
+
458
+ if filter[:type] == :boolean
459
+ filter = HashWithIndifferentAccess.new(:values => [true, false]).merge(filter)
408
460
  end
461
+
462
+ filter
409
463
  end
410
464
 
411
465
  end