pluginaweek-table_helper 0.2.1

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