effective_datatables 1.4.3 → 1.5.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 (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