grapple 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +182 -0
- data/Rakefile +9 -0
- data/app/assets/images/grapple/arrow-down.png +0 -0
- data/app/assets/images/grapple/arrow-up.png +0 -0
- data/app/assets/images/grapple/loading-bar.gif +0 -0
- data/app/assets/images/grapple/minus.png +0 -0
- data/app/assets/images/grapple/plus.png +0 -0
- data/app/assets/javascripts/grapple-history.js +81 -0
- data/app/assets/javascripts/grapple-jquery.js +202 -0
- data/app/assets/javascripts/grapple.js +39 -0
- data/app/assets/stylesheets/grapple.css +252 -0
- data/config/locales/en.yml +2 -0
- data/lib/grapple/ajax_data_grid_builder.rb +38 -0
- data/lib/grapple/base_table_builder.rb +58 -0
- data/lib/grapple/components/actions.rb +24 -0
- data/lib/grapple/components/base_component.rb +91 -0
- data/lib/grapple/components/column_headings.rb +42 -0
- data/lib/grapple/components/html_body.rb +37 -0
- data/lib/grapple/components/html_colgroup.rb +14 -0
- data/lib/grapple/components/html_component.rb +24 -0
- data/lib/grapple/components/html_footer.rb +14 -0
- data/lib/grapple/components/html_header.rb +16 -0
- data/lib/grapple/components/html_row.rb +11 -0
- data/lib/grapple/components/search_form.rb +27 -0
- data/lib/grapple/components/search_query_field.rb +14 -0
- data/lib/grapple/components/search_submit.rb +11 -0
- data/lib/grapple/components/toolbar.rb +15 -0
- data/lib/grapple/components/will_paginate_infobar.rb +22 -0
- data/lib/grapple/components/will_paginate_pagination.rb +30 -0
- data/lib/grapple/components.rb +23 -0
- data/lib/grapple/data_grid_builder.rb +24 -0
- data/lib/grapple/engine.rb +11 -0
- data/lib/grapple/helpers/table_helper.rb +31 -0
- data/lib/grapple/helpers.rb +8 -0
- data/lib/grapple/html_table_builder.rb +31 -0
- data/lib/grapple.rb +14 -0
- data/spec/builders/ajax_data_grid_builder_spec.rb +19 -0
- data/spec/components/actions_spec.rb +33 -0
- data/spec/components/column_headings_spec.rb +33 -0
- data/spec/components/html_body_spec.rb +53 -0
- data/spec/components/html_colgroup_spec.rb +38 -0
- data/spec/components/html_footer_spec.rb +38 -0
- data/spec/components/search_form_spec.rb +29 -0
- data/spec/components/toolbar_spec.rb +38 -0
- data/spec/components/will_paginate_spec.rb +33 -0
- data/spec/fixtures/schema.rb +17 -0
- data/spec/fixtures/users.yml +56 -0
- data/spec/spec_helper.rb +137 -0
- data/spec/support/test_environment.rb +30 -0
- metadata +207 -0
@@ -0,0 +1,252 @@
|
|
1
|
+
/*************************
|
2
|
+
** Core **
|
3
|
+
*************************/
|
4
|
+
|
5
|
+
.grapple {
|
6
|
+
position: relative;
|
7
|
+
}
|
8
|
+
|
9
|
+
.grapple table {
|
10
|
+
border-collapse: collapse;
|
11
|
+
}
|
12
|
+
|
13
|
+
.grapple thead .column-headers th {
|
14
|
+
white-space: nowrap; /* Don't wrap text in column headers */
|
15
|
+
}
|
16
|
+
|
17
|
+
.grapple thead .sortable {
|
18
|
+
cursor: pointer;
|
19
|
+
}
|
20
|
+
|
21
|
+
|
22
|
+
/*************************
|
23
|
+
** Skin **
|
24
|
+
*************************/
|
25
|
+
|
26
|
+
.grapple table {
|
27
|
+
/* 1px border at the top of the table */
|
28
|
+
border: 1px solid #cbcbcb;
|
29
|
+
border-width: 1px 0 0 0;
|
30
|
+
}
|
31
|
+
|
32
|
+
/* Cells */
|
33
|
+
.grapple td, .grapple th {
|
34
|
+
padding: 4px 10px;
|
35
|
+
border: 1px solid #cbcbcb;
|
36
|
+
border-width: 0 1px;
|
37
|
+
}
|
38
|
+
|
39
|
+
/* Loading */
|
40
|
+
.grapple-loading table {
|
41
|
+
opacity: 0.5;
|
42
|
+
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
|
43
|
+
filter: alpha(opacity=50);
|
44
|
+
}
|
45
|
+
|
46
|
+
.grapple .loading-overlay {
|
47
|
+
display: none;
|
48
|
+
position: absolute;
|
49
|
+
height: 19px;
|
50
|
+
width: 100%;
|
51
|
+
top: 150px;
|
52
|
+
left: 0;
|
53
|
+
background: url("grapple/loading-bar.gif") no-repeat scroll center center transparent;
|
54
|
+
}
|
55
|
+
|
56
|
+
.grapple.grapple-loading .loading-overlay {
|
57
|
+
display: block;
|
58
|
+
}
|
59
|
+
|
60
|
+
/* Grapple AJAX */
|
61
|
+
.grapple-ajax-loading .search-form,
|
62
|
+
.grapple-ajax-loading .pagination,
|
63
|
+
.grapple-ajax-loading .column-headers {
|
64
|
+
/* Hide interactive elements until javascript finishes initializing */
|
65
|
+
visibility: hidden;
|
66
|
+
}
|
67
|
+
|
68
|
+
|
69
|
+
/**************** Header ****************/
|
70
|
+
|
71
|
+
/* Column Headers */
|
72
|
+
.grapple thead .column-headers th {
|
73
|
+
font-size: 108%;
|
74
|
+
border-bottom: 1px solid #cbcbcb;
|
75
|
+
border-top: 1px solid #cbcbcb;
|
76
|
+
background-color: #eee;
|
77
|
+
}
|
78
|
+
|
79
|
+
/* Sortable Column Headings */
|
80
|
+
.grapple thead .sortable:hover {
|
81
|
+
background-color: #e0e0e0;
|
82
|
+
}
|
83
|
+
|
84
|
+
.grapple thead .sortable a:hover {
|
85
|
+
text-decoration: none;
|
86
|
+
}
|
87
|
+
|
88
|
+
.grapple thead .column-headers th.sorted {
|
89
|
+
background-color: #e0e0e0;
|
90
|
+
}
|
91
|
+
|
92
|
+
.grapple thead .sortable .sort-asc {
|
93
|
+
/* TODO: put in sprite */
|
94
|
+
background: url("grapple/arrow-up.png") no-repeat scroll right center transparent;
|
95
|
+
}
|
96
|
+
|
97
|
+
.grapple thead .sortable .sort-desc {
|
98
|
+
/* TODO: put in sprite */
|
99
|
+
background: url("grapple/arrow-down.png") no-repeat scroll right center transparent;
|
100
|
+
}
|
101
|
+
|
102
|
+
/* Infobar */
|
103
|
+
.grapple thead .infobar th{
|
104
|
+
border-bottom: 1px solid #aaa;
|
105
|
+
}
|
106
|
+
|
107
|
+
/* Toolbar */
|
108
|
+
.grapple .toolbar th {
|
109
|
+
background: #cbcbcb;
|
110
|
+
border-bottom: 1px solid #aaa;
|
111
|
+
/*border-width: 0;*/
|
112
|
+
}
|
113
|
+
|
114
|
+
/* Search Form */
|
115
|
+
.grapple .search-form {
|
116
|
+
float: left;
|
117
|
+
padding: 3px 0;
|
118
|
+
}
|
119
|
+
|
120
|
+
.grapple .search-form .search-icon {
|
121
|
+
margin-left: 5px;
|
122
|
+
}
|
123
|
+
|
124
|
+
.grapple .search-form * {
|
125
|
+
vertical-align: middle;
|
126
|
+
}
|
127
|
+
|
128
|
+
.grapple .search-form table,
|
129
|
+
.grapple .search-form table td {
|
130
|
+
border: 0;
|
131
|
+
padding: 0;
|
132
|
+
}
|
133
|
+
|
134
|
+
.grapple .search-form table td {
|
135
|
+
padding-right: 5px;
|
136
|
+
}
|
137
|
+
|
138
|
+
/* Actions */
|
139
|
+
.grapple .toolbar .actions {
|
140
|
+
height: 22px;
|
141
|
+
float: right;
|
142
|
+
}
|
143
|
+
|
144
|
+
|
145
|
+
/**************** Footer ****************/
|
146
|
+
|
147
|
+
/* Footer */
|
148
|
+
.grapple tfoot td {
|
149
|
+
background-color: #fff;
|
150
|
+
border-top: 1px solid #cbcbcb;
|
151
|
+
border-width: 1px 0 0 0;
|
152
|
+
}
|
153
|
+
|
154
|
+
/* Pagination */
|
155
|
+
.grapple tfoot .pagination {
|
156
|
+
display: block;
|
157
|
+
width: 100%;
|
158
|
+
text-align: center;
|
159
|
+
margin: 6px 0;
|
160
|
+
}
|
161
|
+
|
162
|
+
.grapple .pagination .disabled.previous_page,
|
163
|
+
.grapple .pagination .disabled.next_page {
|
164
|
+
display: none;
|
165
|
+
}
|
166
|
+
|
167
|
+
.grapple .pagination .current {
|
168
|
+
font-weight: bold;
|
169
|
+
background-color: transparent;
|
170
|
+
border: medium none;
|
171
|
+
padding: 3px 6px;
|
172
|
+
}
|
173
|
+
|
174
|
+
.grapple .pagination a {
|
175
|
+
background-color: #FFFFFF;
|
176
|
+
border: 1px solid #CBCBCB;
|
177
|
+
padding: 2px 6px;
|
178
|
+
color: #0066CC;
|
179
|
+
outline: 0 none;
|
180
|
+
text-decoration: underline;
|
181
|
+
margin-left: 1px;
|
182
|
+
margin-right: 1px;
|
183
|
+
}
|
184
|
+
|
185
|
+
.grapple .pagination .next_page,
|
186
|
+
.grapple .pagination .previous_page {
|
187
|
+
background-color: transparent;
|
188
|
+
border: 0;
|
189
|
+
}
|
190
|
+
|
191
|
+
|
192
|
+
/**************** Body ****************/
|
193
|
+
|
194
|
+
/* Alternating row colors */
|
195
|
+
.grapple tbody .even {
|
196
|
+
background-color: #fff;
|
197
|
+
}
|
198
|
+
|
199
|
+
.grapple tbody .odd {
|
200
|
+
background-color: #edf5ff;
|
201
|
+
}
|
202
|
+
|
203
|
+
/* Row Actions (for Edit/Delete/Show/etc) */
|
204
|
+
.grapple td.actions {
|
205
|
+
white-space: nowrap;
|
206
|
+
}
|
207
|
+
|
208
|
+
/* Expanding/collapsing content */
|
209
|
+
.grapple .expand-icon {
|
210
|
+
background: url('grapple/plus.png') no-repeat;
|
211
|
+
}
|
212
|
+
|
213
|
+
.grapple .collapse-icon {
|
214
|
+
background: url('grapple/minus.png') no-repeat;
|
215
|
+
}
|
216
|
+
|
217
|
+
.grapple .spacer, .grapple .expand-icon, .grapple .collapse-icon {
|
218
|
+
padding-left: 10px;
|
219
|
+
float: left;
|
220
|
+
width: 16px;
|
221
|
+
height: 16px;
|
222
|
+
display: block;
|
223
|
+
}
|
224
|
+
|
225
|
+
/* Nested tables */
|
226
|
+
.grapple table.inner-table {
|
227
|
+
border: none;
|
228
|
+
}
|
229
|
+
|
230
|
+
.grapple table.inner-table {
|
231
|
+
border: none;
|
232
|
+
}
|
233
|
+
|
234
|
+
.grapple table.inner-table td {
|
235
|
+
border: none;
|
236
|
+
padding: 1px;
|
237
|
+
margin: 0px;
|
238
|
+
}
|
239
|
+
|
240
|
+
|
241
|
+
/* TODO: Remove this stuff */
|
242
|
+
.grapple tbody .red {
|
243
|
+
background-color: #ffdddd;
|
244
|
+
}
|
245
|
+
.grapple h2 {
|
246
|
+
font-size: 116%;
|
247
|
+
padding: 2px 0;
|
248
|
+
}
|
249
|
+
|
250
|
+
.grapple .not-ready {
|
251
|
+
visibility: hidden;
|
252
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Grapple
|
2
|
+
class AjaxDataGridBuilder < DataGridBuilder
|
3
|
+
|
4
|
+
CONTAINER_CLASSES = 'grapple grapple-ajax grapple-ajax-loading'
|
5
|
+
|
6
|
+
@@next_id = 1000
|
7
|
+
|
8
|
+
def self.container_attributes(template, options)
|
9
|
+
@@next_id += 1
|
10
|
+
options[:id] ||= "grapple_ajax_table_#{@@next_id}"
|
11
|
+
css = CONTAINER_CLASSES
|
12
|
+
css += " #{options[:container_class]}" unless options[:container_class].nil?
|
13
|
+
|
14
|
+
data = {
|
15
|
+
"grapple-ajax-url" => options[:url] || template.url_for(action: 'table'),
|
16
|
+
"grapple-ajax-history" => options[:history] === false ? '0' : '1'
|
17
|
+
}
|
18
|
+
|
19
|
+
return {
|
20
|
+
:id => options[:id],
|
21
|
+
:class => css,
|
22
|
+
:data => data
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_table
|
27
|
+
style = 'background-image: url(' + template.image_path("grapple/loading-bar.gif") + ')'
|
28
|
+
template.content_tag :div, '', :class => 'loading-overlay', :style => style
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.after_container(template, options)
|
32
|
+
selector = '#' + options[:id]
|
33
|
+
js = "$(#{selector.to_json}).grapple();"
|
34
|
+
return template.javascript_tag(js)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Grapple
|
2
|
+
class BaseTableBuilder
|
3
|
+
|
4
|
+
# Create a helper
|
5
|
+
def self.helper(name, klass, settings = {})
|
6
|
+
class_eval <<-RUBY_EVAL
|
7
|
+
def #{name}(*arguments, &block)
|
8
|
+
invoke_helper(:"#{name}", *arguments, &block)
|
9
|
+
end
|
10
|
+
RUBY_EVAL
|
11
|
+
define_singleton_method(:"class_for_#{name}") { klass }
|
12
|
+
define_singleton_method(:"settings_for_#{name}") { settings }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Update settings for a helper
|
16
|
+
def self.configure(helper_name, *options)
|
17
|
+
settings = options[0] || {}
|
18
|
+
method = :"settings_for_#{helper_name}"
|
19
|
+
if self.respond_to?(method)
|
20
|
+
self.send(method).each do |key, value|
|
21
|
+
settings[key] = value unless settings.has_key?(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
define_singleton_method(method) { settings }
|
25
|
+
end
|
26
|
+
|
27
|
+
attr_reader :columns, :records, :template, :params
|
28
|
+
|
29
|
+
def initialize(template, columns, records, params = {}, *options)
|
30
|
+
@template = template
|
31
|
+
@columns = columns
|
32
|
+
@records = records
|
33
|
+
@params = params
|
34
|
+
@options = options[0] || {}
|
35
|
+
@helper_instances = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def before_table
|
39
|
+
''
|
40
|
+
end
|
41
|
+
|
42
|
+
def after_table
|
43
|
+
''
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def invoke_helper(name, *arguments, &block)
|
49
|
+
unless @helper_instances.has_key?(name)
|
50
|
+
klass = self.class.send(:"class_for_#{name}")
|
51
|
+
settings = self.class.send(:"settings_for_#{name}")
|
52
|
+
@helper_instances[name] = klass.new(@columns, @records, @template, @params, self, settings)
|
53
|
+
end
|
54
|
+
@helper_instances[name].send(:render, *arguments, &block)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class Actions < HtmlComponent
|
4
|
+
|
5
|
+
setting :link_to_helper, :link_to
|
6
|
+
|
7
|
+
def render(actions = [], &block)
|
8
|
+
html = capture_block(&block)
|
9
|
+
actions.each do |action|
|
10
|
+
if action.kind_of?(String)
|
11
|
+
html << action
|
12
|
+
else
|
13
|
+
# TODO: why are we deleting the label and url?
|
14
|
+
label = action.delete(:label)
|
15
|
+
url = action.delete(:url)
|
16
|
+
html << template.send(link_to_helper, label, url, action)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
content_tag(:div, html.html_safe, :class => 'actions')
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class BaseComponent
|
4
|
+
|
5
|
+
cattr_accessor :default_settings
|
6
|
+
@@default_settings = {}
|
7
|
+
|
8
|
+
def self.setting(name, default = nil)
|
9
|
+
attr_accessor(name)
|
10
|
+
@@default_settings[self.name] = {} unless @@default_settings.has_key?(self.name)
|
11
|
+
@@default_settings[self.name][name] = default
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :columns, :records, :template, :params, :builder
|
15
|
+
|
16
|
+
def initialize(columns, records, template, params, builder, settings = {})
|
17
|
+
@template = template
|
18
|
+
@columns = columns
|
19
|
+
@records = records
|
20
|
+
@params = params
|
21
|
+
@builder = builder
|
22
|
+
merge_settings(settings).each do |name, value|
|
23
|
+
self.send(:"#{name}=", value)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def render(*options, &block)
|
28
|
+
raise StandardError.new("Component must override render method")
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
# TODO: this is all pretty hacky
|
34
|
+
def merge_settings(settings)
|
35
|
+
result = {}
|
36
|
+
klass = self.class
|
37
|
+
while klass && klass.name != 'BaseComponent'
|
38
|
+
if @@default_settings[klass.name]
|
39
|
+
@@default_settings[klass.name].each do |name, value|
|
40
|
+
result[name] = value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
klass = klass.superclass
|
44
|
+
end
|
45
|
+
|
46
|
+
settings.each do |name, value|
|
47
|
+
result[name] = value
|
48
|
+
end
|
49
|
+
result
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shortcut for translations
|
53
|
+
def t(*args)
|
54
|
+
begin
|
55
|
+
return template.t(*args) if template.method_defined?(:t)
|
56
|
+
ensure
|
57
|
+
return I18n.translate(*args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def num_columns
|
62
|
+
@columns.length
|
63
|
+
end
|
64
|
+
|
65
|
+
def capture_block(default = '', &block)
|
66
|
+
return default if block.nil?
|
67
|
+
template.with_output_buffer(&block).html_safe
|
68
|
+
end
|
69
|
+
|
70
|
+
def block_or_components(components, options, &block)
|
71
|
+
block.nil? ? render_components(components, options, &block).join : capture_block(&block)
|
72
|
+
end
|
73
|
+
|
74
|
+
def render_components(components, options, &block)
|
75
|
+
html = []
|
76
|
+
components.each do |component|
|
77
|
+
if component == :body
|
78
|
+
html << capture_block(&block)
|
79
|
+
elsif options[component] === false
|
80
|
+
next
|
81
|
+
else
|
82
|
+
args = options[component] || []
|
83
|
+
html << self.builder.send(component, *args)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
html
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class ColumnHeadings < HtmlComponent
|
4
|
+
|
5
|
+
setting :alignment_classes, { left: 'text-left', center: 'text-center', right: 'text-right' }
|
6
|
+
setting :tooltip_class, 'table-tooltip'
|
7
|
+
|
8
|
+
def render(url_params = {})
|
9
|
+
cols = columns.collect do |column|
|
10
|
+
indent + column_header(column, url_params)
|
11
|
+
end
|
12
|
+
builder.row cols.join("\n"), :class => 'column-headers'
|
13
|
+
end
|
14
|
+
|
15
|
+
def column_header(column, additional_parameters = {})
|
16
|
+
cell_classes = []
|
17
|
+
cell_classes << alignment_classes[(column[:align] || :left).to_sym]
|
18
|
+
|
19
|
+
liner_classes = []
|
20
|
+
liner_classes << tooltip_class if column[:title].present?
|
21
|
+
|
22
|
+
if column[:sort] && params.present?
|
23
|
+
cell_classes << 'sortable'
|
24
|
+
if column[:sort] == params[:sort]
|
25
|
+
liner_classes << (params[:dir] == 'desc' ? 'sort-desc' : 'sort-asc')
|
26
|
+
cell_classes << 'sorted'
|
27
|
+
end
|
28
|
+
content = template.link_to(column[:label], table_url(additional_parameters.merge({:sort => column[:sort]})))
|
29
|
+
else
|
30
|
+
content = column[:label]
|
31
|
+
end
|
32
|
+
|
33
|
+
cell_classes = ' class="' + cell_classes.join(' ') + '"'
|
34
|
+
title = column[:title] ? " title=\"#{h(column[:title])}\"" : ''
|
35
|
+
liner_classes = liner_classes.length ? " class=\"#{liner_classes.join(" ")}\"" : ''
|
36
|
+
|
37
|
+
"<th#{cell_classes}><div#{title}#{liner_classes}>#{h content}</div></th>".html_safe
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class HtmlBody < HtmlComponent
|
4
|
+
|
5
|
+
# If not false, each row will be wrapped in a <tr> tag
|
6
|
+
# if false, the <tr> tag needs to be added in the block
|
7
|
+
# if the value is a proc, it will be called for each row
|
8
|
+
# and the returned value will be passed as the options to
|
9
|
+
# the tr tag
|
10
|
+
setting :tr, true
|
11
|
+
|
12
|
+
def render(*options, &block)
|
13
|
+
options = options[0] || {}
|
14
|
+
|
15
|
+
wrap_row = if options[:tr].nil?
|
16
|
+
self.tr
|
17
|
+
elsif options[:tr] === false
|
18
|
+
false
|
19
|
+
else
|
20
|
+
options[:tr]
|
21
|
+
end
|
22
|
+
|
23
|
+
args = {}
|
24
|
+
html = records.collect do |data|
|
25
|
+
if wrap_row
|
26
|
+
args = wrap_row.call(template) if wrap_row.is_a?(Proc)
|
27
|
+
builder.row(capture_block { block.call(data) }, args)
|
28
|
+
else
|
29
|
+
indent + capture_block { block.call(data) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
"<tbody>\n#{html.join("\n")}\n</tbody>\n".html_safe
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class HtmlColgroup < HtmlComponent
|
4
|
+
|
5
|
+
def render
|
6
|
+
cols = columns.collect do |col|
|
7
|
+
indent + (col[:width].nil? ? "<col>" : "<col width=\"#{col[:width]}\">")
|
8
|
+
end
|
9
|
+
"<colgroup>\n#{cols.join("\n")}\n</colgroup>\n".html_safe
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class HtmlComponent < BaseComponent
|
4
|
+
|
5
|
+
setting :indent, "\t"
|
6
|
+
|
7
|
+
protected
|
8
|
+
|
9
|
+
include ERB::Util
|
10
|
+
|
11
|
+
def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
|
12
|
+
template.content_tag(name, content_or_options_with_block, options, escape, &block)
|
13
|
+
end
|
14
|
+
|
15
|
+
def table_url(options)
|
16
|
+
if options[:sort] == params[:sort]
|
17
|
+
options[:dir] = (params[:dir] == 'desc') ? 'asc' : 'desc'
|
18
|
+
end
|
19
|
+
template.url_for params.stringify_keys().merge(options.stringify_keys())
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class HtmlFooter < HtmlComponent
|
4
|
+
|
5
|
+
setting :components, []
|
6
|
+
|
7
|
+
def render(*options, &block)
|
8
|
+
html = block_or_components(components, options[0] || {}, &block)
|
9
|
+
"<tfoot>\n#{html}</tfoot>\n".html_safe
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
|
4
|
+
class HtmlHeader < HtmlComponent
|
5
|
+
|
6
|
+
setting :components, []
|
7
|
+
|
8
|
+
def render(*options, &block)
|
9
|
+
html = block_or_components(components, options[0] || {}, &block)
|
10
|
+
"<thead>\n#{html}</thead>\n".html_safe
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class SearchForm < HtmlComponent
|
4
|
+
|
5
|
+
setting :components, [:body, :search_query_field, :search_submit]
|
6
|
+
setting :page_param, 'page'
|
7
|
+
setting :form_class, 'search-form'
|
8
|
+
|
9
|
+
def render(*options, &block)
|
10
|
+
options = options[0] ? options[0] : {}
|
11
|
+
form_classes = [form_class]
|
12
|
+
html = ''
|
13
|
+
html << template.form_tag({}, { :class => form_classes.join(' ') })
|
14
|
+
html << template.hidden_field_tag(page_param, 1)
|
15
|
+
# TODO: don't use tables for vertical alignment
|
16
|
+
html << '<table><tr>'
|
17
|
+
render_components(components, options, &block).each do |c|
|
18
|
+
html << "<td>#{c}</td>\n"
|
19
|
+
end
|
20
|
+
html << '</tr></table>'
|
21
|
+
html << '</form>'
|
22
|
+
html.html_safe
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class SearchQueryField < HtmlComponent
|
4
|
+
|
5
|
+
setting :search_query_param, 'query'
|
6
|
+
setting :search_query_field_class, 'search-query'
|
7
|
+
|
8
|
+
def render(*options, &block)
|
9
|
+
template.text_field_tag(search_query_param.to_s, params[search_query_param.to_sym], { :class => search_query_field_class })
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Grapple
|
2
|
+
module Components
|
3
|
+
class Toolbar < HtmlComponent
|
4
|
+
|
5
|
+
setting :components, []
|
6
|
+
|
7
|
+
def render(*options, &block)
|
8
|
+
options = options[0] || {}
|
9
|
+
html = block_or_components(components, options, &block)
|
10
|
+
builder.row "<th colspan=\"#{num_columns}\">#{html}</th>", :class => 'toolbar'
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|