listalicious 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.textile +260 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/builders/generic_builder.rb +32 -0
- data/lib/builders/table_builder.rb +148 -0
- data/lib/listalicious.rb +82 -0
- data/listalicious.gemspec +56 -0
- data/test/helper.rb +10 -0
- data/test/test_listalicious.rb +7 -0
- metadata +77 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Jeremy Jackson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,260 @@
|
|
1
|
+
h1. Listalicious
|
2
|
+
|
3
|
+
Semantic listing; a semantic way to build datagrid structures in Rails.
|
4
|
+
|
5
|
+
h2. The Story
|
6
|
+
|
7
|
+
Ok, so I really liked what came of my navigasmic gem, and it's proved itself useful and flexible. I'm also working on unifying some aspects of a CMS and decided that datagrids should be unified to some degree so I can simplify the generators, make broad changes if I need to, and have a potentially more efficient way to create them.
|
8
|
+
|
9
|
+
So I started this project to do just that. I quickly learned that datagrids aren't especially easy to make amazing, and there's a lot of things that one might want to do with them... grouping (with a row that creates a separator), sorting, ordering, additional informational rows, footers, ajax pagination, etc. etc. etc.. The list is actually pretty long, and I wrote a lot of those things, but it's nowhere near perfect. I sort of don't think it's possible with how I've approached it this time around. Anyhow, I thought I could get something to a point that simplified the datagrid process, and I believe I've succeeded in providing a good start. I opted out of adding several features for the time being until I need them -- at which point I might have a better understanding of how I could approach it better. I cover some of my thoughts below in this document.
|
10
|
+
|
11
|
+
So, as always, I set up some requirements for what the project should do, and here's what I came up with (a lot of these are standard):
|
12
|
+
|
13
|
+
* Should be simple
|
14
|
+
* Should be easily customizable
|
15
|
+
* Should handle sorting
|
16
|
+
* Should handle grouping
|
17
|
+
* Should handle ordering columns
|
18
|
+
* Should handle additional informational rows
|
19
|
+
* Should be pleasant to use
|
20
|
+
* Should use less code to create than it generates
|
21
|
+
|
22
|
+
I wrote a DSL that met those requirements:
|
23
|
+
|
24
|
+
<pre>
|
25
|
+
<% semantic_list_for @users do |l| %>
|
26
|
+
<%= l.head do %>
|
27
|
+
<%= l.column 'Name', :sort => 'last_name' %>
|
28
|
+
<%= l.column 'Email Address' %>
|
29
|
+
<% end %>
|
30
|
+
<%= l.columns do |user, index| %>
|
31
|
+
<%= l.column "#{user.first_name} #{user.last_name}" %>
|
32
|
+
<%= l.column link_to(user.email, "mailto:#{user.email}") %>
|
33
|
+
<%= l.controls link_to('edit', edit_user_path(user)) %>
|
34
|
+
<% end %>
|
35
|
+
<%= l.foot do %>
|
36
|
+
<%= l.full_column will_paginate(@users) %>
|
37
|
+
<% end %>
|
38
|
+
<% end %>
|
39
|
+
</pre>
|
40
|
+
|
41
|
+
I liked this, but could it be reduced further if the use case allowed? I came up with the following, which simplifies it, but comes with a minor snag. Note the line that instantiates a new User object. This is needed if there's no objects in the collection you pass it, because it needs to know about the object while it's building the columns.
|
42
|
+
|
43
|
+
<pre>
|
44
|
+
<% semantic_list_for @users do |l| %>
|
45
|
+
<%= l.columns [:head, :body] do |user, index| %>
|
46
|
+
<% user ||= User.new # this DSL requires a user object to be here %>
|
47
|
+
<%= l.column user.login, :title => 'Login', :sort => 'login' %>
|
48
|
+
<%= l.column user.email, :title => 'Email Address', :sort => 'email' %>
|
49
|
+
<%= l.controls link_to('edit', edit_user_path(user)) %>
|
50
|
+
<% end %>
|
51
|
+
<% end %>
|
52
|
+
</pre>
|
53
|
+
|
54
|
+
Clearly there's some ups and downs here, so I implemented both DSLs and leave it to your discretion to figure out which one is best to use in your case.
|
55
|
+
|
56
|
+
Since using builders on previous projects has worked out fairly well, I ventured down that path again. There's a single builder provided for now, TableBuilder (which inherits from GenericBuilder). GenericBuilder provides some basic functionality (ordering links for example), and isn't intended to be used as a builder by itself. And of course, the builders can always be extended or replaced if you need more custom markup.
|
57
|
+
|
58
|
+
h2. Installation
|
59
|
+
|
60
|
+
The gem is hosted on gemcutter, so *if you haven't already*, add it as a gem source:
|
61
|
+
|
62
|
+
<pre>
|
63
|
+
sudo gem sources -a http://gemcutter.org/
|
64
|
+
</pre>
|
65
|
+
|
66
|
+
Then install the listalicious gem:
|
67
|
+
|
68
|
+
<pre>
|
69
|
+
sudo gem install listalicious
|
70
|
+
</pre>
|
71
|
+
|
72
|
+
If you would like to get the default JS and CSS files you can use the generator after installing:
|
73
|
+
|
74
|
+
<pre>
|
75
|
+
[no javascript or css provided yet]
|
76
|
+
</pre>
|
77
|
+
|
78
|
+
h2. Usage
|
79
|
+
|
80
|
+
Listalicious works fine just as a view helper, but it also handles ordering of the lists. If you plan on using ordering in the lists you'll have to add a bit to the model and controller. All of the methods are added to the model by using sortable_fields, and the :order option on finders can be generated by using sort_order_from in the controller.
|
81
|
+
|
82
|
+
h3. Models
|
83
|
+
|
84
|
+
<pre>
|
85
|
+
class User < ActiveRecord::Base
|
86
|
+
# communicates to Listalicious how a list of this model can be ordered
|
87
|
+
sortable_fields :first_name, :last_name, :login, :email, :default => :login
|
88
|
+
end
|
89
|
+
</pre>
|
90
|
+
|
91
|
+
h3. Controllers
|
92
|
+
|
93
|
+
<pre>
|
94
|
+
def index
|
95
|
+
@users = User.paginate :page => params[:page], :per_page => 2, :order => User.sort_order_from(params)
|
96
|
+
respond_to do |format|
|
97
|
+
format.html # index.html.erb
|
98
|
+
format.xml { render :xml => @users }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
</pre>
|
102
|
+
|
103
|
+
h3. Views (I prefer HAML, so no ERB examples, but it should work with ERB fine as well)
|
104
|
+
|
105
|
+
It's important to note that all the methods take blocks or content strings. For instance, look at the extra and controls methods in the simple usage example.
|
106
|
+
|
107
|
+
It's also important to notice that the controls cell is not being counted in the columns. This is because I position these absolutely and do a hover event on them.. I'll likely take this out shortly so it doesn't effect anyone else.
|
108
|
+
|
109
|
+
*Simple Usage*
|
110
|
+
|
111
|
+
<pre>
|
112
|
+
- semantic_list_for @users, :html => {:class => 'list'} do |l|
|
113
|
+
= l.columns [:head, :body] do |user, index|
|
114
|
+
- user ||= User.new # this method requires a user object
|
115
|
+
= l.column user.login, :title => 'Login', :sort => 'login', :width => '20%'
|
116
|
+
= l.column user.email, :title => 'Email Address', :sort => 'email'
|
117
|
+
= l.extra do
|
118
|
+
= "You can add more information about #{user.first_name} here."
|
119
|
+
= l.controls link_to('edit', edit_user_path(user))
|
120
|
+
</pre>
|
121
|
+
...produces...
|
122
|
+
<pre>
|
123
|
+
<table class="list semantic-list" id="user_list">
|
124
|
+
<thead>
|
125
|
+
<tr class="header">
|
126
|
+
<th width="20%"><a class="sort-ascending" href="?user_sort_desc=login">Login</a></th>
|
127
|
+
<th><a href="?user_sort_asc=email">Email Address</a></th>
|
128
|
+
</tr>
|
129
|
+
</thead>
|
130
|
+
<tbody>
|
131
|
+
<tr class="even">
|
132
|
+
<td>jejacks0n</td>
|
133
|
+
<td>jeremy@email.com</td>
|
134
|
+
<td class="controls"><a href="/users/1/edit">edit</a></td>
|
135
|
+
</tr>
|
136
|
+
<tr class="even">
|
137
|
+
<td colspan="2">You can add more information about Jeremy here.</td>
|
138
|
+
</tr>
|
139
|
+
<tr class="odd">
|
140
|
+
<td>user1</td>
|
141
|
+
<td>user1@email.com</td>
|
142
|
+
<td class="controls"><a href="/users/2/edit">edit</a></td>
|
143
|
+
</tr>
|
144
|
+
<tr class="odd">
|
145
|
+
<td colspan="2">You can add more information about User here.</td>
|
146
|
+
</tr>
|
147
|
+
</tbody>
|
148
|
+
</table>
|
149
|
+
</pre>
|
150
|
+
|
151
|
+
*Extended Usage*
|
152
|
+
|
153
|
+
<pre>
|
154
|
+
- semantic_list_for @users do |l|
|
155
|
+
= l.head do
|
156
|
+
= l.column 'Name', :sort => 'last_name', :width => '20%'
|
157
|
+
= l.column 'Email Address'
|
158
|
+
= l.columns do |user, index|
|
159
|
+
= l.column "#{user.first_name} #{user.last_name}"
|
160
|
+
= l.column :html => {:class => 'email'} do
|
161
|
+
= link_to(user.email, "mailto:#{user.email}")
|
162
|
+
= l.controls link_to('edit', edit_protosite_user_path(user))
|
163
|
+
= l.foot do
|
164
|
+
= l.full_column will_paginate(l.collection)
|
165
|
+
</pre>
|
166
|
+
...produces...
|
167
|
+
<pre>
|
168
|
+
<table class="semantic-list" id="user_list">
|
169
|
+
<thead>
|
170
|
+
<tr class="header">
|
171
|
+
<th width="20%"><a class="sort-ascending" href="?user_sort_asc=last_name">Name</a></th>
|
172
|
+
<th>Email Address</th>
|
173
|
+
</tr>
|
174
|
+
</thead>
|
175
|
+
<tbody>
|
176
|
+
<tr class="even">
|
177
|
+
<td>Jeremy Jackson</td>
|
178
|
+
<td class="email"><a href="mailto:jeremy@email.com">jeremy@email.com</a></td>
|
179
|
+
<td class="controls"><a href="/users/1/edit">edit</a></td>
|
180
|
+
</tr>
|
181
|
+
<tr class="odd">
|
182
|
+
<td>User 1</td>
|
183
|
+
<td class="email"><a href="mailto:user1@email.com">user1@email.com</a></td>
|
184
|
+
<td class="controls"><a href="/users/2/edit">edit</a></td>
|
185
|
+
</tr>
|
186
|
+
</tbody>
|
187
|
+
<tfoot>
|
188
|
+
<tr>
|
189
|
+
<th colspan="2">
|
190
|
+
<div class="pagination">[removed for your sanity]</div>
|
191
|
+
</th>
|
192
|
+
</tr>
|
193
|
+
</tfoot>
|
194
|
+
</table>
|
195
|
+
</pre>
|
196
|
+
|
197
|
+
*Grouping* -- You can group lists, and this will add extra header rows as separators. This will include sort links if those were provided as well.
|
198
|
+
|
199
|
+
<pre>
|
200
|
+
- semantic_list_for @users, :group_by => :login do |l|
|
201
|
+
</pre>
|
202
|
+
|
203
|
+
*Sorting* -- Sorting requires javascript. It's part of the javascript code that comes with Listalicious and requires Prototype.js. If you want to add sorting to the list, just provide a url for a sort action and it will put that into the HTML5 data-sorturl attribute. You can use your own javascript if you would like.
|
204
|
+
|
205
|
+
<pre>
|
206
|
+
- semantic_list_for @users, :sort_url => { :action => 'sort' } do |l|
|
207
|
+
</pre>
|
208
|
+
|
209
|
+
*Additional JS Functionality* -- There a few functions that are javascript specific. You can add any additional functionality this way as well, since we're using unobtrusive JS to get the following features accomplished.
|
210
|
+
|
211
|
+
You can make the list multi-selectable. There isn't any pre-bundled functionality to do actions on the selected items, but you can fetch the list of selected items by using JS.
|
212
|
+
|
213
|
+
<pre>
|
214
|
+
- semantic_list_for @users, :selectable => true do |l|
|
215
|
+
</pre>
|
216
|
+
|
217
|
+
You can make the "extra" information rows expandable. If you add this, the extra rows won't show unless the row they belong to is highlighted.
|
218
|
+
|
219
|
+
<pre>
|
220
|
+
- semantic_list_for @users, :expandable => true do |l|
|
221
|
+
</pre>
|
222
|
+
|
223
|
+
h3. UL / OL Datagrids
|
224
|
+
|
225
|
+
The nature of doing a UL / OL based datagrid requires a certain level of CSS, and I haven't had time or reason to provide that level yet. When I finish up this project and include the JS and CSS needed for it (in a generator) I may add a builder for this, but there isn't one currently.
|
226
|
+
|
227
|
+
h3. Other
|
228
|
+
|
229
|
+
It's important to note that a lot of the functionality of these lists do not play nicely with one another -- I don't believe this is a shortcoming, and consider it more an effort to avoid overkill. A good example of this is the sortable features. For example, if you need the extra information and sorting together, you should consider using the :expandable => true option. Grouping and sorting don't play together nicely for obvious reasons as well. The javascript handles moving the extra container, in sorting, but doesn't attempt to do it gracefully. If I do the ListBuilder (UL/OL) to compliment the TableBuilder, it would likely handle these things somewhat better, but I haven't had a need for it yet.
|
230
|
+
|
231
|
+
I'm kicking around plans for how to make it more featureful without the annoying conflicts mentioned above. If you have any thoughts let me know, but I expect my solution will come by breaking up the builders by features.. So, instead of having a single TableBuilder, there would be a SortableTableBuilder, and an InformationalTableBuilder, and a MultiSelectTableBuilder, etc.. not sure how that would play out, but that's what I'm thinking might resolve some of the annoyances mentioned above.
|
232
|
+
|
233
|
+
And, as always, you can create your own builder by extending one of the existing ones, or by creating one from scratch.
|
234
|
+
|
235
|
+
Then just specify your builder, or do it as a configuration.
|
236
|
+
|
237
|
+
<pre>
|
238
|
+
semantic_list_for @users, :builder => MyCustomBuilder do
|
239
|
+
</pre>
|
240
|
+
|
241
|
+
<pre>
|
242
|
+
Listalicious::SemanticListHelper.builder = MyCustomBuilder
|
243
|
+
</pre>
|
244
|
+
|
245
|
+
h2. Documentation
|
246
|
+
|
247
|
+
RDoc documentation _should_ be automatically generated after each commit and made available on the "rdoc.info website":http://rdoc.info/projects/jejacks0n/listalicious.
|
248
|
+
|
249
|
+
Documentation is pretty sparse right now.
|
250
|
+
|
251
|
+
h2. Compatibility
|
252
|
+
|
253
|
+
I'm only testing with the latest Rails 2.4.x stable release, and it should work under Rails 2.3.x as well. Feel free to add backwards compatibility if you need it.
|
254
|
+
|
255
|
+
h2. Project Info
|
256
|
+
|
257
|
+
Listalicious is hosted on Github: "http://github.com/jejacks0n/listalicious":http://github.com/jejacks0n/listalicious, and the gem is available on Gemcutter: "http://gemcutter.org/gems/listalicious":http://gemcutter.org/gems/listalicious
|
258
|
+
|
259
|
+
|
260
|
+
Copyright (c) Jeremy Jackson, released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "listalicious"
|
8
|
+
gem.summary = %Q{Semantic lists (datagrids) for Rails}
|
9
|
+
gem.description = %Q{Semantic listing; a semantic way to build datagrid structures in Rails.}
|
10
|
+
gem.email = "jejacks0n@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/jejacks0n/listalicious"
|
12
|
+
gem.authors = ["Jeremy Jackson"]
|
13
|
+
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
14
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
+
end
|
16
|
+
Jeweler::GemcutterTasks.new
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
19
|
+
end
|
20
|
+
|
21
|
+
require 'rake/testtask'
|
22
|
+
Rake::TestTask.new(:test) do |test|
|
23
|
+
test.libs << 'lib' << 'test'
|
24
|
+
test.pattern = 'test/**/test_*.rb'
|
25
|
+
test.verbose = true
|
26
|
+
end
|
27
|
+
|
28
|
+
begin
|
29
|
+
require 'rcov/rcovtask'
|
30
|
+
Rcov::RcovTask.new do |test|
|
31
|
+
test.libs << 'test'
|
32
|
+
test.pattern = 'test/**/test_*.rb'
|
33
|
+
test.verbose = true
|
34
|
+
end
|
35
|
+
rescue LoadError
|
36
|
+
task :rcov do
|
37
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
task :test => :check_dependencies
|
42
|
+
|
43
|
+
task :default => :test
|
44
|
+
|
45
|
+
require 'rake/rdoctask'
|
46
|
+
Rake::RDocTask.new do |rdoc|
|
47
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
48
|
+
|
49
|
+
rdoc.rdoc_dir = 'rdoc'
|
50
|
+
rdoc.title = "listalicious #{version}"
|
51
|
+
rdoc.rdoc_files.include('README*')
|
52
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
53
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Listalicious
|
2
|
+
class GenericBuilder
|
3
|
+
|
4
|
+
attr_accessor :template, :collection
|
5
|
+
|
6
|
+
def initialize(template, collection, options = {}, &proc)
|
7
|
+
@template, @collection, @options = template, collection, options
|
8
|
+
@object_name = collection.first.class.name.underscore unless collection.empty?
|
9
|
+
|
10
|
+
render(options.delete(:html), &proc)
|
11
|
+
end
|
12
|
+
|
13
|
+
def render(options, &proc); end
|
14
|
+
|
15
|
+
def sortable_link(contents, field)
|
16
|
+
sort_url, sort_direction = sortable_params(field)
|
17
|
+
template.content_tag(:a, contents, :href => "?#{sort_url}", :class => "sort-#{sort_direction == 'descending' ? 'ascending' : 'descending'}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def sortable_params(field)
|
21
|
+
object_name = @object_name.nil? ? '' : "#{@object_name}_"
|
22
|
+
|
23
|
+
params = template.params.reject { |param, value| ['action', 'controller', "#{object_name}sort_desc"].include?(param) }
|
24
|
+
direction = params.delete("#{object_name}sort_asc") == field.to_s ? 'descending' : 'ascending'
|
25
|
+
method = direction == 'descending' ? "#{object_name}sort_desc" : "#{object_name}sort_asc"
|
26
|
+
params[method] = field
|
27
|
+
|
28
|
+
[params.to_query, direction]
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module Listalicious
|
2
|
+
class TableBuilder < GenericBuilder
|
3
|
+
|
4
|
+
attr_accessor :template, :collection
|
5
|
+
|
6
|
+
def render(options, &proc)
|
7
|
+
buffer = template.capture(self, &proc)
|
8
|
+
template.concat(template.content_tag(:table, buffer, options))
|
9
|
+
end
|
10
|
+
|
11
|
+
def head(options = {}, &proc)
|
12
|
+
column_group(:head, options, &proc)
|
13
|
+
end
|
14
|
+
|
15
|
+
def body(options = {}, &proc)
|
16
|
+
column_group(:body, options, &proc)
|
17
|
+
end
|
18
|
+
|
19
|
+
def foot(options = {}, &proc)
|
20
|
+
column_group(:foot, options, &proc)
|
21
|
+
end
|
22
|
+
|
23
|
+
def columns(scope = :body, options = {}, &proc)
|
24
|
+
raise ArgumentError, "Missing block" unless block_given?
|
25
|
+
|
26
|
+
if scope.kind_of? Array
|
27
|
+
scope.collect { |scope| column_group(scope, options, &proc) }
|
28
|
+
else
|
29
|
+
column_group(scope, options, &proc)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def column_group(scope, options = {}, &proc)
|
34
|
+
@current_scope = scope
|
35
|
+
|
36
|
+
options[:html] ||= {}
|
37
|
+
|
38
|
+
self.send("column_group_#{scope}", options, &proc)
|
39
|
+
end
|
40
|
+
|
41
|
+
def column_group_head(options = {}, &proc)
|
42
|
+
@column_count = 0
|
43
|
+
|
44
|
+
@head_wrapper = template.content_tag(:tr, template.capture(collection.first, 0, &proc),
|
45
|
+
options[:html].merge({:class => template.add_class(options[:html][:class], 'header')}))
|
46
|
+
template.content_tag(:thead, @head_wrapper, options.delete(:wrapper_html))
|
47
|
+
end
|
48
|
+
|
49
|
+
def column_group_body(options = {}, &proc)
|
50
|
+
buffer = ''
|
51
|
+
return unless collection.first.present?
|
52
|
+
collection.each_with_index do |record, index|
|
53
|
+
@column_count = 0
|
54
|
+
|
55
|
+
if @options[:grouped_by]
|
56
|
+
buffer << @head_wrapper if record[@options[:grouped_by]] != @last_row_grouping
|
57
|
+
@last_row_grouping = record[@options[:grouped_by]]
|
58
|
+
end
|
59
|
+
|
60
|
+
@cycle = template.cycle('even', 'odd');
|
61
|
+
buffer << template.content_tag(:tr, template.capture(record, index, &proc),
|
62
|
+
options[:html].merge({:class => template.add_class(options[:html][:class], @cycle)}))
|
63
|
+
buffer << template.content_tag(:tr, @extra,
|
64
|
+
options[:html].merge({:class => template.add_class(options[:html][:class], @cycle)})) if @extra.present?
|
65
|
+
@extra = nil
|
66
|
+
end
|
67
|
+
template.content_tag(:tbody, buffer, options.delete(:wrapper_html))
|
68
|
+
end
|
69
|
+
|
70
|
+
def column_group_foot(options = {}, &proc)
|
71
|
+
buffer = template.content_tag(:tr, template.capture(collection.first, 0, &proc), options[:html])
|
72
|
+
template.content_tag(:tfoot, buffer, options.delete(:wrapper_html))
|
73
|
+
end
|
74
|
+
|
75
|
+
def column(*args, &proc)
|
76
|
+
options = args.extract_options!
|
77
|
+
contents = options == args.first ? nil : args.first
|
78
|
+
|
79
|
+
if @current_scope == :body
|
80
|
+
contents = template.capture(self, &proc) if block_given? && collection.first.present?
|
81
|
+
else
|
82
|
+
contents = options[:title] || contents
|
83
|
+
end
|
84
|
+
|
85
|
+
@column_count = @column_count + 1
|
86
|
+
self.send("#{@current_scope}_column", contents, options)
|
87
|
+
end
|
88
|
+
|
89
|
+
def full_column(contents = nil, options = {}, &proc)
|
90
|
+
raise ArgumentError, "Must provide a string or a block" if !block_given? && contents.nil?
|
91
|
+
contents ||= template.capture(self, &proc)
|
92
|
+
|
93
|
+
options[:html] ||= {}
|
94
|
+
options[:html][:colspan] ||= @column_count
|
95
|
+
|
96
|
+
options[:wrapper_html] ||= {}
|
97
|
+
options[:wrapper_html][:class] = template.add_class(options[:wrapper_html][:class], 'full-column')
|
98
|
+
|
99
|
+
@extra = self.send("#{@current_scope}_column", contents, options)
|
100
|
+
end
|
101
|
+
|
102
|
+
def controls(contents = nil, options = {}, &proc)
|
103
|
+
return unless @current_scope == :body
|
104
|
+
raise ArgumentError, "Must provide a string or a block" if !block_given? && contents.nil?
|
105
|
+
contents ||= template.capture(self, &proc)
|
106
|
+
|
107
|
+
options[:html] ||= {}
|
108
|
+
options[:html][:class] = template.add_class(options[:html][:class], 'controls')
|
109
|
+
|
110
|
+
self.send("#{@current_scope}_column", contents, options)
|
111
|
+
end
|
112
|
+
|
113
|
+
def extra(contents = nil, options = {}, &proc)
|
114
|
+
return unless @current_scope == :body
|
115
|
+
raise ArgumentError, "Must provide a string or a block" if !block_given? && contents.nil?
|
116
|
+
contents ||= template.capture(self, &proc)
|
117
|
+
|
118
|
+
options[:html] ||= {}
|
119
|
+
options[:html][:colspan] ||= @column_count
|
120
|
+
|
121
|
+
options[:wrapper_html] ||= {}
|
122
|
+
options[:wrapper_html][:class] = template.add_class(options[:wrapper_html][:class], 'extra')
|
123
|
+
|
124
|
+
@extra = self.send("#{@current_scope}_column", contents, options)
|
125
|
+
''
|
126
|
+
end
|
127
|
+
|
128
|
+
def head_column(contents, options = {})
|
129
|
+
options[:html] ||= {}
|
130
|
+
options[:html][:width] ||= options[:width]
|
131
|
+
|
132
|
+
contents = sortable_link(contents, options[:sort]) if options[:sort]
|
133
|
+
|
134
|
+
template.content_tag(:th, contents, options.delete(:html))
|
135
|
+
end
|
136
|
+
|
137
|
+
def body_column(contents, options = {})
|
138
|
+
template.content_tag(:td, contents, options.delete(:html))
|
139
|
+
end
|
140
|
+
|
141
|
+
def foot_column(contents, options = {})
|
142
|
+
contents = sortable_link(contents, options[:sort]) if options[:sort]
|
143
|
+
|
144
|
+
template.content_tag(:th, contents, options.delete(:html))
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
data/lib/listalicious.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require File.join(File.dirname(__FILE__), *%w[builders generic_builder])
|
3
|
+
require File.join(File.dirname(__FILE__), *%w[builders table_builder])
|
4
|
+
|
5
|
+
module Listalicious #:nodoc:
|
6
|
+
|
7
|
+
# Semantic list helper methods
|
8
|
+
#
|
9
|
+
# Example Usage:
|
10
|
+
#
|
11
|
+
module SemanticListHelper
|
12
|
+
|
13
|
+
@@builder = ::Listalicious::TableBuilder
|
14
|
+
mattr_accessor :builder
|
15
|
+
|
16
|
+
def semantic_list_for(collection, *args, &proc)
|
17
|
+
raise ArgumentError, "Missing block" unless block_given?
|
18
|
+
|
19
|
+
options = args.extract_options!
|
20
|
+
|
21
|
+
options[:html] ||= {}
|
22
|
+
options[:html][:class] = add_class(options[:html][:class], 'semantic-list')
|
23
|
+
options[:html][:id] ||= collection.first ? "#{collection.first.class.name.underscore}_list" : 'semantic_list'
|
24
|
+
|
25
|
+
options[:html][:class] = add_class(options[:html][:class], 'actionable') if options[:actionable]
|
26
|
+
options[:html][:class] = add_class(options[:html][:class], 'selectable') if options[:selectable]
|
27
|
+
|
28
|
+
if options[:sort_url]
|
29
|
+
options[:html][:class] = add_class(options[:html][:class], 'sortable')
|
30
|
+
options[:html]['data-sorturl'] = url_for(options[:sort_url])
|
31
|
+
end
|
32
|
+
|
33
|
+
builder = options[:builder] || TableBuilder
|
34
|
+
builder.new(@template, collection, options, &proc)
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_class(classnames, classname)
|
38
|
+
out = (classnames.is_a?(String) ? classnames.split(' ') : []) << classname
|
39
|
+
out.join(' ')
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
module ActiveRecordExtensions # :nodoc:
|
45
|
+
|
46
|
+
def self.included(base) # :nodoc:
|
47
|
+
return if base.kind_of?(::Listalicious::ActiveRecordExtensions::ClassMethods)
|
48
|
+
base.class_eval do
|
49
|
+
extend ClassMethods
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
|
55
|
+
attr_accessor :default_sort_field
|
56
|
+
|
57
|
+
def sortable_fields(*args)
|
58
|
+
options = args.extract_options!
|
59
|
+
@acceptable_sort_fields = args
|
60
|
+
@default_sort_field = options[:default]
|
61
|
+
end
|
62
|
+
|
63
|
+
def acceptable_sort_field?(column)
|
64
|
+
column.present? ? @acceptable_sort_fields.include?(column.to_sym) : false
|
65
|
+
end
|
66
|
+
|
67
|
+
def sort_order_from(params)
|
68
|
+
field = params["#{self.name.underscore}_sort_asc"] || params["#{self.name.underscore}_sort_desc"]
|
69
|
+
field = @default_sort_field.to_s unless acceptable_sort_field?(field)
|
70
|
+
|
71
|
+
method = (params["#{self.name.underscore}_sort_desc".to_sym] == field) ? 'DESC' : 'ASC'
|
72
|
+
"#{field} #{method}" unless field.blank?
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
ActionController::Base.helper Listalicious::SemanticListHelper
|
82
|
+
ActiveRecord::Base.class_eval { include ::Listalicious::ActiveRecordExtensions }
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{listalicious}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Jeremy Jackson"]
|
12
|
+
s.date = %q{2010-01-24}
|
13
|
+
s.description = %q{Semantic listing; a semantic way to build datagrid structures in Rails.}
|
14
|
+
s.email = %q{jejacks0n@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.textile"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"LICENSE",
|
23
|
+
"README.textile",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"lib/builders/generic_builder.rb",
|
27
|
+
"lib/builders/table_builder.rb",
|
28
|
+
"lib/listalicious.rb",
|
29
|
+
"listalicious.gemspec",
|
30
|
+
"test/helper.rb",
|
31
|
+
"test/test_listalicious.rb"
|
32
|
+
]
|
33
|
+
s.homepage = %q{http://github.com/jejacks0n/listalicious}
|
34
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
35
|
+
s.require_paths = ["lib"]
|
36
|
+
s.rubygems_version = %q{1.3.5}
|
37
|
+
s.summary = %q{Semantic lists (datagrids) for Rails}
|
38
|
+
s.test_files = [
|
39
|
+
"test/helper.rb",
|
40
|
+
"test/test_listalicious.rb"
|
41
|
+
]
|
42
|
+
|
43
|
+
if s.respond_to? :specification_version then
|
44
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
45
|
+
s.specification_version = 3
|
46
|
+
|
47
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
48
|
+
s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
49
|
+
else
|
50
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
51
|
+
end
|
52
|
+
else
|
53
|
+
s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
data/test/helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: listalicious
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jeremy Jackson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-24 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: thoughtbot-shoulda
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
description: Semantic listing; a semantic way to build datagrid structures in Rails.
|
26
|
+
email: jejacks0n@gmail.com
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files:
|
32
|
+
- LICENSE
|
33
|
+
- README.textile
|
34
|
+
files:
|
35
|
+
- .document
|
36
|
+
- .gitignore
|
37
|
+
- LICENSE
|
38
|
+
- README.textile
|
39
|
+
- Rakefile
|
40
|
+
- VERSION
|
41
|
+
- lib/builders/generic_builder.rb
|
42
|
+
- lib/builders/table_builder.rb
|
43
|
+
- lib/listalicious.rb
|
44
|
+
- listalicious.gemspec
|
45
|
+
- test/helper.rb
|
46
|
+
- test/test_listalicious.rb
|
47
|
+
has_rdoc: true
|
48
|
+
homepage: http://github.com/jejacks0n/listalicious
|
49
|
+
licenses: []
|
50
|
+
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options:
|
53
|
+
- --charset=UTF-8
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.3.5
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Semantic lists (datagrids) for Rails
|
75
|
+
test_files:
|
76
|
+
- test/helper.rb
|
77
|
+
- test/test_listalicious.rb
|