tangofoxtrot-table_helper 0.2.2

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,113 @@
1
+ require 'table_helper/body_row'
2
+
3
+ module TableHelper
4
+ # Represents the body of the table. In HTML, you can think of this as
5
+ # the <tbody> tag of the table.
6
+ class Body < HtmlElement
7
+ # The css class to apply for all rows in the body
8
+ cattr_accessor :empty_caption_class
9
+ @@empty_caption_class = 'ui-collection-empty'
10
+
11
+ # The table this body is a part of
12
+ attr_reader :table
13
+
14
+ # If set to :odd or :even, every odd or even-numbered row will have the
15
+ # alternate class appended to its html attributes. Default is nil.
16
+ attr_accessor :alternate
17
+
18
+ # The caption to display in the collection is empty
19
+ attr_accessor :empty_caption
20
+
21
+ def initialize(table) #:nodoc:
22
+ super()
23
+
24
+ @table = table
25
+ @empty_caption = 'No matches found.'
26
+ end
27
+
28
+ def alternate=(value) #:nodoc:
29
+ raise ArgumentError, 'alternate must be set to :odd or :even' if value && ![:odd, :even].include?(value)
30
+ @alternate = value
31
+ end
32
+
33
+ # Builds the body of the table. This includes the actual data that is
34
+ # generated for each object in the collection.
35
+ #
36
+ # +each+ expects a block that defines the data in each cell. Each
37
+ # iteration of the block will provide the row within the table, the object
38
+ # being rendered, and the index of the object. For example,
39
+ #
40
+ # t.rows.each do |row, post, index|
41
+ # row.title "<div class=\"wrapped\">#{post.title}</div>"
42
+ # row.category post.category.name
43
+ # end
44
+ #
45
+ # In addition, to specifying the data, you can also modify the html
46
+ # options of the row. For more information on doing this, see the
47
+ # BodyRow class.
48
+ #
49
+ # If the collection is empty and +empty_caption+ is set on the body,
50
+ # then the actual body will be replaced by a single row containing the
51
+ # html that was stored in +empty_caption+.
52
+ #
53
+ # == Default Values
54
+ #
55
+ # Whenever possible, the default value of a cell will be set to the
56
+ # object's attribute with the same name as the cell. For example,
57
+ # if a Post consists of the attribute +title+, then the cell for the
58
+ # title will be prepopulated with that attribute's value:
59
+ #
60
+ # t.rows.each do |row, post index|
61
+ # row.title post.title
62
+ # end
63
+ #
64
+ # <tt>row.title</tt> is already set to post.category so there's no need to
65
+ # manually set the value of that cell. However, it is always possible
66
+ # to override the default value like so:
67
+ #
68
+ # t.rows.each do |row, post, index|
69
+ # row.title link_to(post.title, post_url(post))
70
+ # row.category post.category.name
71
+ # end
72
+ def each(&block)
73
+ @builder = block
74
+ end
75
+
76
+ # Builds a row for an object in the table.
77
+ #
78
+ # The provided block should set the values for each cell in the row.
79
+ def build_row(object, index = table.collection.index(object))
80
+ row = BodyRow.new(object, self)
81
+ row.alternate = alternate ? index.send("#{alternate}?") : false
82
+ @builder.call(row.builder, object, index) if @builder
83
+ row.html
84
+ end
85
+
86
+ private
87
+ def tag_name
88
+ 'tbody'
89
+ end
90
+
91
+ def content
92
+ content = ''
93
+
94
+ if table.empty? && @empty_caption
95
+ # No objects to display
96
+ row = Row.new(self)
97
+ row[:class] = empty_caption_class
98
+
99
+ html_options = {}
100
+ html_options[:colspan] = table.header.columns.size if table.header.columns.size > 1
101
+ row.cell nil, @empty_caption, html_options
102
+
103
+ content << row.html
104
+ else
105
+ table.collection.each_with_index do |object, i|
106
+ content << build_row(object, i)
107
+ end
108
+ end
109
+
110
+ content
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,74 @@
1
+ require 'table_helper/row'
2
+
3
+ module TableHelper
4
+ # Represents a single row within the body of a table. The row can consist
5
+ # of either data cells or header cells.
6
+ #
7
+ # == Alternating rows
8
+ #
9
+ # Alternating rows can be automated by setting the +alternate+ property.
10
+ # For example,
11
+ #
12
+ # r = BodyRow.new(object, parent)
13
+ # r.alternate = true
14
+ class BodyRow < Row
15
+ # The css class to apply for all rows in the body
16
+ cattr_accessor :result_class
17
+ @@result_class = 'ui-collection-result'
18
+
19
+ # The css class to apply for rows that are marked as "alternate"
20
+ cattr_accessor :alternate_class
21
+ @@alternate_class = 'ui-state-alternate'
22
+
23
+ # True if this is an alternating row, otherwise false. Default is false.
24
+ attr_accessor :alternate
25
+
26
+ def initialize(object, parent) #:nodoc:
27
+ super(parent)
28
+
29
+ @alternate = false
30
+ self[:class] = "#{self[:class]} #{table.object_name} #{result_class}".strip
31
+
32
+ # For each column defined in the table, see if we can prepopulate the
33
+ # cell based on the data in the object. If not, we can at least
34
+ # provide shortcut accessors to the cell
35
+ table.header.column_names.each do |column|
36
+ value = object.respond_to?(column) ? object.send(column) : nil
37
+ cell(column, value)
38
+ end
39
+ end
40
+
41
+ # Generates the html for this row in additional to the border row
42
+ # (if specified)
43
+ def html
44
+ original_options = @html_options.dup
45
+ self[:class] = "#{self[:class]} #{alternate_class}".strip if alternate
46
+ html = super
47
+ @html_options = original_options
48
+ html
49
+ end
50
+
51
+ private
52
+ # Builds the row's cells based on the order of the columns in the
53
+ # header. If a cell cannot be found for a specific column, then a blank
54
+ # cell is rendered.
55
+ def content
56
+ number_to_skip = 0 # Keeps track of the # of columns to skip
57
+
58
+ html = ''
59
+ table.header.column_names.each do |column|
60
+ number_to_skip -= 1 and next if number_to_skip > 0
61
+
62
+ if cell = @cells[column]
63
+ number_to_skip = (cell[:colspan] || 1) - 1
64
+ else
65
+ cell = Cell.new(column, nil)
66
+ end
67
+
68
+ html << cell.html
69
+ end
70
+
71
+ html
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,60 @@
1
+ module TableHelper
2
+ # Represents a single cell within a table. This can either be a regular
3
+ # data cell (td) or a header cell (th). By default, all cells will have
4
+ # their name appended to the cell's class attribute.
5
+ #
6
+ # == Examples
7
+ #
8
+ # # Data cell
9
+ # c = Cell.new(:author, 'John Doe')
10
+ # c.html # => <td class="author">John Doe</td>
11
+ #
12
+ # # Header cell
13
+ # c = Cell.new(:author, 'Author Name')
14
+ # c.content_type = :header
15
+ # c.html
16
+ #
17
+ # # With namespace
18
+ # c = Cell.new(:author, :namespace => 'post')
19
+ # c.content_type = :header
20
+ # c.html # => <td class="post-author">Author</td>
21
+ class Cell < HtmlElement
22
+ # The css class to apply to empty cells
23
+ cattr_accessor :empty_class
24
+ @@empty_class = 'ui-state-empty'
25
+
26
+ # The content to display within the cell
27
+ attr_reader :content
28
+
29
+ # The type of content this cell represents (:data or :header)
30
+ attr_reader :content_type
31
+
32
+ def initialize(name, content = name.to_s.titleize, html_options = {}) #:nodoc
33
+ html_options, content = content, name.to_s.titleize if content.is_a?(Hash)
34
+ namespace = html_options.delete(:namespace)
35
+ super(html_options)
36
+
37
+ @content = content.to_s
38
+
39
+ if name
40
+ name = "#{namespace}-#{name}" unless namespace.blank?
41
+ self[:class] = "#{self[:class]} #{name}".strip
42
+ end
43
+ self[:class] = "#{self[:class]} #{empty_class}".strip if content.blank?
44
+
45
+ self.content_type = :data
46
+ end
47
+
48
+ # Indicates what type of content will be stored in this cell. This can
49
+ # be set to either :data or :header.
50
+ def content_type=(value)
51
+ raise ArgumentError, "content_type must be set to :data or :header, was: #{value.inspect}" unless [:data, :header].include?(value)
52
+ @content_type = value
53
+ end
54
+
55
+ private
56
+ def tag_name
57
+ @content_type == :data ? 'td' : 'th'
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,138 @@
1
+ require 'table_helper/html_element'
2
+ require 'table_helper/header'
3
+ require 'table_helper/body'
4
+ require 'table_helper/footer'
5
+
6
+ module TableHelper
7
+ # Represents a table that displays data for multiple objects within a
8
+ # collection.
9
+ class CollectionTable < HtmlElement
10
+ # The css class to apply for all menu bars
11
+ cattr_accessor :collection_class
12
+ @@collection_class = 'ui-collection'
13
+
14
+ # The class of objects that will be rendered in the table
15
+ attr_reader :klass
16
+
17
+ # The name of the objects in the collection. By default, this is the
18
+ # underscored name of the class of objects being rendered. This value is
19
+ # used for generates parts of the css classes in the table, such as rows
20
+ # and cells.
21
+ #
22
+ # For example, if the class is Post, then the object name will be "post".
23
+ attr_accessor :object_name
24
+
25
+ # The actual collection of objects being rendered in the table's body.
26
+ # This collection will also be used to help automatically determine what
27
+ # the class is.
28
+ attr_reader :collection
29
+
30
+ # The body of rows that will be generated for each object in the
31
+ # collection. The actual content for each row and the html options can
32
+ # be configured as well.
33
+ #
34
+ # See TableHelper::Body for more information.
35
+ #
36
+ # == Examples
37
+ #
38
+ # # Defining rows
39
+ # t.rows.each do |row, post, index|
40
+ # row.category post.category.name
41
+ # row.author post.author.name
42
+ # ...
43
+ # row[:style] = 'margin-top: 5px;'
44
+ # end
45
+ #
46
+ # # Customizing html options
47
+ # t.rows[:class] = 'pretty'
48
+ attr_reader :rows
49
+
50
+ # Creates a new table based on the objects within the given collection
51
+ def initialize(collection, klass = nil, html_options = {}) #:nodoc:
52
+ html_options, klass = klass, nil if klass.is_a?(Hash)
53
+ super(html_options)
54
+
55
+ @collection = collection
56
+ @klass = klass || default_class
57
+ @object_name = @klass ? @klass.name.split('::').last.underscore : nil
58
+ @html_options.reverse_merge!(:cellspacing => '0', :cellpadding => '0')
59
+
60
+ self[:class] = "#{self[:class]} #{@object_name.pluralize}".strip if @object_name
61
+ self[:class] = "#{self[:class]} #{collection_class}".strip
62
+
63
+ # Build actual content
64
+ @header = Header.new(self)
65
+ @rows = Body.new(self)
66
+ @footer = Footer.new(self)
67
+
68
+ yield self if block_given?
69
+ end
70
+
71
+ # Will any rows be generated in the table's body?
72
+ def empty?
73
+ collection.empty?
74
+ end
75
+
76
+ # Creates a new header cell with the given name. Headers must be defined
77
+ # in the order in which they will be rendered.
78
+ #
79
+ # The actual caption and html options for the header can be configured
80
+ # as well. The caption determines what will be displayed in each cell.
81
+ #
82
+ # == Examples
83
+ #
84
+ # # Adding headers
85
+ # t.header :title # => <th class="post-title">Title</th>
86
+ # t.header :title, 'The Title' # => <th class="post-title">The Title</th>
87
+ # t.header :title, :class => 'pretty' # => <th class="post-title pretty">Title</th>
88
+ # t.header :title, 'The Title', :class => 'pretty' # => <th class="post-title pretty">The Title</th>
89
+ #
90
+ # # Customizing html options
91
+ # t.header[:class] = 'pretty'
92
+ def header(*args)
93
+ args.empty? ? @header : @header.column(*args)
94
+ end
95
+
96
+ # Creates a new footer cell with the given name. Footers must be defined
97
+ # in the order in which they will be rendered.
98
+ #
99
+ # The actual caption and html options for the footer can be configured
100
+ # as well. The caption determines what will be displayed in each cell.
101
+ #
102
+ # == Examples
103
+ #
104
+ # # Adding footers
105
+ # t.footer :total # => <td class="post-total"></th>
106
+ # t.footer :total, 5 # => <td class="post-total">5</th>
107
+ # t.footer :total, :class => 'pretty' # => <td class="post-total pretty"></th>
108
+ # t.footer :total, 5, :class => 'pretty' # => <td class="post-total pretty">5</th>
109
+ #
110
+ # # Customizing html options
111
+ # t.footer[:class] = 'pretty'
112
+ def footer(*args)
113
+ args.empty? ? @footer : @footer.cell(*args)
114
+ end
115
+
116
+ private
117
+ # Finds the class representing the objects within the collection
118
+ def default_class
119
+ if collection.respond_to?(:proxy_reflection)
120
+ collection.proxy_reflection.klass
121
+ elsif !collection.empty?
122
+ collection.first.class
123
+ end
124
+ end
125
+
126
+ def tag_name
127
+ 'table'
128
+ end
129
+
130
+ def content
131
+ content = ''
132
+ content << @header.html unless @header.empty?
133
+ content << @rows.html
134
+ content << @footer.html unless @footer.empty?
135
+ content
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,52 @@
1
+ require 'table_helper/row'
2
+
3
+ module TableHelper
4
+ # Represents the header of the table. In HTML, you can think of this as
5
+ # the <tfoot> tag of the table.
6
+ class Footer < HtmlElement
7
+ # The table this footer is a part of
8
+ attr_reader :table
9
+
10
+ # The actual footer row
11
+ attr_reader :row
12
+
13
+ # Whether or not the footer should be hidden when the collection is
14
+ # empty. Default is true.
15
+ attr_accessor :hide_when_empty
16
+
17
+ delegate :cell, :empty?, :to => :row
18
+
19
+ def initialize(table) #:nodoc:
20
+ super()
21
+
22
+ @table = table
23
+ @row = Row.new(self)
24
+ @hide_when_empty = true
25
+ end
26
+
27
+ def html #:nodoc:
28
+ # Force the last cell to span the remaining columns
29
+ cells = row.cells.values
30
+ colspan = table.header.columns.length - cells[0..-2].inject(0) {|count, cell| count += (cell[:colspan] || 1).to_i}
31
+ cells.last[:colspan] ||= colspan if colspan > 1
32
+
33
+ html_options = @html_options.dup
34
+ html_options[:style] = "display: none; #{html_options[:style]}".strip if table.empty? && hide_when_empty
35
+
36
+ content_tag(tag_name, content, html_options)
37
+ end
38
+
39
+ private
40
+ def tag_name
41
+ 'tfoot'
42
+ end
43
+
44
+ # Generates the html for the footer. The footer generally consists of a
45
+ # summary of the data in the body. This row will be wrapped inside of
46
+ # a tfoot tag. If the collection is empty and hide_when_empty was set
47
+ # to true, then the footer will be hidden.
48
+ def content
49
+ @row.html
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,109 @@
1
+ require 'table_helper/row'
2
+
3
+ module TableHelper
4
+ # Represents the header of the table. In HTML, you can think of this as
5
+ # the <thead> tag of the table.
6
+ class Header < HtmlElement
7
+ # The table this header is a part of
8
+ attr_reader :table
9
+
10
+ # The actual header row
11
+ attr_reader :row
12
+
13
+ # Whether or not the header should be hidden when the collection is
14
+ # empty. Default is true.
15
+ attr_accessor :hide_when_empty
16
+
17
+ delegate :empty?, :to => :row
18
+
19
+ # Creates a new header for the given table.
20
+ #
21
+ # If the class is known, then the header will be pre-filled with
22
+ # the columns defined in that class (assuming it's an ActiveRecord
23
+ # class).
24
+ def initialize(table)
25
+ super()
26
+
27
+ @table = table
28
+ @row = Row.new(self)
29
+ @hide_when_empty = true
30
+
31
+ # If we know what class the objects in the collection are and we can
32
+ # figure out what columns are defined in that class, then we can
33
+ # pre-fill the header with those columns so that the user doesn't
34
+ # have to
35
+ klass = table.klass
36
+ if klass && klass.respond_to?(:column_names)
37
+ if !table.empty? && klass < ActiveRecord::Base
38
+ # Make sure only the attributes that have been loaded are used
39
+ column_names = table.collection.first.attributes.keys
40
+ else
41
+ # Assume all attributes are loaded
42
+ column_names = klass.column_names
43
+ end
44
+
45
+ column(*column_names.map(&:to_sym))
46
+ end
47
+
48
+ @customized = false
49
+ end
50
+
51
+ # The current columns in this header, in the order in which they will be
52
+ # built
53
+ def columns
54
+ row.cells
55
+ end
56
+
57
+ # Gets the names of all of the columns being displayed in the table
58
+ def column_names
59
+ row.cell_names
60
+ end
61
+
62
+ # Clears all of the current columns from the header
63
+ def clear
64
+ row.clear
65
+ end
66
+
67
+ # Creates one or more to columns in the header. This will clear any
68
+ # pre-existing columns if it is being customized for the first time after
69
+ # it was initially created.
70
+ def column(*names)
71
+ # Clear the header row if this is being customized by the user
72
+ unless @customized
73
+ @customized = true
74
+ clear
75
+ end
76
+
77
+ # Extract configuration
78
+ options = names.last.is_a?(Hash) ? names.pop : {}
79
+ content = names.last.is_a?(String) ? names.pop : nil
80
+ args = [content, options].compact
81
+
82
+ names.collect! do |name|
83
+ column = row.cell(name, *args)
84
+ column.content_type = :header
85
+ column[:scope] ||= 'col'
86
+ column
87
+ end
88
+
89
+ names.length == 1 ? names.first : names
90
+ end
91
+
92
+ # Creates and returns the generated html for the header
93
+ def html
94
+ html_options = @html_options.dup
95
+ html_options[:style] = 'display: none;' if table.empty? && hide_when_empty
96
+
97
+ content_tag(tag_name, content, html_options)
98
+ end
99
+
100
+ private
101
+ def tag_name
102
+ 'thead'
103
+ end
104
+
105
+ def content
106
+ @row.html
107
+ end
108
+ end
109
+ end