listpress 0.1.11
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/.gitignore +8 -0
- data/Gemfile +6 -0
- data/README.md +94 -0
- data/Rakefile +2 -0
- data/app/assets/javascript/listpress.js +130 -0
- data/app/assets/stylesheets/listpress.scss +13 -0
- data/app/views/shared/_listing.html.erb +28 -0
- data/app/views/shared/_listing_filters.html.erb +34 -0
- data/app/views/shared/_listing_table.html.erb +37 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/listpress/bootstrap_paginate_renderer.rb +47 -0
- data/lib/listpress/config.rb +10 -0
- data/lib/listpress/default_resolver.rb +66 -0
- data/lib/listpress/engine.rb +15 -0
- data/lib/listpress/helper.rb +74 -0
- data/lib/listpress/listing.rb +161 -0
- data/lib/listpress/locale/cs.yml +7 -0
- data/lib/listpress/locale/en.yml +7 -0
- data/lib/listpress/materialize_paginate_renderer.rb +36 -0
- data/lib/listpress/version.rb +3 -0
- data/lib/listpress.rb +13 -0
- data/listpress.gemspec +37 -0
- metadata +109 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3695595a23f6a3377c6248a0c9e01d9c6702e1a526567daebbb9d4724392f226
|
4
|
+
data.tar.gz: 0fb82ad6524ed2a9c80d8cf6f8d78751c4b5c14f80adce353e127e37e5098573
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f3d0826640b1c3e4ba011a71ea41e0bd4203ffe98b730aca7535d48a9b0809e39d05ae4a374aab133ca295aba71d2d62c1ef20a8612bbfb452276b5a9a3e8982
|
7
|
+
data.tar.gz: ad69433776dbf5fc5d008416277df82556bc2a9d628301c609eda730b7c139dd80be1715206520d221bbc49c1a979c1f906d945f77e61cbd9e257b70ff839582
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# Listpress
|
2
|
+
|
3
|
+
Listing helper for Timepress projects
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'listpress', :git => 'git@git.timepress.cz:timepress/listpress.git', :branch => "master"
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle install
|
16
|
+
|
17
|
+
Add include to your `application.js`
|
18
|
+
|
19
|
+
//= require listpress
|
20
|
+
|
21
|
+
And styles to your `application.scss`
|
22
|
+
|
23
|
+
@import 'listpress';
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
In your view:
|
28
|
+
|
29
|
+
```erb
|
30
|
+
<%= listing @messages, per_page: 100, default_sort: "date:desc", default_filter: { resolved: false }, table_options: { class: "compact"} do |l| %>
|
31
|
+
<%# :code filter will use where(code: filter_value), because model has this attribute %>
|
32
|
+
<% l.filter :code, as: :select, collection: Message.distinct.order(:code).pluck(:code).collect{|c| [c, c]} %>
|
33
|
+
|
34
|
+
<%# :resolved filter will call method "resolved" on collection passing filter value as argument, (instead of attribute :resolved) %>
|
35
|
+
<% l.filter :resolved, as: :boolean, method: :resolved %>
|
36
|
+
|
37
|
+
<%# will use search method %>
|
38
|
+
<% l.filter :search, as: :search %>
|
39
|
+
|
40
|
+
<%# set attributes for whole row %>
|
41
|
+
<% l.row_attributes {|item| { class: item.red? "red" : "" }} %>
|
42
|
+
|
43
|
+
<%# add class "nowrap", allow sorting by :date, custom output specified with a block %>
|
44
|
+
<% l.column(:date, class: "nowrap", sort: true, th_options: {title: "Wow such dates"}, td_options: {title: "Very short"}) {|m| lf m.date, format: :short} %>
|
45
|
+
|
46
|
+
<%# passes the output (even if given by block) through format_helper() %>
|
47
|
+
<% l.column(:number, class: "num", helper: :format_number) %>
|
48
|
+
|
49
|
+
<%# custom field label and output %>
|
50
|
+
<% l.column("!", class: "nowrap") do |m| %>
|
51
|
+
<span title="Status" style="color: red"><%= m.status %></span>
|
52
|
+
<% end %>
|
53
|
+
|
54
|
+
<% sort by custom SQL %>
|
55
|
+
<% l.column(:customer, sort: Arel.sql("customers.name")) {|m| m.customer.name} %>
|
56
|
+
|
57
|
+
<% l.column(:subject, sort: true) do |m| %>
|
58
|
+
<% if m.resolved? %>
|
59
|
+
<%= link_to m.subject, ote_msg_path(m, type: 'dec') %>
|
60
|
+
<% else %>
|
61
|
+
<b><%= link_to m.subject, ote_msg_path(m, type: 'dec') %></b>
|
62
|
+
<% end %>
|
63
|
+
<% end %>
|
64
|
+
<% end %>
|
65
|
+
```
|
66
|
+
|
67
|
+
In your controller:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
def index
|
71
|
+
@messages = Message.all # listing gets the whole collection and it will apply sorting, filtering and paging on it's own
|
72
|
+
|
73
|
+
respond_with_listing # just as render :index, but with some tricks to manage AJAX requests
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Config
|
78
|
+
|
79
|
+
You can change some setting in `Listpress::Config` class (for example in initializers).
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
Listpress::Config.color = "green" # affects class of paginate links
|
83
|
+
Listpress::Config.paginate_links_renderer = MaterializePaginateRenderer # affects how WillPaginate renders the paging links
|
84
|
+
```
|
85
|
+
|
86
|
+
## Overriding views
|
87
|
+
|
88
|
+
You can copy and override these templates:
|
89
|
+
|
90
|
+
```
|
91
|
+
app/views/shared/_listing.html.erb
|
92
|
+
app/views/shared/_listing_filters.html.erb
|
93
|
+
app/views/shared/_listing_table.html.erb
|
94
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
$(document).ready(function () {
|
2
|
+
function updateUrlWithParams(url, params) {
|
3
|
+
var query;
|
4
|
+
|
5
|
+
// split query from url
|
6
|
+
var pos = url.indexOf('?');
|
7
|
+
if (pos > 0) {
|
8
|
+
query = url.substring(pos + 1);
|
9
|
+
url = url.substr(0, pos);
|
10
|
+
} else {
|
11
|
+
query = "";
|
12
|
+
}
|
13
|
+
|
14
|
+
// parse query into an object
|
15
|
+
var parsed_query = {};
|
16
|
+
query.replace(/&*([^=&]+)=([^=&]*)/gi, function(str,key,value) {parsed_query[decodeURIComponent(key.replace(/\+/g, ' '))] = decodeURIComponent(value.replace(/\+/g, ' '))});
|
17
|
+
|
18
|
+
// update query with params
|
19
|
+
$.each(params, function (i, param) {
|
20
|
+
parsed_query[param[0]] = param[1];
|
21
|
+
});
|
22
|
+
|
23
|
+
// create new URL
|
24
|
+
var sep = '?';
|
25
|
+
$.each(parsed_query, function (k, val) {
|
26
|
+
url += sep + encodeURIComponent(k) + '=' + encodeURIComponent(val);
|
27
|
+
sep = '&'
|
28
|
+
});
|
29
|
+
|
30
|
+
return url;
|
31
|
+
}
|
32
|
+
|
33
|
+
// timeout used so URL is not updated too often on typing
|
34
|
+
var pushStateTimeout = null;
|
35
|
+
|
36
|
+
// url that represents the last known state
|
37
|
+
var currentStateUrl = location.href;
|
38
|
+
|
39
|
+
// currently running AJAX
|
40
|
+
var currentXhr = null;
|
41
|
+
|
42
|
+
$('.listing').each(function () {
|
43
|
+
var $listing = $(this);
|
44
|
+
var $filters = $listing.find('.filters');
|
45
|
+
var $table = $listing.find('.table');
|
46
|
+
var $pages = $listing.find('.pages');
|
47
|
+
var name = $listing.data('name');
|
48
|
+
|
49
|
+
// factory for success function
|
50
|
+
var updateFn = function (delayedPush) {
|
51
|
+
return function (data) {
|
52
|
+
// update html
|
53
|
+
$table.html(data.table);
|
54
|
+
$pages.html(data.pages);
|
55
|
+
|
56
|
+
// update current state according to params sent from Rails
|
57
|
+
currentStateUrl = updateUrlWithParams(location.href, data.params);
|
58
|
+
|
59
|
+
// push state to history
|
60
|
+
if (pushStateTimeout) clearTimeout(pushStateTimeout);
|
61
|
+
if (delayedPush) {
|
62
|
+
pushStateTimeout = setTimeout(function () {history.pushState(location.href, "listing", currentStateUrl)}, 300);
|
63
|
+
} else {
|
64
|
+
history.pushState(location.href, "listing", currentStateUrl);
|
65
|
+
}
|
66
|
+
|
67
|
+
$listing.trigger('update', currentStateUrl);
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
// makes AJAX request and handles the response
|
72
|
+
var doRequest = function (url, delayedPush) {
|
73
|
+
if (currentXhr) currentXhr.abort();
|
74
|
+
|
75
|
+
currentXhr = $.ajax(url, {
|
76
|
+
method: 'GET',
|
77
|
+
dataType: 'json',
|
78
|
+
success: updateFn(delayedPush),
|
79
|
+
error: function (xhr, status, error) {
|
80
|
+
if (status != "abort") {
|
81
|
+
console.log("AJAX failed:", xhr, status, error);
|
82
|
+
window.location = url;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
});
|
86
|
+
};
|
87
|
+
|
88
|
+
// handle paging links
|
89
|
+
$pages.on('click', 'a[href]', function () {
|
90
|
+
doRequest(updateUrlWithParams(currentStateUrl, [["listing", name], [name + "[page]", $(this).data('page')]]));
|
91
|
+
return false;
|
92
|
+
});
|
93
|
+
|
94
|
+
// handle sorting links
|
95
|
+
$table.on('click', 'a.sort', function () {
|
96
|
+
doRequest(updateUrlWithParams(currentStateUrl, [["listing", name], [name + "[sort]", $(this).data('sort')]]));
|
97
|
+
return false;
|
98
|
+
});
|
99
|
+
|
100
|
+
// handle filters
|
101
|
+
var filterTimeout = null;
|
102
|
+
|
103
|
+
$filters.on('submit', 'form', function () {return false;}); // prevent manual form submit
|
104
|
+
|
105
|
+
var submitFilters = function () {
|
106
|
+
// collect form data as params
|
107
|
+
var data = [];
|
108
|
+
$.each($filters.find('form').serializeArray(), function (i, o) {
|
109
|
+
if (o.name.substr(0, name.length + 8) == name + '[filter]') {
|
110
|
+
data.push([o.name, o.value])
|
111
|
+
}
|
112
|
+
});
|
113
|
+
data.push(['listing', name]);
|
114
|
+
data.push([name + '[page]', 1]);
|
115
|
+
|
116
|
+
// send
|
117
|
+
doRequest(updateUrlWithParams(currentStateUrl, data), true);
|
118
|
+
};
|
119
|
+
|
120
|
+
$filters.on('input', 'input[type=text],textarea', function () {
|
121
|
+
if (filterTimeout) clearTimeout(filterTimeout);
|
122
|
+
filterTimeout = setTimeout(submitFilters, 200);
|
123
|
+
});
|
124
|
+
$filters.on('change', 'select', submitFilters);
|
125
|
+
});
|
126
|
+
|
127
|
+
$(window).on('popstate', function (e) {
|
128
|
+
location.reload()
|
129
|
+
})
|
130
|
+
});
|
@@ -0,0 +1,13 @@
|
|
1
|
+
.listing {
|
2
|
+
.sort-direction { font-size: 0.8rem; position: relative; top: -2px; }
|
3
|
+
.pages { padding: 15px 0;
|
4
|
+
.pagination { margin: 0; }
|
5
|
+
.pagination-info { float: right; font-size: 1.1rem; line-height: 2.0; margin: 0px 0; }
|
6
|
+
}
|
7
|
+
|
8
|
+
form { float: right; clear: right;
|
9
|
+
.input-field.inline { margin-top: -5px; margin-bottom: 0; }
|
10
|
+
}
|
11
|
+
table { clear: both; }
|
12
|
+
.list-empty td {height: 100px;line-height: 100px;text-align: center;vertical-align: middle;}
|
13
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<div class="listing <%= list.options[:class] %>" data-name="<%= list.name %>">
|
2
|
+
<div class="filters">
|
3
|
+
<%# render and capture filters %>
|
4
|
+
<%= list.captures[:filters] = capture do %>
|
5
|
+
<% if list.filters.any? %>
|
6
|
+
<%= render "shared/listing_filters", list: list %>
|
7
|
+
<% end %>
|
8
|
+
<% end %>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div class="table">
|
12
|
+
<%# render and capture table %>
|
13
|
+
<%= list.captures[:table] = capture do %>
|
14
|
+
<%= render "shared/listing_table", list: list %>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<div class="pages">
|
19
|
+
<%# render and capture pages %>
|
20
|
+
<%= list.captures[:pages] = capture do %>
|
21
|
+
<% if list.paginate? %>
|
22
|
+
<div class="pagination-info"><%= page_entries_info list.collection, model: list.collection.model %></div>
|
23
|
+
<%= will_paginate list.collection, renderer: Listpress::Config.paginate_link_renderer, param_name: list.page_param_name %>
|
24
|
+
<% end %>
|
25
|
+
<% end %>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
<%= form_with url: request.params, method: :get do |f| %>
|
2
|
+
<%# make hidden fields with all the GET params so they don't get lost on submit %>
|
3
|
+
<% list.flatten_params(request.GET).each do |k, v| %>
|
4
|
+
<%= f.hidden_field k, value: v unless ['utf8', list.page_param_name].include?(k) %>
|
5
|
+
<% end %>
|
6
|
+
|
7
|
+
<%# reset page when changing filters %>
|
8
|
+
<%= f.hidden_field list.page_param_name, value: 1 %>
|
9
|
+
|
10
|
+
<%# render filter fields %>
|
11
|
+
<% list.filters.each do |filter| %>
|
12
|
+
<% field_name = "#{list.name}[filter][#{filter[:name]}]" %>
|
13
|
+
<% field_value = list.filter_value(filter[:name]) %>
|
14
|
+
<% case filter[:as] %>
|
15
|
+
<% when :boolean %>
|
16
|
+
<div class="input-field inline">
|
17
|
+
<%= f.select field_name, [[t('listpress.do_not_filter'), nil], [t('listpress.yes'), true], [t('listpress.no'), false]], selected: field_value %>
|
18
|
+
<label><%= list.human_name(filter[:name]) %></label>
|
19
|
+
</div>
|
20
|
+
<% when :select %>
|
21
|
+
<div class="input-field inline">
|
22
|
+
<%= f.select field_name, [[t('listpress.do_not_filter'), ""]] + filter[:collection], selected: field_value %>
|
23
|
+
<label><%= list.human_name(filter[:name]) %></label>
|
24
|
+
</div>
|
25
|
+
<% when :search %>
|
26
|
+
<div class="input-field inline">
|
27
|
+
<i class="material-icons prefix">search</i>
|
28
|
+
<%= f.text_field field_name, value: field_value, placeholder: t('listpress.search') %>
|
29
|
+
</div>
|
30
|
+
<% else %>
|
31
|
+
Unknown filter type <%= filter[:as] %>
|
32
|
+
<% end %>
|
33
|
+
<% end %>
|
34
|
+
<% end %>
|
@@ -0,0 +1,37 @@
|
|
1
|
+
<%= content_tag :table, list.options[:table_options] do %>
|
2
|
+
<thead>
|
3
|
+
<tr>
|
4
|
+
<%# render column headers with sorting indicator and links %>
|
5
|
+
<% sort_attr, sort_dir = list.sort %>
|
6
|
+
<% list.columns.each do |col| %>
|
7
|
+
<%= content_tag :th, (col[:th_options] || {}).merge(class: col[:class]) do %>
|
8
|
+
<% if col[:sort] %>
|
9
|
+
<% sort = sort_attr == col[:attr] && sort_dir == :asc ? "#{col[:attr]}:desc" : "#{col[:attr]}:asc" %>
|
10
|
+
<%= link_to list.human_name(col[:attr]), request.params.deep_merge(list.name => {sort: sort}), class: "sort", "data-sort": sort %>
|
11
|
+
<% if col[:attr] == sort_attr %>
|
12
|
+
<span class="sort-direction"><%= sort_dir == :asc ? "▲" : "▼" %></span>
|
13
|
+
<% end %>
|
14
|
+
<% else %>
|
15
|
+
<%= list.human_name(col[:attr]) %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
<% end %>
|
19
|
+
</tr>
|
20
|
+
</thead>
|
21
|
+
<tbody>
|
22
|
+
<% list.collection.each do |item| %>
|
23
|
+
<%= content_tag :tr, list.row_attributes[:proc]&.call(item) || list.row_attributes do %>
|
24
|
+
<%# render each row according to it's definition %>
|
25
|
+
<% list.columns.each do |col| %>
|
26
|
+
<%= content_tag :td, (col[:td_options] || {}).merge(class: col[:class]) do %>
|
27
|
+
<% v = col[:proc] ? capture { col[:proc].call(item).to_s } : item.public_send(col[:attr]) %>
|
28
|
+
<%= col[:helper] ? send(col[:helper], v) : v %>
|
29
|
+
<% end %>
|
30
|
+
<% end %>
|
31
|
+
<% end %>
|
32
|
+
<% end %>
|
33
|
+
<% if list.collection.empty? %>
|
34
|
+
<tr class="list-empty"><td colspan="<%= list.columns.size %>"><%= t('listpress.no_results') %></td></tr>
|
35
|
+
<% end %>
|
36
|
+
</tbody>
|
37
|
+
<% end %>
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "listpress"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'will_paginate'
|
2
|
+
require 'will_paginate/view_helpers/action_view'
|
3
|
+
|
4
|
+
module Listpress
|
5
|
+
class BootstrapPaginateRenderer < WillPaginate::ActionView::LinkRenderer
|
6
|
+
def html_container(html)
|
7
|
+
tag(:nav, tag(:ul, html, class: "pagination"))
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_number(page)
|
11
|
+
if page == current_page
|
12
|
+
"<li class=\"page-item active\">" + link(page, page, rel: rel_value(page), "data-page": page) + "</li>"
|
13
|
+
else
|
14
|
+
"<li class=\"page-item\">" + link(page, page, rel: rel_value(page), "data-page": page) + "</li>"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def previous_page
|
19
|
+
num = @collection.current_page > 1 && @collection.current_page - 1
|
20
|
+
previous_or_next_page(num, "<i class=\"fas fa-chevron-left\"></i>")
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_page
|
24
|
+
num = @collection.current_page < total_pages && @collection.current_page + 1
|
25
|
+
previous_or_next_page(num, "<i class=\"fas fa-chevron-right\"></i>")
|
26
|
+
end
|
27
|
+
|
28
|
+
def previous_or_next_page(page, text)
|
29
|
+
if page
|
30
|
+
"<li class=\"page-item\">" + link(text, page, "data-page": page) + "</li>"
|
31
|
+
else
|
32
|
+
"<li class=\"page-item disabled\"><span class=\"page-link\">" + text + "</span></li>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def link(text, target, attributes = {})
|
37
|
+
if target.is_a?(Integer)
|
38
|
+
attributes[:rel] = rel_value(target)
|
39
|
+
target = url(target)
|
40
|
+
end
|
41
|
+
attributes[:href] = target
|
42
|
+
attributes[:class] = 'page-link'
|
43
|
+
tag(:a, text, attributes)
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Listpress
|
2
|
+
class DefaultResolver
|
3
|
+
attr_reader :listing
|
4
|
+
|
5
|
+
def initialize(collection, listing)
|
6
|
+
@collection = collection
|
7
|
+
@listing = listing
|
8
|
+
end
|
9
|
+
|
10
|
+
# apply filters to collection
|
11
|
+
def filter(collection)
|
12
|
+
listing.filters.each do |filter|
|
13
|
+
val = listing.filter_value(filter[:name])
|
14
|
+
|
15
|
+
next if val.nil? || val == "" # beware of false (valid filter value)
|
16
|
+
|
17
|
+
if filter[:proc]
|
18
|
+
collection = filter[:proc].call(collection, val)
|
19
|
+
elsif filter[:method]
|
20
|
+
collection = collection.public_send(filter[:method], val)
|
21
|
+
elsif filter[:column]
|
22
|
+
collection = collection.where(filter[:column] => val)
|
23
|
+
else
|
24
|
+
if collection.respond_to?(filter[:name]) && !collection.model.column_names.include?(filter[:name].to_s)
|
25
|
+
collection = collection.public_send(filter[:name], val)
|
26
|
+
elsif
|
27
|
+
collection = collection.where(filter[:name] => val)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
collection
|
33
|
+
end
|
34
|
+
|
35
|
+
# apply sort to collection
|
36
|
+
def sort(collection)
|
37
|
+
attr, dir = listing.sort
|
38
|
+
if attr && column = listing.columns.detect {|c| c[:attr] == attr}
|
39
|
+
case column[:sort]
|
40
|
+
when true
|
41
|
+
collection = collection.order(attr => dir)
|
42
|
+
when Symbol, String
|
43
|
+
collection = collection.order(column[:sort] => dir)
|
44
|
+
when Hash
|
45
|
+
collection = collection.order(column[:sort][dir] || (raise ArgumentError, "When using hash as a :sort option for a column, :asc and :desc keys are expected."))
|
46
|
+
else
|
47
|
+
# do not sort
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
collection
|
52
|
+
end
|
53
|
+
|
54
|
+
def page(collection)
|
55
|
+
if listing.paginate?
|
56
|
+
collection = collection.paginate(page: listing.params[:page] || 1, per_page: listing.options[:per_page])
|
57
|
+
end
|
58
|
+
|
59
|
+
collection
|
60
|
+
end
|
61
|
+
|
62
|
+
def collection
|
63
|
+
@_collection_cache ||= page(sort(filter(@collection)))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Listpress
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
initializer 'listpress' do
|
4
|
+
ActiveSupport.on_load(:action_view) do
|
5
|
+
include Listpress::Helper
|
6
|
+
end
|
7
|
+
|
8
|
+
ActiveSupport.on_load :i18n do
|
9
|
+
I18n.load_path.concat(Dir[File.join(File.dirname(__FILE__), 'locale', '*.yml')])
|
10
|
+
end
|
11
|
+
|
12
|
+
ActionController::Base.send :include, Helper
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Listpress
|
2
|
+
module Helper
|
3
|
+
# Used to define and render Listing.
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
# <%= listing collection, options do |l| %>
|
7
|
+
# <% l.column :id %>
|
8
|
+
# ...
|
9
|
+
# <% end %>
|
10
|
+
#
|
11
|
+
# Options:
|
12
|
+
# name: Identifies listing within a page - required for multiple listings on one page, (default: :list)
|
13
|
+
# per_page: Limit output to this many items and turn paging on
|
14
|
+
# params: Use these params instead of parsing them from request
|
15
|
+
# default_sort: Use this sort if none specified in params (eg: "id:asc")
|
16
|
+
#
|
17
|
+
def listing(collection, options = {}, &block)
|
18
|
+
options = options.dup
|
19
|
+
|
20
|
+
name = options.delete(:name)&.to_sym || :list
|
21
|
+
pars = options.delete(:params) || params[name] || {}
|
22
|
+
|
23
|
+
if @_listing_only.nil? || @_listing_only == name
|
24
|
+
list = Listing.new(name, collection, options, pars)
|
25
|
+
yield list
|
26
|
+
output = render 'shared/listing', list: list
|
27
|
+
|
28
|
+
if controller.respond_to?(:listing_content)
|
29
|
+
controller.listing_content[name] = list.captures
|
30
|
+
end
|
31
|
+
if controller.respond_to?(:listing_instances)
|
32
|
+
controller.listing_instances[name] = list
|
33
|
+
end
|
34
|
+
|
35
|
+
output
|
36
|
+
else
|
37
|
+
""
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Use in controller's index action to automatically handle AJAX response for listing
|
42
|
+
#
|
43
|
+
# Responds as usually for HTML requests, renders only the selected listing for AJAX.
|
44
|
+
def respond_with_listing(action = :index)
|
45
|
+
respond_to do |format|
|
46
|
+
format.html { render action }
|
47
|
+
format.json {
|
48
|
+
render_listing(action, name = params[:listing].presence&.to_sym || :list)
|
49
|
+
render json: listing_content[name].merge(params: Listing.flatten_params(request.GET.except(:listing)))
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def render_listing(action = :index, name = nil)
|
55
|
+
list_only(name)
|
56
|
+
render_to_string action: action, formats: [:html], layout: nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Used in controller to store rendered listing content for JSON response
|
60
|
+
def listing_content
|
61
|
+
@_listing_content ||= {}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Used in controller to retrieve rendered listing instance
|
65
|
+
def listing_instances
|
66
|
+
@_listing_instances ||= {}
|
67
|
+
end
|
68
|
+
|
69
|
+
# Used in controller to limit listing rendering only for +name+
|
70
|
+
def list_only(name)
|
71
|
+
@_listing_only = name
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module Listpress
|
2
|
+
class Listing
|
3
|
+
attr_reader :name, :collection, :filters, :options, :columns, :captures, :params
|
4
|
+
|
5
|
+
def initialize(name, collection, options = {}, params = {})
|
6
|
+
@name = name
|
7
|
+
@collection = collection
|
8
|
+
@options = options
|
9
|
+
@params = params
|
10
|
+
@columns = []
|
11
|
+
@filters = []
|
12
|
+
@captures = {}
|
13
|
+
@row_attributes = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Defines a column
|
17
|
+
#
|
18
|
+
# Usage:
|
19
|
+
# <%= listing collection, options do |l| %>
|
20
|
+
# <% l.column :id, class: "right-align", sort: true %>
|
21
|
+
# <% l.column :name, sort: true, helper: :shorten %>
|
22
|
+
# <% l.column :special, th_options: { title: "Special column"}, td_options: { title: "With special cells" } %>
|
23
|
+
#
|
24
|
+
# Options:
|
25
|
+
# class: Used as HTML class for this column's cells
|
26
|
+
# sort: Allows sorting by this attribute
|
27
|
+
def column(*args, &block)
|
28
|
+
opts = args.extract_options!
|
29
|
+
attr = args[0] || ""
|
30
|
+
|
31
|
+
column = opts.merge(attr: attr)
|
32
|
+
column[:proc] = block if block_given?
|
33
|
+
|
34
|
+
@columns << column
|
35
|
+
end
|
36
|
+
|
37
|
+
# Defines attributes for each row (tr)
|
38
|
+
#
|
39
|
+
# Usage:
|
40
|
+
# <%= listing collection, options do |l| %>
|
41
|
+
# <% l.row_attributes {|item| { class: item.red? "red" : "" }} %>
|
42
|
+
# ...
|
43
|
+
def row_attributes(*args, &block)
|
44
|
+
if block_given?
|
45
|
+
@row_attributes[:proc] = block
|
46
|
+
elsif !args.empty?
|
47
|
+
@row_attributes = args.extract_options!
|
48
|
+
else
|
49
|
+
@row_attributes
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Defines a filter
|
54
|
+
#
|
55
|
+
# Usage:
|
56
|
+
# <%= listing collection, options do |l| %>
|
57
|
+
# <% l.filter :search, as: :search %>
|
58
|
+
# <% l.filter :name, as: :text %>
|
59
|
+
# <% l.filter(:active, as: :boolean) {|collection, value| collection.where(active: value)} %>
|
60
|
+
# ...
|
61
|
+
# <% end %>
|
62
|
+
#
|
63
|
+
# Filter value is either used on collection method with the same name as the filter (ie. the first filter calls `collection.search(filter_value)`)
|
64
|
+
# or you can use block to alter your collection
|
65
|
+
#
|
66
|
+
def filter(*args, &block)
|
67
|
+
opts = args.extract_options!
|
68
|
+
name = args[0] || ""
|
69
|
+
|
70
|
+
filter = opts.merge(name: name)
|
71
|
+
filter[:proc] = block if block_given?
|
72
|
+
|
73
|
+
@filters << filter
|
74
|
+
end
|
75
|
+
|
76
|
+
# finds human_name (using I18n) for column +attr+
|
77
|
+
def human_name(attr)
|
78
|
+
return "" unless attr.present?
|
79
|
+
|
80
|
+
return attr if attr.is_a?(String)
|
81
|
+
|
82
|
+
if @collection.respond_to?(:model)
|
83
|
+
@collection.model.human_attribute_name(attr)
|
84
|
+
elsif @collection.respond_to?(:human_attribute_name)
|
85
|
+
@collection.human_attribute_name(attr)
|
86
|
+
else
|
87
|
+
attr.to_s.humanize
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# return resolver object which decides how to filter, sort and page a collection according to listing params and options
|
92
|
+
def resolver
|
93
|
+
@resolver ||= (options[:resolver] || DefaultResolver).new(@collection, self)
|
94
|
+
end
|
95
|
+
|
96
|
+
# returns filtered, sorted and paged collection
|
97
|
+
def collection
|
98
|
+
resolver.collection
|
99
|
+
end
|
100
|
+
|
101
|
+
def paginate?
|
102
|
+
options[:per_page]
|
103
|
+
end
|
104
|
+
|
105
|
+
def page_param_name
|
106
|
+
"#{name}[page]"
|
107
|
+
end
|
108
|
+
|
109
|
+
# returns representation of arbitrarily nested params as list of pairs [field_name, value], that
|
110
|
+
# can be used in forms to recreate the params
|
111
|
+
def self.flatten_params(params, level=0)
|
112
|
+
out = []
|
113
|
+
|
114
|
+
case params
|
115
|
+
when Array
|
116
|
+
params.each do |v|
|
117
|
+
out += flatten_params(v, level+1).collect {|kk, vv| ['[]' + kk, vv]}
|
118
|
+
end
|
119
|
+
when Hash
|
120
|
+
params.each do |k, v|
|
121
|
+
k = level > 0 ? "[#{k}]" : k.to_s
|
122
|
+
out += flatten_params(v, level+1).collect {|kk, vv| [k + kk, vv]}
|
123
|
+
end
|
124
|
+
else
|
125
|
+
out << ['', params]
|
126
|
+
end
|
127
|
+
|
128
|
+
out
|
129
|
+
end
|
130
|
+
|
131
|
+
def flatten_params(*args)
|
132
|
+
self.class.flatten_params(*args)
|
133
|
+
end
|
134
|
+
|
135
|
+
# returns current sorting as [+attr+, +dir+] (both symbols) or nil if not set
|
136
|
+
def sort
|
137
|
+
s = params[:sort].presence || options.dig(:default_sort)
|
138
|
+
|
139
|
+
if s.present?
|
140
|
+
attr, dir = s.split(':', 2)
|
141
|
+
|
142
|
+
if %w(asc desc).include?(dir)
|
143
|
+
return attr.to_sym, dir.to_sym
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
# return current value of filter +filter_name+
|
151
|
+
def filter_value(filter_name)
|
152
|
+
if params[:filter].respond_to?(:dig)
|
153
|
+
params[:filter].dig(filter_name)
|
154
|
+
elsif options[:default_filter]
|
155
|
+
options[:default_filter][filter_name]
|
156
|
+
else
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'will_paginate'
|
2
|
+
require 'will_paginate/view_helpers/action_view'
|
3
|
+
|
4
|
+
module Listpress
|
5
|
+
class MaterializePaginateRenderer < WillPaginate::ActionView::LinkRenderer
|
6
|
+
def html_container(html)
|
7
|
+
tag(:ul, html, class: "pagination")
|
8
|
+
end
|
9
|
+
|
10
|
+
def page_number(page)
|
11
|
+
if page == current_page
|
12
|
+
"<li class=\"#{Config.color} active\">" + link(page, page, rel: rel_value(page), "data-page": page) + "</li>"
|
13
|
+
else
|
14
|
+
"<li class=\"waves-effect\">" + link(page, page, rel: rel_value(page), "data-page": page) + "</li>"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def previous_page
|
19
|
+
num = @collection.current_page > 1 && @collection.current_page - 1
|
20
|
+
previous_or_next_page(num, "<i class=\"material-icons\">chevron_left</i>")
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_page
|
24
|
+
num = @collection.current_page < total_pages && @collection.current_page + 1
|
25
|
+
previous_or_next_page(num, "<i class=\"material-icons\">chevron_right</i>")
|
26
|
+
end
|
27
|
+
|
28
|
+
def previous_or_next_page(page, text)
|
29
|
+
if page
|
30
|
+
"<li class=\"waves-effect\">" + link(text, page, "data-page": page) + "</li>"
|
31
|
+
else
|
32
|
+
"<li class=\"disabled\"><a>" + text + "</a></li>"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/listpress.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "listpress/version"
|
2
|
+
require "listpress/materialize_paginate_renderer"
|
3
|
+
require "listpress/bootstrap_paginate_renderer"
|
4
|
+
|
5
|
+
require "listpress/config"
|
6
|
+
require "listpress/engine"
|
7
|
+
require "listpress/default_resolver"
|
8
|
+
require "listpress/listing"
|
9
|
+
require "listpress/helper"
|
10
|
+
|
11
|
+
module Listpress
|
12
|
+
|
13
|
+
end
|
data/listpress.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "listpress/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "listpress"
|
8
|
+
spec.version = Listpress::VERSION
|
9
|
+
spec.authors = ["Petr Sedivy"]
|
10
|
+
spec.email = ["petr@timepress.cz"]
|
11
|
+
|
12
|
+
spec.summary = %q{Listing helper for Timepress projects}
|
13
|
+
spec.homepage = "https://git.timepress.cz/timepress/listpress"
|
14
|
+
|
15
|
+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
|
16
|
+
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
17
|
+
if spec.respond_to?(:metadata)
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = "https://git.timepress.cz/timepress/listpress"
|
20
|
+
else
|
21
|
+
raise "RubyGems 2.0 or newer is required to protect against " \
|
22
|
+
"public gem pushes."
|
23
|
+
end
|
24
|
+
|
25
|
+
# Specify which files should be added to the gem when it is released.
|
26
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
27
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
28
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
29
|
+
end
|
30
|
+
spec.bindir = "exe"
|
31
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
32
|
+
spec.require_paths = ["lib"]
|
33
|
+
|
34
|
+
spec.add_development_dependency "bundler", "~> 1.17"
|
35
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
36
|
+
spec.add_runtime_dependency "will_paginate", "~> 3.1"
|
37
|
+
end
|
metadata
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: listpress
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.11
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Petr Sedivy
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-11-08 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.17'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.17'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: will_paginate
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.1'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.1'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- petr@timepress.cz
|
58
|
+
executables: []
|
59
|
+
extensions: []
|
60
|
+
extra_rdoc_files: []
|
61
|
+
files:
|
62
|
+
- ".gitignore"
|
63
|
+
- Gemfile
|
64
|
+
- README.md
|
65
|
+
- Rakefile
|
66
|
+
- app/assets/javascript/listpress.js
|
67
|
+
- app/assets/stylesheets/listpress.scss
|
68
|
+
- app/views/shared/_listing.html.erb
|
69
|
+
- app/views/shared/_listing_filters.html.erb
|
70
|
+
- app/views/shared/_listing_table.html.erb
|
71
|
+
- bin/console
|
72
|
+
- bin/setup
|
73
|
+
- lib/listpress.rb
|
74
|
+
- lib/listpress/bootstrap_paginate_renderer.rb
|
75
|
+
- lib/listpress/config.rb
|
76
|
+
- lib/listpress/default_resolver.rb
|
77
|
+
- lib/listpress/engine.rb
|
78
|
+
- lib/listpress/helper.rb
|
79
|
+
- lib/listpress/listing.rb
|
80
|
+
- lib/listpress/locale/cs.yml
|
81
|
+
- lib/listpress/locale/en.yml
|
82
|
+
- lib/listpress/materialize_paginate_renderer.rb
|
83
|
+
- lib/listpress/version.rb
|
84
|
+
- listpress.gemspec
|
85
|
+
homepage: https://git.timepress.cz/timepress/listpress
|
86
|
+
licenses: []
|
87
|
+
metadata:
|
88
|
+
homepage_uri: https://git.timepress.cz/timepress/listpress
|
89
|
+
source_code_uri: https://git.timepress.cz/timepress/listpress
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubygems_version: 3.0.3
|
106
|
+
signing_key:
|
107
|
+
specification_version: 4
|
108
|
+
summary: Listing helper for Timepress projects
|
109
|
+
test_files: []
|