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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cbdfba833204a62c126fb91ec6b95e6b17a76654
4
+ data.tar.gz: ee880107cf1becc73adc3c46d0f908be8b7ff465
5
+ SHA512:
6
+ metadata.gz: 1976ae3090721348125616fdb2703bf5e568849c366c62904a81de9b6359a072522cc2fa8307d13e983961f1e9546ad54dac01ac6fc51e29bed25486eb262865
7
+ data.tar.gz: 591e70f06fbf5ef355a692fc082a7016a77d993a021b3f6f89c9aaff91c2eb22303825fe4f6d8496e372fffca403116aa3176a7b41938fb444363ecdabcc174e
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Equal Level
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,182 @@
1
+ # grapple
2
+ Customizable data grid for Rails
3
+
4
+ ## Features
5
+ * Modular design
6
+ * Server side rendering
7
+ * Usable out of the box
8
+ * Sorting
9
+ * Searching/Filtering
10
+ * Pagination
11
+ * AJAX
12
+
13
+ ## Installation
14
+
15
+ ``` ruby
16
+ # Gemfile for Rails 3+
17
+ gem 'grapple'
18
+ ```
19
+
20
+ ``` css
21
+ /* app/assets/stylesheets/application.css */
22
+ *= require grapple
23
+ ```
24
+
25
+ ## Dependencies
26
+ * Rails 3+
27
+
28
+ Optional Dependencies:
29
+
30
+ * will_paginate - for pagination support
31
+ * jQuery - for AJAX support
32
+ * history.js - for back button support when using the AJAX data table
33
+
34
+ ## Table Builders
35
+ HtmlTableBuilder - A basic HTML table builder
36
+
37
+ DataGridBuilder (default) - An HTML table builder with support for paging, filtering, sorting, and actions.
38
+
39
+ AjaxDataGridBuilder - DataGridBuilder that uses AJAX to retrieve results when sorting/filtering the table.
40
+
41
+ In an initializer set the default builder:
42
+ ``` ruby
43
+ Grapple::Helpers::TableHelper.builder = Grapple::AjaxDataGridBuilder
44
+ ```
45
+
46
+ ## Basic Usage (DataGridBuilder)
47
+ app/controllers/posts_controller.rb
48
+ ``` ruby
49
+ class PostsController < ApplicationController
50
+ def index
51
+ @posts = Post.all
52
+ end
53
+ end
54
+ ```
55
+
56
+ app/views/posts/index.html.erb
57
+ ``` HTML+ERB
58
+ <%
59
+ columns = [
60
+ { label: 'Name' },
61
+ { label: 'Title' },
62
+ { label: 'Content' },
63
+ { label: '' },
64
+ { label: '' },
65
+ { label: '' }
66
+ ]
67
+
68
+ actions = [
69
+ { label: 'New Post', url: new_posts_path }
70
+ ]
71
+ %>
72
+ <%= table_for(columns, @posts) do |t| %>
73
+ <%= t.header do %>
74
+ <%= t.toolbar do %>
75
+ <%= t.actions actions %>
76
+ <% end %>
77
+ <%= t.column_headings %>
78
+ <% end %>
79
+ <%= t.body do |item| %>
80
+ <td><%= post.name %></td>
81
+ <td><%= post.title %></td>
82
+ <td><%= post.content %></td>
83
+ <td><%= link_to 'Show', post %></td>
84
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
85
+ <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td>
86
+ <% end %>
87
+ <% end %>
88
+ ```
89
+
90
+ ## Sorting
91
+ TODO
92
+
93
+ ## Pagination (requires will_paginate)
94
+ app/controllers/posts_controller.rb
95
+ ``` ruby
96
+ def index
97
+ @posts = Post.paginate(page: params[:page] || 1, per_page: 10)
98
+ end
99
+ ```
100
+
101
+ app/views/posts/index.html.erb
102
+ ``` HTML+ERB
103
+ <%= table_for(columns, @posts) do |t| %>
104
+ <%= t.header %>
105
+ <%= t.footer do %>
106
+ <%= t.pagination %>
107
+ <% end %>
108
+ <% end %>
109
+ ```
110
+
111
+ ## Filtering/Searching
112
+ TODO
113
+
114
+ ## Actions
115
+ The Actions component can be used to generate buttons/links for actions related to the table. This can be used to provide links to export the data in the table or create new objects.
116
+ ``` HTML+ERB
117
+ <%= table_for(columns, @posts) do |t| %>
118
+ <%= t.header do %>
119
+ <%= t.toolbar do %>
120
+ <%= t.actions [
121
+ { label: :new_post, url: new_posts_path },
122
+ { label: :export_posts, url: export_posts_path }
123
+ ] %>
124
+ <% end %>
125
+ <%= t.column_headings %>
126
+ <% end %>
127
+ <% end %>
128
+ ```
129
+
130
+ ## AJAX
131
+ The AjaxDataGridBuilder generates tables that can update their content using AJAX rather than re-loading the page. jQuery is required.
132
+ ``` javascript
133
+ // app/assets/javascripts/application.js
134
+ //= require grapple
135
+ //= require grapple-jquery
136
+ ```
137
+
138
+ ``` ruby
139
+ # app/controllers/posts_controller.rb
140
+ class PostsController < ApplicationController
141
+ def index
142
+ @posts = table_results
143
+ end
144
+
145
+ # Method called by AJAX requests - renders the table without a layout
146
+ def table
147
+ @posts = table_results
148
+ render partial: 'table'
149
+ end
150
+
151
+ protected
152
+
153
+ def table_results
154
+ Post.paginate(page: params[:page] || 1, per_page: 10)
155
+ end
156
+ end
157
+ ```
158
+
159
+ Create a container around the table that can be updated by the JavaScript
160
+ ``` HTML+ERB
161
+ <%# app/views/posts/index.html.erb %>
162
+ <%= grapple_container(id: 'posts_table') do %>
163
+ <%= render :partial => 'table' %>
164
+ <% end %>
165
+ ```
166
+
167
+ Render the table using `table_for` in `app/views/posts/_table.html.erb`
168
+
169
+ ## History w/AJAX (back button)
170
+
171
+ Requires: https://github.com/browserstate/history.js/
172
+
173
+ ``` javascript
174
+ // app/assets/javascripts/application.js
175
+ //= require jquery-history
176
+ //= require grapple
177
+ //= require grapple-history
178
+ //= require grapple-jquery
179
+ ```
180
+
181
+ ## Customizing
182
+ TODO
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+ require 'bundler/setup'
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Test the datable plugin'
6
+ RSpec::Core::RakeTask.new('spec') do |t|
7
+ t.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+
Binary file
Binary file
@@ -0,0 +1,81 @@
1
+ (function(window, Grapple, $) {
2
+ 'use strict';
3
+
4
+ var urlQuery = Grapple.Util.urlQuery,
5
+ parseUrlQuery = Grapple.Util.parseUrlQuery;
6
+
7
+ var GrappleHistory = function() {
8
+ if(History.init) {
9
+ // https://github.com/browserstate/history.js/
10
+ this.api = History;
11
+
12
+ // Initialization of history.js can be delayed
13
+ // if it was do it now
14
+ if(this.api.options && this.api.options.delayInit) {
15
+ this.api.options.delayInit = false;
16
+ this.api.init();
17
+ }
18
+ }
19
+ else {
20
+ // TODO: support native history api
21
+ this.api = window.history;
22
+ }
23
+ this.api = History;
24
+ this.changeCallback = null;
25
+ };
26
+
27
+ // Don't clutter the url with rails form parameters
28
+ GrappleHistory.IGNORE_PARAMS = { 'utf8': true, 'authenticity_token': true };
29
+
30
+ GrappleHistory.prototype = {
31
+
32
+ add: function(namespace, params) {
33
+ var state = this.api.getState();
34
+ var historyParams = parseUrlQuery(urlQuery(state.url));
35
+ var newParams = parseUrlQuery(params);
36
+
37
+ // Remove any parameters from the current state
38
+ // that are for this table
39
+ for(var x in historyParams) {
40
+ var remove = namespace ?
41
+ // Remove any parameters in the tables namespace
42
+ x.indexOf(namespace + '.') === 0 :
43
+ // Table is in the global namespace, remove any parameters that aren't namespaced
44
+ x.indexOf('.') === -1;
45
+
46
+ if(remove) {
47
+ delete historyParams[x];
48
+ }
49
+ }
50
+
51
+ // Add the new parameters
52
+ for(var x in newParams) {
53
+ if(GrappleHistory.IGNORE_PARAMS[x]) continue;
54
+ var key = namespace ? namespace + '.' + x : x;
55
+ historyParams[key] = newParams[key];
56
+ }
57
+
58
+ this.api.pushState(null, document.title, '?' + $.param(historyParams));
59
+ },
60
+
61
+ subscribe: function(callback) {
62
+ var api = this.api;
63
+ this.changeCallback = function(event) {
64
+ var state = api.getState();
65
+ callback(parseUrlQuery(urlQuery(state.url)));
66
+ };
67
+ $(window).bind('statechange', this.changeCallback);
68
+ },
69
+
70
+ unsubscribe: function() {
71
+ if(this.changeCallback) {
72
+ $(window).unbind('statechange', this.changeCallback);
73
+ this.changeCallback = null;
74
+ }
75
+ }
76
+
77
+ };
78
+
79
+ Grapple.History = GrappleHistory;
80
+
81
+ })(window, Grapple, $);
@@ -0,0 +1,202 @@
1
+ (function(Grapple, $) {
2
+ 'use strict';
3
+
4
+ var urlQuery = Grapple.Util.urlQuery,
5
+ parseUrlQuery = Grapple.Util.parseUrlQuery;
6
+
7
+ var overrideLink = function(clickable, anchor, callback) {
8
+ var href = $(anchor).attr('href');
9
+ $(anchor).attr('href', 'javascript:void(0)');
10
+ $(clickable).on('click', function() {
11
+ callback(href ? href.split('?')[1] : '');
12
+ });
13
+ };
14
+
15
+ /**
16
+ * Creates a new instance of the Grapple AJAX widget.
17
+ *
18
+ * @param {String} Selector for the table container element.
19
+ * @param {Object} Hash of options for the table (url, history)
20
+ */
21
+ var GrappleTable = function(element, options) {
22
+ options = options || {};
23
+ this.element = $(element);
24
+ this.url = options.url || this.element.data('grapple-ajax-url');
25
+ this.namespace = options.namespace || this.element.data('grapple-ajax-namespace') || null;
26
+ this.currentParams = options.params || '';
27
+ if(typeof options.history !== 'undefined' && options.history !== true) {
28
+ this.history = options.history;
29
+ }
30
+ else if(this.element.data('grapple-ajax-history') == 1 || options.history === true) {
31
+ this.history = new Grapple.History();
32
+ }
33
+ else {
34
+ this.history = null;
35
+ }
36
+ this.init();
37
+ };
38
+
39
+ GrappleTable.CSS_AJAX_LOADING = 'grapple-ajax-loading';
40
+ GrappleTable.CSS_LOADING = 'grapple-loading';
41
+ GrappleTable.CSS_LOADING_OVERLAY = 'loading-overlay';
42
+ GrappleTable.NON_TABLE_RESPONSE = '<!DOCTYPE html>';
43
+
44
+ GrappleTable.prototype = {
45
+
46
+ /**
47
+ *
48
+ */
49
+ init: function() {
50
+ var self = this;
51
+ self.table = self.element.children('table');
52
+ self.header = self.table.children('thead');
53
+ self.body = self.table.children('tbody');
54
+ self.footer = self.table.children('tfoot');
55
+
56
+ self.initSorting();
57
+ self.initSearchForm();
58
+ self.initPagination();
59
+ self.initHistory();
60
+
61
+ self.element.removeClass(GrappleTable.CSS_AJAX_LOADING);
62
+ },
63
+
64
+ initHistory: function() {
65
+ if(this.history) {
66
+ var self = this;
67
+ this.history.unsubscribe();
68
+ this.history = new Grapple.History();
69
+ this.history.subscribe(function(params) {
70
+ self.onHistoryChange(params);
71
+ });
72
+ }
73
+ },
74
+
75
+ onHistoryChange: function(params) {
76
+ console.log("HISTORY CHANGE");
77
+ this._showLoading();
78
+ this._updateTable($.param(params));
79
+ },
80
+
81
+ /**
82
+ *
83
+ */
84
+ loadTable: function(params) {
85
+ console.log("LOAD TABLE ", params);
86
+ this._showLoading();
87
+
88
+ if(this.history) {
89
+ console.log("ADD History");
90
+ this.history.unsubscribe();
91
+ this.history.add(this.namespace, params);
92
+ }
93
+
94
+ console.log("Update table");
95
+ this._updateTable(params);
96
+ },
97
+
98
+ _showLoading: function() {
99
+ // Add loading class to the container
100
+ this.element.addClass(GrappleTable.CSS_LOADING);
101
+
102
+ // Set the position of the loading overlay based on the size of the table
103
+ var loadingBar = this.element.find('.' + GrappleTable.CSS_LOADING_OVERLAY)
104
+ loadingBar.width(this.table.width());
105
+ var barHeight = loadingBar.height() || 20;
106
+ var top = (this.table.height() / 2) - barHeight;
107
+ loadingBar.css('top', top + 'px');
108
+ },
109
+
110
+ _hideLoading: function() {
111
+ // Remove the loading class from the container
112
+ this.element.removeClass(GrappleTable.CSS_LOADING);
113
+ },
114
+
115
+ _updateTable: function(params) {
116
+ var self = this;
117
+ var url = this.url;
118
+ if(params.length) {
119
+ url += '?' + params;
120
+ }
121
+
122
+ $.ajax(url, {
123
+ success: function(data) {
124
+ // HACK
125
+ var nonTableKeyIndex = data.indexOf(GrappleTable.NON_TABLE_RESPONSE);
126
+ if(nonTableKeyIndex > -1 && nonTableKeyIndex < 100) {
127
+ data = "Failed to load table";
128
+ }
129
+ self.element.addClass(GrappleTable.CSS_AJAX_LOADING);
130
+ self.element.html(data);
131
+ self.init();
132
+ self._hideLoading();
133
+ },
134
+ error: function(a, b, c) {
135
+ // TODO: handle loading errors
136
+ console.log("Failed to load table");
137
+ console.log(a);
138
+ console.log(b);
139
+ console.log(c);
140
+ }
141
+ });
142
+ },
143
+
144
+ initSorting: function() {
145
+ var self = this;
146
+ this.header.find('th.sortable').each(function(i, elem) {
147
+ overrideLink(elem, $(elem).find('a'), function(params) {
148
+ // Return to the first page on sorting
149
+ params = params.replace(/&?page=[0-9]+/, '');
150
+ params += '&page=1';
151
+ self.loadTable(params);
152
+ });
153
+ });
154
+ },
155
+
156
+ initSearchForm: function() {
157
+ var self = this;
158
+ this.header.find('form.search-form').each(function(i, elem) {
159
+ $(elem).on('submit', function(event) {
160
+ // Don't submit the form
161
+ event.preventDefault();
162
+ self.loadTable($(elem).serialize());
163
+ });
164
+ });
165
+ },
166
+
167
+ initPagination: function() {
168
+ var self = this;
169
+ this.footer.find('.pagination a').each(function(i, elem) {
170
+ overrideLink(elem, elem, function(params) {
171
+ self.loadTable(params);
172
+ });
173
+ });
174
+ }
175
+
176
+ };
177
+
178
+ Grapple.Table = GrappleTable;
179
+
180
+ function Plugin(option) {
181
+ return this.each(function() {
182
+ var $this = $(this);
183
+ var data = $this.data('grapple');
184
+ var options = typeof option == 'object' && option;
185
+
186
+ if (!data && /destroy|hide/.test(option)) return;
187
+ if (!data) $this.data('grapple', (data = new GrappleTable(this, options)));
188
+ if (typeof option == 'string') data[option]();
189
+ });
190
+ }
191
+
192
+ var old = $.fn.grapple;
193
+
194
+ $.fn.grapple = Plugin;
195
+ $.fn.grapple.Constructor = GrappleTable;
196
+
197
+ $.fn.grapple.noConflict = function() {
198
+ $.fn.grapple = old;
199
+ return this;
200
+ }
201
+
202
+ })(Grapple, jQuery);
@@ -0,0 +1,39 @@
1
+ (function(globals) {
2
+ 'use strict';
3
+
4
+ // Namespace
5
+ var Grapple = {};
6
+
7
+ var decodeParam = function(str) {
8
+ return decodeURIComponent(str.replace(/\+/g, " "));
9
+ };
10
+
11
+ var parseUrlQuery = function(query) {
12
+ var regex = /([^&=]+)=?([^&]*)/g;
13
+ var params = {}, e;
14
+ while(e = regex.exec(query)) {
15
+ var k = decodeParam(e[1]), v = decodeParam(e[2]);
16
+ if(k.substring(k.length - 2) === '[]') {
17
+ k = k.substring(0, k.length - 2);
18
+ (params[k] || (params[k] = [])).push(v);
19
+ }
20
+ else {
21
+ params[k] = v;
22
+ }
23
+ }
24
+ return params;
25
+ };
26
+
27
+ // Get the query string from a url, returns an empty string if there is no query
28
+ var urlQuery = function(url) {
29
+ return url.split('?')[1] || '';
30
+ };
31
+
32
+ globals.Grapple = {
33
+ Util: {
34
+ urlQuery: urlQuery,
35
+ parseUrlQuery: parseUrlQuery
36
+ }
37
+ };
38
+
39
+ })(window);