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.
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