grapple 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +182 -0
  4. data/Rakefile +9 -0
  5. data/app/assets/images/grapple/arrow-down.png +0 -0
  6. data/app/assets/images/grapple/arrow-up.png +0 -0
  7. data/app/assets/images/grapple/loading-bar.gif +0 -0
  8. data/app/assets/images/grapple/minus.png +0 -0
  9. data/app/assets/images/grapple/plus.png +0 -0
  10. data/app/assets/javascripts/grapple-history.js +81 -0
  11. data/app/assets/javascripts/grapple-jquery.js +202 -0
  12. data/app/assets/javascripts/grapple.js +39 -0
  13. data/app/assets/stylesheets/grapple.css +252 -0
  14. data/config/locales/en.yml +2 -0
  15. data/lib/grapple/ajax_data_grid_builder.rb +38 -0
  16. data/lib/grapple/base_table_builder.rb +58 -0
  17. data/lib/grapple/components/actions.rb +24 -0
  18. data/lib/grapple/components/base_component.rb +91 -0
  19. data/lib/grapple/components/column_headings.rb +42 -0
  20. data/lib/grapple/components/html_body.rb +37 -0
  21. data/lib/grapple/components/html_colgroup.rb +14 -0
  22. data/lib/grapple/components/html_component.rb +24 -0
  23. data/lib/grapple/components/html_footer.rb +14 -0
  24. data/lib/grapple/components/html_header.rb +16 -0
  25. data/lib/grapple/components/html_row.rb +11 -0
  26. data/lib/grapple/components/search_form.rb +27 -0
  27. data/lib/grapple/components/search_query_field.rb +14 -0
  28. data/lib/grapple/components/search_submit.rb +11 -0
  29. data/lib/grapple/components/toolbar.rb +15 -0
  30. data/lib/grapple/components/will_paginate_infobar.rb +22 -0
  31. data/lib/grapple/components/will_paginate_pagination.rb +30 -0
  32. data/lib/grapple/components.rb +23 -0
  33. data/lib/grapple/data_grid_builder.rb +24 -0
  34. data/lib/grapple/engine.rb +11 -0
  35. data/lib/grapple/helpers/table_helper.rb +31 -0
  36. data/lib/grapple/helpers.rb +8 -0
  37. data/lib/grapple/html_table_builder.rb +31 -0
  38. data/lib/grapple.rb +14 -0
  39. data/spec/builders/ajax_data_grid_builder_spec.rb +19 -0
  40. data/spec/components/actions_spec.rb +33 -0
  41. data/spec/components/column_headings_spec.rb +33 -0
  42. data/spec/components/html_body_spec.rb +53 -0
  43. data/spec/components/html_colgroup_spec.rb +38 -0
  44. data/spec/components/html_footer_spec.rb +38 -0
  45. data/spec/components/search_form_spec.rb +29 -0
  46. data/spec/components/toolbar_spec.rb +38 -0
  47. data/spec/components/will_paginate_spec.rb +33 -0
  48. data/spec/fixtures/schema.rb +17 -0
  49. data/spec/fixtures/users.yml +56 -0
  50. data/spec/spec_helper.rb +137 -0
  51. data/spec/support/test_environment.rb +30 -0
  52. 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,2 @@
1
+ en:
2
+ no_search_results: "We're sorry. No results were found."
@@ -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,11 @@
1
+ module Grapple
2
+ module Components
3
+ class HtmlRow < HtmlComponent
4
+
5
+ def render(content, *options)
6
+ (indent + template.content_tag(:tr, content.html_safe, *options) + "\n").html_safe
7
+ end
8
+
9
+ end
10
+ end
11
+ 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,11 @@
1
+ module Grapple
2
+ module Components
3
+ class SearchSubmit < HtmlComponent
4
+
5
+ def render
6
+ template.submit_tag(t(:filter))
7
+ end
8
+
9
+ end
10
+ end
11
+ 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