pagy 43.1.0 → 43.1.2
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 +4 -4
- data/apps/calendar.ru +1 -1
- data/apps/demo.ru +3 -3
- data/apps/keynav+root_key.ru +321 -0
- data/apps/keynav.ru +3 -1
- data/apps/keyset.ru +1 -1
- data/apps/keyset_sequel.ru +1 -1
- data/apps/rails.ru +1 -1
- data/apps/repro.ru +4 -4
- data/bin/pagy +1 -1
- data/config/pagy.rb +1 -1
- data/javascripts/pagy.js +11 -10
- data/javascripts/pagy.js.map +3 -3
- data/javascripts/pagy.min.js +1 -1
- data/javascripts/pagy.mjs +10 -9
- data/lib/pagy/classes/keyset/keynav.rb +1 -1
- data/lib/pagy/classes/offset/countless.rb +5 -4
- data/lib/pagy/classes/request.rb +12 -10
- data/lib/pagy/modules/abilities/linkable.rb +8 -11
- data/lib/pagy/modules/searcher.rb +2 -2
- data/lib/pagy/toolbox/helpers/support/a_lambda.rb +1 -1
- data/lib/pagy/toolbox/paginators/countless.rb +2 -2
- data/lib/pagy/toolbox/paginators/keynav_js.rb +3 -3
- data/lib/pagy/toolbox/paginators/keyset.rb +2 -2
- data/lib/pagy/toolbox/paginators/method.rb +9 -8
- data/lib/pagy/toolbox/paginators/offset.rb +2 -2
- data/lib/pagy.rb +6 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3856b77e08f60c0524723eb9bfa2573ff45eb3a57f6811cff02a47474dfd55f5
|
|
4
|
+
data.tar.gz: 6d3633c3c54bae725508c851aae01dae0082f2f94428dda03e1ecf05a6d77e1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c040dfe0c8696a51ac5349442249e4a382def9be2dd73e4067cc9abecdee2ab3655c395c6c15a2e554c33fc1eea7ad2be9c9a3856d278705a8d01ba8839d56d6
|
|
7
|
+
data.tar.gz: 13d307256113cebbae7b25544fdd4b069194350d7e8810b405faffdccad39076d06a699deb3a1fe2a849afbc212762c1d02d377441d599fb1b9caff9c2c3bd48
|
data/apps/calendar.ru
CHANGED
data/apps/demo.ru
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# URL
|
|
20
20
|
# http://127.0.0.1:8000
|
|
21
21
|
|
|
22
|
-
VERSION = '43.1.
|
|
22
|
+
VERSION = '43.1.2'
|
|
23
23
|
|
|
24
24
|
if VERSION != Pagy::VERSION
|
|
25
25
|
Warning.warn("\n>>> WARNING! '#{File.basename(__FILE__)}-#{VERSION}' running with 'pagy-#{Pagy::VERSION}'! <<< \n\n")
|
|
@@ -476,12 +476,12 @@ class MockCollection < Array
|
|
|
476
476
|
end
|
|
477
477
|
|
|
478
478
|
def offset(value)
|
|
479
|
-
@collection = self[value..]
|
|
479
|
+
@collection = self[value..] || []
|
|
480
480
|
self
|
|
481
481
|
end
|
|
482
482
|
|
|
483
483
|
def limit(value)
|
|
484
|
-
@collection[0, value]
|
|
484
|
+
@collection.empty? ? [] : @collection[0, value]
|
|
485
485
|
end
|
|
486
486
|
|
|
487
487
|
def count(*)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# DESCRIPTION
|
|
4
|
+
# Showcase the Keynav pagination with independent instances
|
|
5
|
+
#
|
|
6
|
+
# DOC
|
|
7
|
+
# https://ddnexus.github.io/pagy/playground/#keyset-apps
|
|
8
|
+
#
|
|
9
|
+
# BIN HELP
|
|
10
|
+
# pagy -h
|
|
11
|
+
#
|
|
12
|
+
# DEV USAGE
|
|
13
|
+
# pagy clone keynav
|
|
14
|
+
# pagy ./keynav.ru
|
|
15
|
+
#
|
|
16
|
+
# URL
|
|
17
|
+
# http://127.0.0.1:8000
|
|
18
|
+
|
|
19
|
+
VERSION = '43.1.2'
|
|
20
|
+
|
|
21
|
+
if VERSION != Pagy::VERSION
|
|
22
|
+
Warning.warn("\n>>> WARNING! '#{File.basename(__FILE__)}-#{VERSION}' running with 'pagy-#{Pagy::VERSION}'! <<< \n\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Bundle
|
|
26
|
+
require 'bundler/inline'
|
|
27
|
+
gemfile(!Pagy::ROOT.join('pagy.gemspec').exist?) do
|
|
28
|
+
source 'https://rubygems.org'
|
|
29
|
+
gem 'activerecord'
|
|
30
|
+
gem 'puma'
|
|
31
|
+
gem 'sinatra'
|
|
32
|
+
gem 'sqlite3'
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Sinatra setup
|
|
36
|
+
require 'sinatra/base'
|
|
37
|
+
# Sinatra application
|
|
38
|
+
class PagyKeynav < Sinatra::Base
|
|
39
|
+
include Pagy::Method
|
|
40
|
+
|
|
41
|
+
get('/javascripts/:file') do
|
|
42
|
+
format = params[:file].split('.').last
|
|
43
|
+
if format == 'js'
|
|
44
|
+
content_type 'application/javascript'
|
|
45
|
+
elsif format == 'map'
|
|
46
|
+
content_type 'application/json'
|
|
47
|
+
end
|
|
48
|
+
send_file Pagy::ROOT.join('javascripts', params[:file])
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Root route/action
|
|
52
|
+
get '/' do
|
|
53
|
+
Time.zone = 'UTC'
|
|
54
|
+
|
|
55
|
+
@order = { animal: :asc, name: :asc, birthdate: :desc, id: :asc }
|
|
56
|
+
@pagy1, @pets1 = pagy(:keynav_js, Pet.order(@order), limit: 4, root_key: 'animal1')
|
|
57
|
+
@ids1 = @pets1.pluck(:id)
|
|
58
|
+
@pagy2, @pets2 = pagy(:keynav_js, Pet.order(@order), limit: 4, root_key: 'animal2')
|
|
59
|
+
@ids2 = @pets2.pluck(:id)
|
|
60
|
+
erb :main
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
helpers do
|
|
64
|
+
def order_symbol(dir)
|
|
65
|
+
{ asc: '↗', desc: '↘' }[dir]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Views
|
|
70
|
+
template :layout do
|
|
71
|
+
<<~ERB
|
|
72
|
+
<!DOCTYPE html>
|
|
73
|
+
<html lang="en">
|
|
74
|
+
<html>
|
|
75
|
+
<head>
|
|
76
|
+
<title>Pagy Keynav (root_key) App</title>
|
|
77
|
+
<script src="javascripts/pagy.js"></script>
|
|
78
|
+
<script>
|
|
79
|
+
window.addEventListener("load", Pagy.init);
|
|
80
|
+
</script>
|
|
81
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
82
|
+
<style type="text/css">
|
|
83
|
+
@media screen { html, body {
|
|
84
|
+
font-size: 1rem;
|
|
85
|
+
line-height: 1.2s;
|
|
86
|
+
padding: 0;
|
|
87
|
+
margin: 0;
|
|
88
|
+
} }
|
|
89
|
+
body {
|
|
90
|
+
background: white !important;
|
|
91
|
+
margin: 0 !important;
|
|
92
|
+
font-family: sans-serif !important;
|
|
93
|
+
}
|
|
94
|
+
.main-content {
|
|
95
|
+
padding: 1rem 1.5rem 2rem !important;
|
|
96
|
+
}
|
|
97
|
+
#content {
|
|
98
|
+
display: flex; /* Enables Flexbox */
|
|
99
|
+
flex-wrap: wrap; /* Allows stacking */
|
|
100
|
+
gap: 20px; /* Gap between items */
|
|
101
|
+
/* Just for visual clarity of the container boundaries */
|
|
102
|
+
/*padding: 10px;
|
|
103
|
+
background-color: #e0e7ff;
|
|
104
|
+
border-radius: 8px;
|
|
105
|
+
border: 2px dashed #6366f1; */
|
|
106
|
+
}
|
|
107
|
+
.box {
|
|
108
|
+
flex: 1; /* Grow to fill space */
|
|
109
|
+
min-width: 300px; /* Stack if space < 300px */
|
|
110
|
+
/* Visual Styling */
|
|
111
|
+
background-color: white;
|
|
112
|
+
padding: 25px;
|
|
113
|
+
border-radius: 6px;
|
|
114
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
115
|
+
border: 1px solid #ddd;
|
|
116
|
+
}
|
|
117
|
+
.pagy {
|
|
118
|
+
padding: .5em;
|
|
119
|
+
margin: .3em 0;
|
|
120
|
+
width: fit-content;
|
|
121
|
+
box-shadow: 5px 5px 10px 0px rgba(0,0,0,0.2);
|
|
122
|
+
}
|
|
123
|
+
<%= Pagy::ROOT.join('stylesheets/pagy.css').read %>
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<%= yield %>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
130
|
+
ERB
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
template :main do
|
|
134
|
+
<<~ERB
|
|
135
|
+
<div class="main-content">
|
|
136
|
+
<h1>Pagy Keynav App</h1>
|
|
137
|
+
<p>Self-contained, standalone app usable to easily reproduce any Keynav related pagy issue
|
|
138
|
+
with ActiveRecord sets.</p>
|
|
139
|
+
<p>The panels below show how to use the <code>:root_key</code> option for independent instances in the same request.</p>
|
|
140
|
+
|
|
141
|
+
<p>Notice that Keynav works also with Sequel sets.</p>
|
|
142
|
+
|
|
143
|
+
<h2>Versions</h2>
|
|
144
|
+
<ul>
|
|
145
|
+
<li>Ruby: <%= RUBY_VERSION %></li>
|
|
146
|
+
<li>Rack: <%= Rack::RELEASE %></li>
|
|
147
|
+
<li>Sinatra: <%= Sinatra::VERSION %></li>
|
|
148
|
+
<li>Pagy: <%= Pagy::VERSION %></li>
|
|
149
|
+
</ul>
|
|
150
|
+
|
|
151
|
+
<div id="content">
|
|
152
|
+
<div class="box">
|
|
153
|
+
<h3>Collection 1</h3>
|
|
154
|
+
<p id="records">@ids: <%= @ids1.join(',') %></p>
|
|
155
|
+
<div class="collection">
|
|
156
|
+
<table border="1" style="border-collapse: collapse; border-spacing: 0; padding: 0.2rem;">
|
|
157
|
+
<tr>
|
|
158
|
+
<th scope="col">animal <%= order_symbol(@order[:animal]) %></th>
|
|
159
|
+
<th scope="col">name <%= order_symbol(@order[:name]) %></th>
|
|
160
|
+
<th scope="col">birthdate <%= order_symbol(@order[:birthdate]) %></th>
|
|
161
|
+
<th scope="col">id <%= order_symbol(@order[:id]) %></th>
|
|
162
|
+
</tr>
|
|
163
|
+
<% @pets1.each do |pet| %>
|
|
164
|
+
<tr>
|
|
165
|
+
<td><%= pet.animal %></td>
|
|
166
|
+
<td><%= pet.name %></td>
|
|
167
|
+
<td><%= pet.birthdate %></td>
|
|
168
|
+
<td><%= pet.id %></td>
|
|
169
|
+
</tr>
|
|
170
|
+
<% end %>
|
|
171
|
+
</table>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<h4>@pagy.series_nav</h4>
|
|
175
|
+
<%= @pagy1.series_nav(id: 'series-nav',
|
|
176
|
+
aria_label: 'Pages (nav)') %>
|
|
177
|
+
|
|
178
|
+
<h4>@pagy.series_nav_js (responsive)</h4>
|
|
179
|
+
<%= @pagy1.series_nav_js(id: 'series-nav-js-responsive',
|
|
180
|
+
aria_label: 'Pages (nav_js_responsive)',
|
|
181
|
+
steps: { 0 => 5, 500 => 7, 750 => 9, 1000 => 11 }) %>
|
|
182
|
+
<h4>@pagy.input_nav_js</h4>
|
|
183
|
+
<%= @pagy1.input_nav_js(id: 'input-nav-js',
|
|
184
|
+
aria_label: 'Pages (input_nav_js)') %>
|
|
185
|
+
|
|
186
|
+
<h4>@pagy.info_tag</h4>
|
|
187
|
+
<%= @pagy1.info_tag(id: 'pagy-info') %>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="box">
|
|
191
|
+
<h3>Collection 2</h3>
|
|
192
|
+
<p id="records">@ids: <%= @ids2.join(',') %></p>
|
|
193
|
+
<div class="collection">
|
|
194
|
+
<table border="1" style="border-collapse: collapse; border-spacing: 0; padding: 0.2rem;">
|
|
195
|
+
<tr>
|
|
196
|
+
<th scope="col">animal <%= order_symbol(@order[:animal]) %></th>
|
|
197
|
+
<th scope="col">name <%= order_symbol(@order[:name]) %></th>
|
|
198
|
+
<th scope="col">birthdate <%= order_symbol(@order[:birthdate]) %></th>
|
|
199
|
+
<th scope="col">id <%= order_symbol(@order[:id]) %></th>
|
|
200
|
+
</tr>
|
|
201
|
+
<% @pets2.each do |pet| %>
|
|
202
|
+
<tr>
|
|
203
|
+
<td><%= pet.animal %></td>
|
|
204
|
+
<td><%= pet.name %></td>
|
|
205
|
+
<td><%= pet.birthdate %></td>
|
|
206
|
+
<td><%= pet.id %></td>
|
|
207
|
+
</tr>
|
|
208
|
+
<% end %>
|
|
209
|
+
</table>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<h4>@pagy.series_nav</h4>
|
|
213
|
+
<%= @pagy2.series_nav(id: 'series-nav',
|
|
214
|
+
aria_label: 'Pages (nav)') %>
|
|
215
|
+
|
|
216
|
+
<h4>@pagy.series_nav_js (responsive)</h4>
|
|
217
|
+
<%= @pagy2.series_nav_js(id: 'series-nav-js-responsive',
|
|
218
|
+
aria_label: 'Pages (nav_js_responsive)',
|
|
219
|
+
steps: { 0 => 5, 500 => 7, 750 => 9, 1000 => 11 }) %>
|
|
220
|
+
<h4>@pagy.input_nav_js</h4>
|
|
221
|
+
<%= @pagy2.input_nav_js(id: 'input-nav-js',
|
|
222
|
+
aria_label: 'Pages (input_nav_js)') %>
|
|
223
|
+
|
|
224
|
+
<h4>@pagy.info_tag</h4>
|
|
225
|
+
<%= @pagy2.info_tag(id: 'pagy-info') %>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
ERB
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# ActiveRecord setup
|
|
234
|
+
require 'active_record'
|
|
235
|
+
|
|
236
|
+
# Match the microsecods with the strings stored into the time columns of SQLite
|
|
237
|
+
# ActiveSupport::JSON::Encoding.time_precision = 6
|
|
238
|
+
|
|
239
|
+
# Log
|
|
240
|
+
output = ENV['APP_ENV'].equal?('showcase') ? IO::NULL : $stdout
|
|
241
|
+
ActiveRecord::Base.logger = Logger.new(output)
|
|
242
|
+
# SQLite DB files
|
|
243
|
+
dir = ENV['APP_ENV'].equal?('development') ? '.' : Dir.pwd # app dir in dev or pwd otherwise
|
|
244
|
+
abort "ERROR: Cannot create DB files: the directory #{dir.inspect} is not writable." \
|
|
245
|
+
unless File.writable?(dir)
|
|
246
|
+
# Connection
|
|
247
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: "#{dir}/tmp/pagy-keyset-ar.sqlite3")
|
|
248
|
+
# Schema
|
|
249
|
+
ActiveRecord::Schema.define do
|
|
250
|
+
create_table :pets, force: true do |t|
|
|
251
|
+
t.string :animal
|
|
252
|
+
t.string :name
|
|
253
|
+
t.date :birthdate
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Models
|
|
258
|
+
class Pet < ActiveRecord::Base; end
|
|
259
|
+
|
|
260
|
+
data = <<~DATA
|
|
261
|
+
Luna | dog | 2018-03-10
|
|
262
|
+
Coco | cat | 2019-05-15
|
|
263
|
+
Dodo | dog | 2020-06-25
|
|
264
|
+
Wiki | bird | 2018-03-12
|
|
265
|
+
Baby | rabbit | 2020-01-13
|
|
266
|
+
Neki | horse | 2021-07-20
|
|
267
|
+
Tino | donkey | 2019-06-18
|
|
268
|
+
Plot | cat | 2022-09-21
|
|
269
|
+
Riki | cat | 2018-09-14
|
|
270
|
+
Susi | horse | 2018-10-26
|
|
271
|
+
Coco | pig | 2020-08-29
|
|
272
|
+
Momo | bird | 2023-08-25
|
|
273
|
+
Lili | cat | 2021-07-22
|
|
274
|
+
Beli | pig | 2020-07-26
|
|
275
|
+
Rocky | bird | 2022-08-19
|
|
276
|
+
Vyvy | dog | 2018-05-16
|
|
277
|
+
Susi | horse | 2024-01-25
|
|
278
|
+
Ella | cat | 2020-02-20
|
|
279
|
+
Rocky | dog | 2019-09-19
|
|
280
|
+
Juni | rabbit | 2020-08-24
|
|
281
|
+
Coco | bird | 2021-03-17
|
|
282
|
+
Susi | dog | 2021-07-28
|
|
283
|
+
Luna | horse | 2023-05-14
|
|
284
|
+
Gigi | pig | 2022-05-19
|
|
285
|
+
Coco | cat | 2020-02-20
|
|
286
|
+
Nino | donkey | 2019-06-17
|
|
287
|
+
Luna | cat | 2022-02-09
|
|
288
|
+
Popi | dog | 2020-09-26
|
|
289
|
+
Lili | pig | 2022-06-18
|
|
290
|
+
Mina | horse | 2021-04-21
|
|
291
|
+
Susi | rabbit | 2023-05-18
|
|
292
|
+
Toni | donkey | 2018-06-22
|
|
293
|
+
Rocky | horse | 2019-09-28
|
|
294
|
+
Lili | cat | 2019-03-18
|
|
295
|
+
Roby | cat | 2022-06-19
|
|
296
|
+
Anto | horse | 2022-08-18
|
|
297
|
+
Susi | pig | 2021-04-21
|
|
298
|
+
Boly | bird | 2020-03-29
|
|
299
|
+
Sky | cat | 2023-07-19
|
|
300
|
+
Lili | dog | 2020-01-28
|
|
301
|
+
Fami | snake | 2023-04-27
|
|
302
|
+
Lopi | pig | 2019-06-19
|
|
303
|
+
Rocky | snake | 2022-03-13
|
|
304
|
+
Denis | dog | 2022-06-19
|
|
305
|
+
Maca | cat | 2022-06-19
|
|
306
|
+
Luna | dog | 2022-08-15
|
|
307
|
+
Jeme | horse | 2019-08-08
|
|
308
|
+
Sary | bird | 2023-04-29
|
|
309
|
+
Rocky | bird | 2023-05-14
|
|
310
|
+
Coco | dog | 2023-05-27
|
|
311
|
+
DATA
|
|
312
|
+
|
|
313
|
+
# DB seed
|
|
314
|
+
pets = []
|
|
315
|
+
data.each_line(chomp: true) do |pet|
|
|
316
|
+
name, animal, birthdate = pet.split('|').map(&:strip)
|
|
317
|
+
pets << { name:, animal:, birthdate: }
|
|
318
|
+
end
|
|
319
|
+
Pet.insert_all(pets)
|
|
320
|
+
|
|
321
|
+
run PagyKeynav
|
data/apps/keynav.ru
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# URL
|
|
17
17
|
# http://127.0.0.1:8000
|
|
18
18
|
|
|
19
|
-
VERSION = '43.1.
|
|
19
|
+
VERSION = '43.1.2'
|
|
20
20
|
|
|
21
21
|
if VERSION != Pagy::VERSION
|
|
22
22
|
Warning.warn("\n>>> WARNING! '#{File.basename(__FILE__)}-#{VERSION}' running with 'pagy-#{Pagy::VERSION}'! <<< \n\n")
|
|
@@ -54,6 +54,8 @@ class PagyKeynav < Sinatra::Base
|
|
|
54
54
|
|
|
55
55
|
@order = { animal: :asc, name: :asc, birthdate: :desc, id: :asc }
|
|
56
56
|
@pagy, @pets = pagy(:keynav_js, Pet.order(@order), limit: 4, client_max_limit: 100)
|
|
57
|
+
# Support also root_key for replacing url in javascript
|
|
58
|
+
# @pagy, @pets = pagy(:keynav_js, Pet.order(@order), limit: 4, client_max_limit: 100, root_key: 'animal')
|
|
57
59
|
@ids = @pets.pluck(:id)
|
|
58
60
|
erb :main
|
|
59
61
|
end
|
data/apps/keyset.ru
CHANGED
data/apps/keyset_sequel.ru
CHANGED
data/apps/rails.ru
CHANGED
data/apps/repro.ru
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# URL
|
|
17
17
|
# http://127.0.0.1:8000
|
|
18
18
|
|
|
19
|
-
VERSION = '43.1.
|
|
19
|
+
VERSION = '43.1.2'
|
|
20
20
|
|
|
21
21
|
if VERSION != Pagy::VERSION
|
|
22
22
|
Warning.warn("\n>>> WARNING! '#{File.basename(__FILE__)}-#{VERSION}' running with 'pagy-#{Pagy::VERSION}'! <<< \n\n")
|
|
@@ -160,7 +160,7 @@ class PagyRepro < Sinatra::Base
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
# Simple array-based collection that acts as a standard DB collection.
|
|
163
|
-
# Use it as a simple way to get a collection that acts as
|
|
163
|
+
# Use it as a simple way to get a collection that acts as an AR scope, but without any DB
|
|
164
164
|
# or create an ActiveRecord class or anything else that you need instead
|
|
165
165
|
class MockCollection < Array
|
|
166
166
|
def initialize(arr = Array(1..1000))
|
|
@@ -169,12 +169,12 @@ class MockCollection < Array
|
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
def offset(value)
|
|
172
|
-
@collection = self[value..]
|
|
172
|
+
@collection = self[value..] || []
|
|
173
173
|
self
|
|
174
174
|
end
|
|
175
175
|
|
|
176
176
|
def limit(value)
|
|
177
|
-
@collection[0, value]
|
|
177
|
+
@collection.empty? ? [] : @collection[0, value]
|
|
178
178
|
end
|
|
179
179
|
|
|
180
180
|
def count(*)
|
data/bin/pagy
CHANGED
data/config/pagy.rb
CHANGED
data/javascripts/pagy.js
CHANGED
|
@@ -24,18 +24,18 @@ window.Pagy = (() => {
|
|
|
24
24
|
}));
|
|
25
25
|
const B64SafeEncode = (unicode) => btoa(String.fromCharCode(...new TextEncoder().encode(unicode))).replace(/[+/=]/g, (m) => m == "+" ? "-" : m == "/" ? "_" : ""), B64Decode = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
|
|
26
26
|
const randKey = () => Math.floor(Math.random() * 36 ** 3).toString(36);
|
|
27
|
-
const augmentKeynav = async (nav, [storageKey, pageKey, last, spliceArgs]) => {
|
|
28
|
-
let
|
|
27
|
+
const augmentKeynav = async (nav, [storageKey, rootKey, pageKey, last, spliceArgs]) => {
|
|
28
|
+
let augmentPage;
|
|
29
29
|
const browserKey = document.cookie.split(/;\s+/).find((row) => row.startsWith(pagy + "="))?.split("=")[1] ?? randKey();
|
|
30
30
|
document.cookie = pagy + "=" + browserKey;
|
|
31
31
|
if (storageKey && !(storageKey in storage)) {
|
|
32
32
|
sync.postMessage({ from: tabId, key: storageKey });
|
|
33
33
|
await new Promise((resolve) => setTimeout(() => resolve(""), 100));
|
|
34
34
|
if (!(storageKey in storage)) {
|
|
35
|
-
|
|
35
|
+
augmentPage = (page) => page + "+" + last;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
if (!
|
|
38
|
+
if (!augmentPage) {
|
|
39
39
|
if (!storageKey) {
|
|
40
40
|
do {
|
|
41
41
|
storageKey = randKey();
|
|
@@ -46,7 +46,7 @@ window.Pagy = (() => {
|
|
|
46
46
|
cutoffs.splice(...spliceArgs);
|
|
47
47
|
storage.setItem(storageKey, JSON.stringify(cutoffs));
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
augmentPage = (page) => {
|
|
50
50
|
const pageNum = parseInt(page);
|
|
51
51
|
return B64SafeEncode(JSON.stringify([
|
|
52
52
|
browserKey,
|
|
@@ -58,11 +58,12 @@ window.Pagy = (() => {
|
|
|
58
58
|
]));
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
const search = rootKey ? `${rootKey}%5B${pageKey}%5D` : pageKey;
|
|
62
|
+
const re = new RegExp(`(?<=\\?.*)(\\b${search}=)(\\d+)`);
|
|
61
63
|
for (const a of nav.querySelectorAll("a[href]")) {
|
|
62
|
-
|
|
63
|
-
a.href = url.replace(re, pageKey + "=" + augment(url.match(re)[1]));
|
|
64
|
+
a.href = a.href.replace(re, (_match, prefix, digit) => `${prefix}${augmentPage(digit)}`);
|
|
64
65
|
}
|
|
65
|
-
return
|
|
66
|
+
return augmentPage;
|
|
66
67
|
};
|
|
67
68
|
const buildNavJs = (nav, [
|
|
68
69
|
[before, anchor, current, gap, after],
|
|
@@ -124,7 +125,7 @@ window.Pagy = (() => {
|
|
|
124
125
|
});
|
|
125
126
|
};
|
|
126
127
|
return {
|
|
127
|
-
version: "43.1.
|
|
128
|
+
version: "43.1.2",
|
|
128
129
|
init(arg) {
|
|
129
130
|
const target = arg instanceof HTMLElement ? arg : document, elements = target.querySelectorAll("[data-pagy]");
|
|
130
131
|
for (const element of elements) {
|
|
@@ -147,5 +148,5 @@ window.Pagy = (() => {
|
|
|
147
148
|
};
|
|
148
149
|
})();
|
|
149
150
|
|
|
150
|
-
//# debugId=
|
|
151
|
+
//# debugId=43829892A3C537E264756E2164756E21
|
|
151
152
|
//# sourceMappingURL=pagy.js.map
|
data/javascripts/pagy.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/pagy.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"interface SyncData {\n from?: number\n to?: number\n key: string\n str?: string\n}\ntype InitArgs = [\"k\", KeynavArgs] | // series_nav[_js] with keynav instance\n [\"snj\", SeriesNavJsArgs] | // series_nav_js\n [\"inj\", InputNavJsArgs] | // input_nav_js\n [\"ltj\", LimitTagJsArgs] // limit_tag_js\ntype AugmentKeynav = (nav:HTMLElement, keynavArgs:KeynavArgs) => Promise<((page: string) => string)>\ntype KeynavArgs = readonly [storageKey: string | null,\n pageKey: string,\n last: number,\n spliceArgs?: SpliceArgs]\ntype SpliceArgs = readonly [start: number,\n deleteCount: number, // it would be optional, but ts complains\n ...items: Cutoff[]]\ntype Cutoff = readonly (string | number | boolean)[]\ntype AugmentedPage = [browserId: string,\n storageKey: string,\n pageNumber: number,\n pages: number,\n priorCutoff: Cutoff | null,\n pageCutoff: Cutoff | null]\ntype SeriesNavJsArgs = readonly [NavJsTokens, NavJsSeries, KeynavArgs?]\ntype NavJsSeries = readonly [widths: number[],\n series: (string | number)[][],\n labels: string[][] | null]\ntype InputNavJsArgs = readonly [urlToken: string, KeynavArgs?]\ntype LimitTagJsArgs = readonly [from: number,\n urlToken: string]\ntype NavJsTokens = readonly [before: string,\n anchor: string,\n current: string,\n gap: string,\n after: string]\ninterface NavJsElement extends HTMLElement {\n render(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst Pagy = (() => {\n const storageSupport = 'sessionStorage' in window && 'BroadcastChannel' in window,\n pageRe = \"P \"; // shorten the compiled size\n // eslint-disable-next-line prefer-const\n let pagy = \"pagy\", storage: Storage, sync: BroadcastChannel, tabId: number;\n if (storageSupport) {\n storage = sessionStorage; // shorten the compiled size\n sync = new BroadcastChannel(pagy);\n tabId = Date.now();\n // Sync the sessionStorage keys for the cutoffs opened in a new tab/window\n sync.addEventListener(\"message\", (e:MessageEvent<SyncData>) => {\n if (e.data.from) { // request cutoffs\n const cutoffs = storage.getItem(e.data.key);\n if (cutoffs) {\n sync.postMessage(<SyncData>{to: e.data.from, key: e.data.key, str: cutoffs});\n } // send response\n } else if (e.data.to) { // receive cutoffs\n if (e.data.to == tabId) {\n storage.setItem(e.data.key, <string>e.data.str);\n }\n }\n });\n }\n // The observer instance for responsive navs\n const rjsObserver = new ResizeObserver(\n entries => entries.forEach(e => {\n e.target.querySelectorAll<NavJsElement>(\".pagy-rjs\").forEach(el => el.render());\n }));\n\n /* Full set of B64 functions\n const B64Encode = (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode))),\n B64Safe = (unsafe:string) => unsafe.replace(/[+/=]/g, (m) => m == \"+\" ? \"-\" : m == \"/\" ? \"_\" : \"\"),\n B64SafeEncode = (unicode:string) => B64Safe(B64Encode(unicode)),\n B64Decode = (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0))),\n B64Unsafe = (safe:string) => safe.replace(/[-_]/g, (match) => match == \"-\" ? \"+\" : \"/\"),\n B64SafeDecode = (base64:string) => B64Decode(B64Unsafe(base64))\n */\n const B64SafeEncode = (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode)))\n .replace(/[+/=]/g, (m) => m == \"+\" ? \"-\" : m == \"/\" ? \"_\" : \"\"),\n B64Decode = (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n\n // Return a random key: 3 chars max, base-36 number < 36**3\n const randKey = () => Math.floor(Math.random() * 36 ** 3).toString(36);\n\n // Manage the page augmentation for Keynav, called only if storageSupport\n const augmentKeynav: AugmentKeynav = async (nav, [storageKey, pageKey, last, spliceArgs]) => {\n let augment;\n const browserKey = document.cookie.split(/;\\s+/) // it works even if malformed\n .find((row) => row.startsWith(pagy + \"=\"))\n ?.split(\"=\")[1] ?? randKey();\n document.cookie = pagy + \"=\" + browserKey; // Smaller .min size: set the cookie without checking\n if (storageKey && !(storageKey in storage)) {\n // Sync the sessiongStorage from other tabs/windows (e.g., open page in the new tab/window)\n sync.postMessage(<SyncData>{ from: tabId, key: storageKey });\n // Wait for the listener to copy the cutoffs in the current sessionStorage\n await new Promise<string|null>((resolve) => setTimeout(() => resolve(\"\"), 100));\n if (!(storageKey in storage)) { // the storageKey didn't get copied: fallback to countless pagination\n augment = (page: string) => page + '+' + last;\n }\n }\n if (!augment) { // regular keynav pagination\n if (!storageKey) { do { storageKey = randKey() } while (storageKey in storage) } // no dup keys\n const data = storage.getItem(storageKey),\n cutoffs = <Cutoff[]>(data ? JSON.parse(data) : [undefined]);\n if (spliceArgs) {\n cutoffs.splice(...spliceArgs);\n storage.setItem(storageKey, JSON.stringify(cutoffs));\n }\n // Augment function\n augment = (page:string) => {\n const pageNum = parseInt(page);\n return B64SafeEncode(JSON.stringify(\n <AugmentedPage>[browserKey,\n storageKey,\n pageNum,\n cutoffs.length, // pages/last\n cutoffs[pageNum - 1], // priorCutoff\n cutoffs[pageNum]])); // pageCutoff\n };\n }\n // Augment the page param of each href\n for (const a of <NodeListOf<HTMLAnchorElement>><unknown>nav.querySelectorAll('a[href]')) {\n const url = a.href,\n re = new RegExp(`(?<=\\\\?.*)\\\\b${pageKey}=(\\\\d+)`); // find the numeric page from pageKey\n a.href = url.replace(re, pageKey + \"=\" + augment(url.match(re)![1])); // eslint-disable-line @typescript-eslint/no-non-null-assertion\n }\n // Return the augment function for further augmentation (i.e., url token in input_nav_js)\n return augment;\n };\n\n // Build the series_nav_js helper\n const buildNavJs = (nav:NavJsElement, [[before, anchor, current, gap, after],\n [widths, series, labels], keynavArgs]:SeriesNavJsArgs) => {\n const parent = <HTMLElement>nav.parentElement;\n let lastWidth = -1;\n (nav.render = () => {\n const index = widths.findIndex(w => w < parent.clientWidth);\n if (widths[index] === lastWidth) { return } // no change: abort\n\n let html = before;\n series[index].forEach((item, i) => {\n // Avoid the if blocks and chain the results (shorter pagy.min.js and easier reading)\n html += item == \"gap\" ? gap :\n // @ts-expect-error the item may be a number, but the 'replace' converts it to string (shorter pagy.min.js)\n (typeof item == \"number\" ? anchor.replace(pageRe, item) : current)\n .replace(\"L<\", labels?.[index][i] ?? item + \"<\");\n });\n html += after;\n nav.innerHTML = \"\";\n nav.insertAdjacentHTML(\"afterbegin\", html);\n lastWidth = widths[index];\n if (keynavArgs && storageSupport) { void augmentKeynav(nav, keynavArgs) }\n })();\n if (nav.classList.contains(pagy + \"-rjs\")) { rjsObserver.observe(parent) }\n };\n\n // Init the input_nav_js helpers\n const initInputNavJs = async (nav:HTMLElement, [url_token, keynavArgs]:InputNavJsArgs) => {\n const augment = keynavArgs && storageSupport\n ? await augmentKeynav(nav, keynavArgs)\n : (page: string) => page;\n initInput(nav, inputValue => url_token.replace(pageRe, augment(inputValue)));\n };\n\n // Init the limit_tag_js helper\n const initLimitTagJs = (span:HTMLSpanElement, [from, url_token]:LimitTagJsArgs) => {\n initInput(span, inputValue => {\n // @ts-expect-error the page is a number, but the 'replace' converts it to string (shorter pagy.min.js)\n return url_token.replace(pageRe, Math.max(Math.ceil(from / parseInt(inputValue)), 1))\n .replace('L ', inputValue);\n });\n };\n\n // Init the input element\n const initInput = (element:HTMLElement, getUrl:(v:string) => string) => {\n const input = <HTMLInputElement>element.querySelector(\"input\"),\n link = <HTMLAnchorElement>element.querySelector(\"a\"),\n initial = input.value,\n action = () => {\n if (input.value === initial) { return } // not changed\n const [min, val, max] = [input.min, input.value, input.max].map(n => parseInt(n) || 0);\n if (val < min || val > max) { // reset invalid/out-of-range\n input.value = initial;\n input.select();\n return;\n }\n link.href = getUrl(input.value);\n link.click();\n };\n input.addEventListener(\"focus\", () => input.select());\n input.addEventListener(\"focusout\", action);\n input.addEventListener(\"keypress\", e => { if (e.key == \"Enter\") { action() } });\n };\n\n // Public interface\n return {\n version: \"43.1.0\",\n\n // Scan for elements with a \"data-pagy\" attribute and call their init functions with the decoded args\n init(arg?:HTMLElement) {\n const target = arg instanceof HTMLElement ? arg : document,\n elements = target.querySelectorAll(\"[data-pagy]\");\n for (const element of <NodeListOf<HTMLElement>>elements) {\n try {\n const [helperId, ...args] = <InitArgs>JSON.parse(B64Decode(<string>element.getAttribute(\"data-pagy\")));\n if (helperId == \"k\") {\n // @ts-expect-error spread 2 arguments, not 3 as it complains about\n void augmentKeynav(element, ...<KeynavArgs><unknown>args);\n } else if (helperId == \"snj\") {\n buildNavJs(<NavJsElement>element, <SeriesNavJsArgs><unknown>args);\n } else if (helperId == \"inj\") {\n void initInputNavJs(element, <InputNavJsArgs><unknown>args);\n } else if (helperId == \"ltj\") {\n initLimitTagJs(element, <LimitTagJsArgs><unknown>args);\n }\n // else { console.warn(\"Pagy.init: %o\\nUnknown helperId '%s'\", element, helperId) }\n } catch (err) { console.warn(\"Pagy.init: %o\\n%s\", element, err) }\n }\n }\n };\n})();\n"
|
|
5
|
+
"interface SyncData {\n from?: number\n to?: number\n key: string\n str?: string\n}\ntype InitArgs = [\"k\", KeynavArgs] | // series_nav[_js] with keynav instance\n [\"snj\", SeriesNavJsArgs] | // series_nav_js\n [\"inj\", InputNavJsArgs] | // input_nav_js\n [\"ltj\", LimitTagJsArgs] // limit_tag_js\ntype AugmentKeynav = (nav:HTMLElement, keynavArgs:KeynavArgs) => Promise<((page: string) => string)>\ntype KeynavArgs = readonly [storageKey: string | null,\n rootKey: string | null,\n pageKey: string,\n last: number,\n spliceArgs?: SpliceArgs]\ntype SpliceArgs = readonly [start: number,\n deleteCount: number, // it would be optional, but ts complains\n ...items: Cutoff[]]\ntype Cutoff = readonly (string | number | boolean)[]\ntype AugmentedPage = [browserId: string,\n storageKey: string,\n pageNumber: number,\n pages: number,\n priorCutoff: Cutoff | null,\n pageCutoff: Cutoff | null]\ntype SeriesNavJsArgs = readonly [NavJsTokens, NavJsSeries, KeynavArgs?]\ntype NavJsSeries = readonly [widths: number[],\n series: (string | number)[][],\n labels: string[][] | null]\ntype InputNavJsArgs = readonly [urlToken: string, KeynavArgs?]\ntype LimitTagJsArgs = readonly [from: number,\n urlToken: string]\ntype NavJsTokens = readonly [before: string,\n anchor: string,\n current: string,\n gap: string,\n after: string]\ninterface NavJsElement extends HTMLElement {\n render(): void\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst Pagy = (() => {\n const storageSupport = 'sessionStorage' in window && 'BroadcastChannel' in window,\n pageRe = \"P \"; // shorten the compiled size\n // eslint-disable-next-line prefer-const\n let pagy = \"pagy\", storage: Storage, sync: BroadcastChannel, tabId: number;\n if (storageSupport) {\n storage = sessionStorage; // shorten the compiled size\n sync = new BroadcastChannel(pagy);\n tabId = Date.now();\n // Sync the sessionStorage keys for the cutoffs opened in a new tab/window\n sync.addEventListener(\"message\", (e:MessageEvent<SyncData>) => {\n if (e.data.from) { // request cutoffs\n const cutoffs = storage.getItem(e.data.key);\n if (cutoffs) {\n sync.postMessage(<SyncData>{to: e.data.from, key: e.data.key, str: cutoffs});\n } // send response\n } else if (e.data.to) { // receive cutoffs\n if (e.data.to == tabId) {\n storage.setItem(e.data.key, <string>e.data.str);\n }\n }\n });\n }\n // The observer instance for responsive navs\n const rjsObserver = new ResizeObserver(\n entries => entries.forEach(e => {\n e.target.querySelectorAll<NavJsElement>(\".pagy-rjs\").forEach(el => el.render());\n }));\n\n /* Full set of B64 functions\n const B64Encode = (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode))),\n B64Safe = (unsafe:string) => unsafe.replace(/[+/=]/g, (m) => m == \"+\" ? \"-\" : m == \"/\" ? \"_\" : \"\"),\n B64SafeEncode = (unicode:string) => B64Safe(B64Encode(unicode)),\n B64Decode = (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0))),\n B64Unsafe = (safe:string) => safe.replace(/[-_]/g, (match) => match == \"-\" ? \"+\" : \"/\"),\n B64SafeDecode = (base64:string) => B64Decode(B64Unsafe(base64))\n */\n const B64SafeEncode = (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode)))\n .replace(/[+/=]/g, (m) => m == \"+\" ? \"-\" : m == \"/\" ? \"_\" : \"\"),\n B64Decode = (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0)));\n\n // Return a random key: 3 chars max, base-36 number < 36**3\n const randKey = () => Math.floor(Math.random() * 36 ** 3).toString(36);\n\n // Manage the page augmentation for Keynav, called only if storageSupport\n const augmentKeynav: AugmentKeynav = async (nav, [storageKey, rootKey, pageKey, last, spliceArgs]) => {\n let augmentPage:(page: string) => string;\n const browserKey = document.cookie.split(/;\\s+/) // it works even if malformed\n .find((row) => row.startsWith(pagy + \"=\"))\n ?.split(\"=\")[1] ?? randKey();\n document.cookie = pagy + \"=\" + browserKey; // Smaller .min size: set the cookie without checking\n if (storageKey && !(storageKey in storage)) {\n // Sync the sessiongStorage from other tabs/windows (e.g., open page in the new tab/window)\n sync.postMessage(<SyncData>{ from: tabId, key: storageKey });\n // Wait for the listener to copy the cutoffs in the current sessionStorage\n await new Promise<string|null>((resolve) => setTimeout(() => resolve(\"\"), 100));\n if (!(storageKey in storage)) { // the storageKey didn't get copied: fallback to countless pagination\n augmentPage = (page: string) => page + '+' + last;\n }\n }\n // @ts-expect-error If it is not assigned it means it supports keynav\n if (!augmentPage) { // regular keynav pagination\n if (!storageKey) { do { storageKey = randKey() } while (storageKey in storage) } // no dup keys\n const data = storage.getItem(storageKey),\n cutoffs = <Cutoff[]>(data ? JSON.parse(data) : [undefined]);\n if (spliceArgs) {\n cutoffs.splice(...spliceArgs);\n storage.setItem(storageKey, JSON.stringify(cutoffs));\n }\n // Augment function\n augmentPage = (page:string) => {\n const pageNum = parseInt(page);\n return B64SafeEncode(JSON.stringify(\n <AugmentedPage>[browserKey,\n storageKey,\n pageNum,\n cutoffs.length, // pages/last\n cutoffs[pageNum - 1], // priorCutoff\n cutoffs[pageNum]])); // pageCutoff\n };\n }\n const search = (rootKey) ? `${rootKey}%5B${pageKey}%5D` : pageKey;\n const re = new RegExp(`(?<=\\\\?.*)(\\\\b${search}=)(\\\\d+)`);\n // Augment the page param of each href\n for (const a of <NodeListOf<HTMLAnchorElement>><unknown>nav.querySelectorAll('a[href]')) {\n a.href = a.href.replace(re, (_match, prefix, digit): string => `${prefix}${augmentPage(<string>digit)}`);\n }\n // Return the augment function for further augmentation (i.e., url token in input_nav_js)\n return augmentPage;\n };\n\n // Build the series_nav_js helper\n const buildNavJs = (nav:NavJsElement, [[before, anchor, current, gap, after],\n [widths, series, labels], keynavArgs]:SeriesNavJsArgs) => {\n const parent = <HTMLElement>nav.parentElement;\n let lastWidth = -1;\n (nav.render = () => {\n const index = widths.findIndex(w => w < parent.clientWidth);\n if (widths[index] === lastWidth) { return } // no change: abort\n\n let html = before;\n series[index].forEach((item, i) => {\n // Avoid the if blocks and chain the results (shorter pagy.min.js and easier reading)\n html += item == \"gap\" ? gap :\n // @ts-expect-error the item may be a number, but the 'replace' converts it to string (shorter pagy.min.js)\n (typeof item == \"number\" ? anchor.replace(pageRe, item) : current)\n .replace(\"L<\", labels?.[index][i] ?? item + \"<\");\n });\n html += after;\n nav.innerHTML = \"\";\n nav.insertAdjacentHTML(\"afterbegin\", html);\n lastWidth = widths[index];\n if (keynavArgs && storageSupport) { void augmentKeynav(nav, keynavArgs) }\n })();\n if (nav.classList.contains(pagy + \"-rjs\")) { rjsObserver.observe(parent) }\n };\n\n // Init the input_nav_js helpers\n const initInputNavJs = async (nav:HTMLElement, [url_token, keynavArgs]:InputNavJsArgs) => {\n const augment = keynavArgs && storageSupport\n ? await augmentKeynav(nav, keynavArgs)\n : (page: string) => page;\n initInput(nav, inputValue => url_token.replace(pageRe, augment(inputValue)));\n };\n\n // Init the limit_tag_js helper\n const initLimitTagJs = (span:HTMLSpanElement, [from, url_token]:LimitTagJsArgs) => {\n initInput(span, inputValue => {\n // @ts-expect-error the page is a number, but the 'replace' converts it to string (shorter pagy.min.js)\n return url_token.replace(pageRe, Math.max(Math.ceil(from / parseInt(inputValue)), 1))\n .replace('L ', inputValue);\n });\n };\n\n // Init the input element\n const initInput = (element:HTMLElement, getUrl:(v:string) => string) => {\n const input = <HTMLInputElement>element.querySelector(\"input\"),\n link = <HTMLAnchorElement>element.querySelector(\"a\"),\n initial = input.value,\n action = () => {\n if (input.value === initial) { return } // not changed\n const [min, val, max] = [input.min, input.value, input.max].map(n => parseInt(n) || 0);\n if (val < min || val > max) { // reset invalid/out-of-range\n input.value = initial;\n input.select();\n return;\n }\n link.href = getUrl(input.value);\n link.click();\n };\n input.addEventListener(\"focus\", () => input.select());\n input.addEventListener(\"focusout\", action);\n input.addEventListener(\"keypress\", e => { if (e.key == \"Enter\") { action() } });\n };\n\n // Public interface\n return {\n version: \"43.1.2\",\n\n // Scan for elements with a \"data-pagy\" attribute and call their init functions with the decoded args\n init(arg?:HTMLElement) {\n const target = arg instanceof HTMLElement ? arg : document,\n elements = target.querySelectorAll(\"[data-pagy]\");\n for (const element of <NodeListOf<HTMLElement>>elements) {\n try {\n const [helperId, ...args] = <InitArgs>JSON.parse(B64Decode(<string>element.getAttribute(\"data-pagy\")));\n if (helperId == \"k\") {\n // @ts-expect-error spread 2 arguments, not 3 as it complains about\n void augmentKeynav(element, ...<KeynavArgs><unknown>args);\n } else if (helperId == \"snj\") {\n buildNavJs(<NavJsElement>element, <SeriesNavJsArgs><unknown>args);\n } else if (helperId == \"inj\") {\n void initInputNavJs(element, <InputNavJsArgs><unknown>args);\n } else if (helperId == \"ltj\") {\n initLimitTagJs(element, <LimitTagJsArgs><unknown>args);\n }\n // else { console.warn(\"Pagy.init: %o\\nUnknown helperId '%s'\", element, helperId) }\n } catch (err) { console.warn(\"Pagy.init: %o\\n%s\", element, err) }\n }\n }\n };\n})();\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "43829892A3C537E264756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
data/javascripts/pagy.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
window.Pagy=(()=>{const
|
|
1
|
+
window.Pagy=(()=>{const T="sessionStorage"in window&&"BroadcastChannel"in window;let L="pagy",Y,O,B;if(T)Y=sessionStorage,O=new BroadcastChannel(L),B=Date.now(),O.addEventListener("message",(q)=>{if(q.data.from){const z=Y.getItem(q.data.key);if(z)O.postMessage({to:q.data.from,key:q.data.key,str:z})}else if(q.data.to){if(q.data.to==B)Y.setItem(q.data.key,q.data.str)}});const S=new ResizeObserver((q)=>q.forEach((z)=>{z.target.querySelectorAll(".pagy-rjs").forEach((C)=>C.render())})),_=(q)=>btoa(String.fromCharCode(...new TextEncoder().encode(q))).replace(/[+/=]/g,(z)=>z=="+"?"-":z=="/"?"_":""),N=(q)=>new TextDecoder().decode(Uint8Array.from(atob(q),(z)=>z.charCodeAt(0))),J=()=>Math.floor(Math.random()*46656).toString(36),W=async(q,[z,C,F,G,Q])=>{let M;const $=document.cookie.split(/;\s+/).find((H)=>H.startsWith(L+"="))?.split("=")[1]??J();if(document.cookie=L+"="+$,z&&!(z in Y)){if(O.postMessage({from:B,key:z}),await new Promise((H)=>setTimeout(()=>H(""),100)),!(z in Y))M=(H)=>H+"+"+G}if(!M){if(!z)do z=J();while(z in Y);const H=Y.getItem(z),R=H?JSON.parse(H):[void 0];if(Q)R.splice(...Q),Y.setItem(z,JSON.stringify(R));M=(Z)=>{const X=parseInt(Z);return _(JSON.stringify([$,z,X,R.length,R[X-1],R[X]]))}}const E=C?`${C}%5B${F}%5D`:F,j=new RegExp(`(?<=\\?.*)(\\b${E}=)(\\d+)`);for(let H of q.querySelectorAll("a[href]"))H.href=H.href.replace(j,(R,Z,X)=>`${Z}${M(X)}`);return M},P=(q,[[z,C,F,G,Q],[M,$,E],j])=>{const H=q.parentElement;let R=-1;if((q.render=()=>{const Z=M.findIndex((D)=>D<H.clientWidth);if(M[Z]===R)return;let X=z;if($[Z].forEach((D,A)=>{X+=D=="gap"?G:(typeof D=="number"?C.replace("P ",D):F).replace("L<",E?.[Z][A]??D+"<")}),X+=Q,q.innerHTML="",q.insertAdjacentHTML("afterbegin",X),R=M[Z],j&&T)W(q,j)})(),q.classList.contains(L+"-rjs"))S.observe(H)},V=async(q,[z,C])=>{const F=C&&T?await W(q,C):(G)=>G;U(q,(G)=>z.replace("P ",F(G)))},x=(q,[z,C])=>{U(q,(F)=>{return C.replace("P ",Math.max(Math.ceil(z/parseInt(F)),1)).replace("L ",F)})},U=(q,z)=>{const C=q.querySelector("input"),F=q.querySelector("a"),G=C.value,Q=()=>{if(C.value===G)return;const[M,$,E]=[C.min,C.value,C.max].map((j)=>parseInt(j)||0);if($<M||$>E){C.value=G,C.select();return}F.href=z(C.value),F.click()};C.addEventListener("focus",()=>C.select()),C.addEventListener("focusout",Q),C.addEventListener("keypress",(M)=>{if(M.key=="Enter")Q()})};return{version:"43.1.2",init(q){const z=q instanceof HTMLElement?q:document,C=z.querySelectorAll("[data-pagy]");for(let F of C)try{const[G,...Q]=JSON.parse(N(F.getAttribute("data-pagy")));if(G=="k")W(F,...Q);else if(G=="snj")P(F,Q);else if(G=="inj")V(F,Q);else if(G=="ltj")x(F,Q)}catch(G){console.warn("Pagy.init: %o\n%s",F,G)}}}})();
|
data/javascripts/pagy.mjs
CHANGED
|
@@ -23,18 +23,18 @@ const Pagy = (() => {
|
|
|
23
23
|
}));
|
|
24
24
|
const B64SafeEncode = (unicode) => btoa(String.fromCharCode(...new TextEncoder().encode(unicode))).replace(/[+/=]/g, (m) => m == "+" ? "-" : m == "/" ? "_" : ""), B64Decode = (base64) => new TextDecoder().decode(Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)));
|
|
25
25
|
const randKey = () => Math.floor(Math.random() * 36 ** 3).toString(36);
|
|
26
|
-
const augmentKeynav = async (nav, [storageKey, pageKey, last, spliceArgs]) => {
|
|
27
|
-
let
|
|
26
|
+
const augmentKeynav = async (nav, [storageKey, rootKey, pageKey, last, spliceArgs]) => {
|
|
27
|
+
let augmentPage;
|
|
28
28
|
const browserKey = document.cookie.split(/;\s+/).find((row) => row.startsWith(pagy + "="))?.split("=")[1] ?? randKey();
|
|
29
29
|
document.cookie = pagy + "=" + browserKey;
|
|
30
30
|
if (storageKey && !(storageKey in storage)) {
|
|
31
31
|
sync.postMessage({ from: tabId, key: storageKey });
|
|
32
32
|
await new Promise((resolve) => setTimeout(() => resolve(""), 100));
|
|
33
33
|
if (!(storageKey in storage)) {
|
|
34
|
-
|
|
34
|
+
augmentPage = (page) => page + "+" + last;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (!
|
|
37
|
+
if (!augmentPage) {
|
|
38
38
|
if (!storageKey) {
|
|
39
39
|
do {
|
|
40
40
|
storageKey = randKey();
|
|
@@ -45,7 +45,7 @@ const Pagy = (() => {
|
|
|
45
45
|
cutoffs.splice(...spliceArgs);
|
|
46
46
|
storage.setItem(storageKey, JSON.stringify(cutoffs));
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
augmentPage = (page) => {
|
|
49
49
|
const pageNum = parseInt(page);
|
|
50
50
|
return B64SafeEncode(JSON.stringify([
|
|
51
51
|
browserKey,
|
|
@@ -57,11 +57,12 @@ const Pagy = (() => {
|
|
|
57
57
|
]));
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
const search = rootKey ? `${rootKey}%5B${pageKey}%5D` : pageKey;
|
|
61
|
+
const re = new RegExp(`(?<=\\?.*)(\\b${search}=)(\\d+)`);
|
|
60
62
|
for (const a of nav.querySelectorAll("a[href]")) {
|
|
61
|
-
|
|
62
|
-
a.href = url.replace(re, pageKey + "=" + augment(url.match(re)[1]));
|
|
63
|
+
a.href = a.href.replace(re, (_match, prefix, digit) => `${prefix}${augmentPage(digit)}`);
|
|
63
64
|
}
|
|
64
|
-
return
|
|
65
|
+
return augmentPage;
|
|
65
66
|
};
|
|
66
67
|
const buildNavJs = (nav, [
|
|
67
68
|
[before, anchor, current, gap, after],
|
|
@@ -123,7 +124,7 @@ const Pagy = (() => {
|
|
|
123
124
|
});
|
|
124
125
|
};
|
|
125
126
|
return {
|
|
126
|
-
version: "43.1.
|
|
127
|
+
version: "43.1.2",
|
|
127
128
|
init(arg) {
|
|
128
129
|
const target = arg instanceof HTMLElement ? arg : document, elements = target.querySelectorAll("[data-pagy]");
|
|
129
130
|
for (const element of elements) {
|
|
@@ -8,8 +8,8 @@ class Pagy
|
|
|
8
8
|
assign_options(**)
|
|
9
9
|
assign_and_check(limit: 1, page: 1)
|
|
10
10
|
@page = upto_max_pages(@page)
|
|
11
|
-
assign_last
|
|
12
11
|
assign_offset
|
|
12
|
+
assign_last unless @options[:headless]
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def records(collection)
|
|
@@ -31,9 +31,7 @@ class Pagy
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def assign_last
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@last = upto_max_pages(@options[:last].to_i)
|
|
34
|
+
@last = @options[:last] ? upto_max_pages(@options[:last].to_i) : @page
|
|
37
35
|
end
|
|
38
36
|
|
|
39
37
|
# Finalize the instance variables based on the fetched size
|
|
@@ -51,6 +49,9 @@ class Pagy
|
|
|
51
49
|
assign_previous_and_next
|
|
52
50
|
self
|
|
53
51
|
end
|
|
52
|
+
|
|
53
|
+
# Support easy countless page param overriding (for legacy param and behavior)
|
|
54
|
+
def compose_page_param(page) = "#{page || 1}+#{@last}"
|
|
54
55
|
end
|
|
55
56
|
end
|
|
56
57
|
end
|
data/lib/pagy/classes/request.rb
CHANGED
|
@@ -4,7 +4,9 @@ class Pagy
|
|
|
4
4
|
# Decouple the request from the env, allowing non-rack apps to use pagy by passing a hash.
|
|
5
5
|
# Resolve the :page and :limit options from params.
|
|
6
6
|
class Request
|
|
7
|
-
def initialize(
|
|
7
|
+
def initialize(options)
|
|
8
|
+
@options = options
|
|
9
|
+
request = @options[:request]
|
|
8
10
|
@base_url, @path, @params, @cookie =
|
|
9
11
|
if request.is_a?(Hash)
|
|
10
12
|
request.values_at(:base_url, :path, :params, :cookie)
|
|
@@ -15,20 +17,20 @@ class Pagy
|
|
|
15
17
|
|
|
16
18
|
attr_reader :base_url, :path, :params, :cookie
|
|
17
19
|
|
|
18
|
-
def resolve_page(
|
|
19
|
-
page_key = options[:page_key] || DEFAULT[:page_key]
|
|
20
|
-
page = @params.dig(options[:root_key], page_key) || @params[page_key]
|
|
20
|
+
def resolve_page(force_integer: true)
|
|
21
|
+
page_key = @options[:page_key] || DEFAULT[:page_key]
|
|
22
|
+
page = @params.dig(@options[:root_key], page_key) || @params[page_key]
|
|
21
23
|
page = nil if page == '' # fix for app-generated queries like ?page=
|
|
22
24
|
force_integer ? (page || 1).to_i : page
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def resolve_limit
|
|
26
|
-
limit_key = options[:limit_key] || DEFAULT[:limit_key]
|
|
27
|
-
return options[:limit] || DEFAULT[:limit] \
|
|
28
|
-
unless options[:client_max_limit] &&
|
|
29
|
-
(requested_limit = @params.dig(options[:root_key], limit_key) || @params[limit_key])
|
|
27
|
+
def resolve_limit
|
|
28
|
+
limit_key = @options[:limit_key] || DEFAULT[:limit_key]
|
|
29
|
+
return @options[:limit] || DEFAULT[:limit] \
|
|
30
|
+
unless @options[:client_max_limit] &&
|
|
31
|
+
(requested_limit = @params.dig(@options[:root_key], limit_key) || @params[limit_key])
|
|
30
32
|
|
|
31
|
-
[requested_limit.to_i, options[:client_max_limit]].min
|
|
33
|
+
[requested_limit.to_i, @options[:client_max_limit]].min
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
end
|
|
@@ -9,25 +9,22 @@ class Pagy
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
# Extracted from Rack::Utils and reformatted for rubocop
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
def build_nested_query(value, prefix = nil, unescaped = [])
|
|
12
|
+
# Allow unescaped Pagy::RawQueryValue
|
|
13
|
+
def build_nested_query(value, prefix = nil)
|
|
15
14
|
case value
|
|
16
15
|
when Array
|
|
17
|
-
value.map { |v| build_nested_query(v, "#{prefix}[]"
|
|
16
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
|
|
18
17
|
when Hash
|
|
19
18
|
value.map do |k, v|
|
|
20
|
-
|
|
21
|
-
unescaped[unescaped.find_index(k)] = new_k if unescaped.size.positive? && new_k != k && unescaped.include?(k)
|
|
22
|
-
build_nested_query(v, new_k, unescaped)
|
|
19
|
+
build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
|
|
23
20
|
end.delete_if(&:empty?).join('&')
|
|
24
21
|
when nil
|
|
25
22
|
escape(prefix)
|
|
26
23
|
else
|
|
27
24
|
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
|
28
|
-
return "#{escape(prefix)}=#{value}" if unescaped.include?(prefix)
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
final_value = value.is_a?(RawQueryValue) ? value.to_s : escape(value)
|
|
27
|
+
"#{escape(prefix)}=#{final_value}"
|
|
31
28
|
end
|
|
32
29
|
end
|
|
33
30
|
|
|
@@ -46,12 +43,12 @@ class Pagy
|
|
|
46
43
|
params = @request.params.clone(freeze: false)
|
|
47
44
|
params.delete(root_key || page_key)
|
|
48
45
|
factors = {}.tap do |h|
|
|
49
|
-
h[page_key] = countless? ?
|
|
46
|
+
h[page_key] = countless? ? RawQueryValue.new(compose_page_param(page)) : page
|
|
50
47
|
h[limit_key] = limit_token || limit if client_max_limit
|
|
51
48
|
end.compact # No empty params
|
|
52
49
|
params.merge!(root_key ? { root_key => factors } : factors) if factors.size.positive?
|
|
53
50
|
querify&.(params) # Must modify the params: the returned value is ignored
|
|
54
|
-
query_string = QueryUtils.build_nested_query(params
|
|
51
|
+
query_string = QueryUtils.build_nested_query(params)
|
|
55
52
|
query_string = "?#{query_string}" unless query_string.empty?
|
|
56
53
|
fragment &&= %(##{fragment}) unless fragment&.start_with?('#')
|
|
57
54
|
"#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
|
|
@@ -7,8 +7,8 @@ class Pagy
|
|
|
7
7
|
|
|
8
8
|
# Common search logic
|
|
9
9
|
def wrap(pagy_search_args, options)
|
|
10
|
-
options[:page] ||= options[:request].resolve_page
|
|
11
|
-
options[:limit] = options[:request].resolve_limit
|
|
10
|
+
options[:page] ||= options[:request].resolve_page
|
|
11
|
+
options[:limit] = options[:request].resolve_limit
|
|
12
12
|
pagy, results = yield
|
|
13
13
|
calling = pagy_search_args[4..]
|
|
14
14
|
[pagy, calling.empty? ? results : results.send(*calling)]
|
|
@@ -18,7 +18,7 @@ class Pagy
|
|
|
18
18
|
%( #{anchor_string}) if anchor_string}).split(PAGE_TOKEN, 2)
|
|
19
19
|
|
|
20
20
|
lambda do |page, text = page_label(page), classes: nil, aria_label: nil|
|
|
21
|
-
title = if (counts = @options[:counts]) # only for calendar
|
|
21
|
+
title = if (counts = @options[:counts]) # only for calendar with counts
|
|
22
22
|
count = counts[page - 1]
|
|
23
23
|
classes = classes ? "#{classes} empty-page" : 'empty-page' if count.zero?
|
|
24
24
|
info_key = count.zero? ? 'pagy.info_tag.no_items' : 'pagy.info_tag.single_page'
|
|
@@ -7,14 +7,14 @@ class Pagy
|
|
|
7
7
|
# Return the Offset::Countless instance and records
|
|
8
8
|
def paginate(collection, options)
|
|
9
9
|
if options[:page].nil?
|
|
10
|
-
page = options[:request].resolve_page(
|
|
10
|
+
page = options[:request].resolve_page(force_integer: false) # accept nil and strings
|
|
11
11
|
if page.is_a?(String)
|
|
12
12
|
p, l = page.split(/ /, 2).map(&:to_i) # decoded '+' added by the compose_page_url
|
|
13
13
|
options[:page] = p if p.positive?
|
|
14
14
|
options[:last] = l if l&.positive?
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
-
options[:limit] = options[:request].resolve_limit
|
|
17
|
+
options[:limit] = options[:request].resolve_limit
|
|
18
18
|
pagy = Offset::Countless.new(**options)
|
|
19
19
|
[pagy, pagy.records(collection)]
|
|
20
20
|
end
|
|
@@ -9,16 +9,16 @@ class Pagy
|
|
|
9
9
|
# Return the Pagy::Keyset::Keynav instance and paginated records.
|
|
10
10
|
# Fall back to :countless if the :page has no client data.
|
|
11
11
|
def paginate(set, options)
|
|
12
|
-
page = options[:request].resolve_page(
|
|
12
|
+
page = options[:request].resolve_page(force_integer: false) # allow nil
|
|
13
13
|
if page&.match(' ') # countless page -> no augmentation -> fallback
|
|
14
14
|
return CountlessPaginator.paginate(set, page:, **options)
|
|
15
15
|
elsif page.is_a?(String) # keynav page param
|
|
16
16
|
page_arguments = JSON.parse(B64.urlsafe_decode(page))
|
|
17
|
-
# Restart the pagination from page 1 if the url has been requested from another browser
|
|
17
|
+
# Restart the pagination from page 1/nil if the url has been requested from another browser
|
|
18
18
|
options[:page] = page_arguments if options[:request].cookie == page_arguments.shift
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
options[:limit] = options[:request].resolve_limit
|
|
21
|
+
options[:limit] = options[:request].resolve_limit
|
|
22
22
|
pagy = Keyset::Keynav.new(set, **options)
|
|
23
23
|
[pagy, pagy.records]
|
|
24
24
|
end
|
|
@@ -6,8 +6,8 @@ class Pagy
|
|
|
6
6
|
|
|
7
7
|
# Return Pagy::Keyset instance and paginated records
|
|
8
8
|
def paginate(set, options)
|
|
9
|
-
options[:page] ||= options[:request].resolve_page(
|
|
10
|
-
options[:limit] = options[:request].resolve_limit
|
|
9
|
+
options[:page] ||= options[:request].resolve_page(force_integer: false) # allow nil
|
|
10
|
+
options[:limit] = options[:request].resolve_limit
|
|
11
11
|
pagy = Keyset.new(set, **options)
|
|
12
12
|
[pagy, pagy.records]
|
|
13
13
|
end
|
|
@@ -15,18 +15,19 @@ class Pagy
|
|
|
15
15
|
path = Pathname.new(__dir__)
|
|
16
16
|
paginators.each { |symbol, name| autoload name, path.join(symbol.to_s) }
|
|
17
17
|
|
|
18
|
-
# Pagy::Method defines the pagy method to be included in the app controller/view.
|
|
18
|
+
# Pagy::Method defines the #pagy method to be included in the app controller/view.
|
|
19
19
|
Method = Module.new do
|
|
20
20
|
protected
|
|
21
21
|
|
|
22
22
|
define_method :pagy do |paginator = :offset, collection, **options|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
arguments = if paginator == :calendar
|
|
24
|
+
[self, collection, options]
|
|
25
|
+
else
|
|
26
|
+
[collection, options = Pagy.options.merge(options)]
|
|
27
|
+
end
|
|
28
|
+
options[:root_key] = 'page' if options[:jsonapi] # enforce 'page' root_key for JSON:API
|
|
29
|
+
options[:request] ||= request # user set request or self.request
|
|
30
|
+
options[:request] = Request.new(options) # Pagy::Request
|
|
30
31
|
Pagy.const_get(paginators[paginator]).paginate(*arguments)
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -6,8 +6,8 @@ class Pagy
|
|
|
6
6
|
|
|
7
7
|
# Return the Pagy::Offset instance and results
|
|
8
8
|
def paginate(collection, options)
|
|
9
|
-
options[:page] ||= options[:request].resolve_page
|
|
10
|
-
options[:limit] = options[:request].resolve_limit
|
|
9
|
+
options[:page] ||= options[:request].resolve_page
|
|
10
|
+
options[:limit] = options[:request].resolve_limit
|
|
11
11
|
options[:count] ||= collection.instance_of?(Array) ? collection.size : OffsetPaginator.get_count(collection, options)
|
|
12
12
|
pagy = Offset.new(**options)
|
|
13
13
|
[pagy, collection.instance_of?(Array) ? collection[pagy.offset, pagy.limit] : pagy.records(collection)]
|
data/lib/pagy.rb
CHANGED
|
@@ -8,11 +8,13 @@ require_relative 'pagy/toolbox/helpers/loader'
|
|
|
8
8
|
|
|
9
9
|
# Top superclass: it defines only what's common to all the subclasses
|
|
10
10
|
class Pagy
|
|
11
|
-
|
|
11
|
+
class RawQueryValue < String; end
|
|
12
|
+
|
|
13
|
+
VERSION = '43.1.2'
|
|
12
14
|
ROOT = Pathname.new(__dir__).parent.freeze
|
|
13
15
|
DEFAULT = { limit: 20, limit_key: 'limit', page_key: 'page' }.freeze
|
|
14
|
-
PAGE_TOKEN = 'P '
|
|
15
|
-
LIMIT_TOKEN = 'L '
|
|
16
|
+
PAGE_TOKEN = RawQueryValue.new('P ')
|
|
17
|
+
LIMIT_TOKEN = RawQueryValue.new('L ')
|
|
16
18
|
LABEL_TOKEN = 'L'
|
|
17
19
|
A_TAG = '<a style="display: none;">#</a>'
|
|
18
20
|
|
|
@@ -46,7 +48,7 @@ class Pagy
|
|
|
46
48
|
def keyset? = false
|
|
47
49
|
def keynav? = false
|
|
48
50
|
|
|
49
|
-
# Validates and assign the passed options:
|
|
51
|
+
# Validates and assign the passed options: they must be present and value.to_i must be >= min
|
|
50
52
|
def assign_and_check(name_min)
|
|
51
53
|
name_min.each do |name, min|
|
|
52
54
|
raise OptionError.new(self, name, ">= #{min}", @options[name]) \
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pagy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 43.1.
|
|
4
|
+
version: 43.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Domizio Demichelis
|
|
@@ -49,6 +49,7 @@ files:
|
|
|
49
49
|
- apps/calendar.ru
|
|
50
50
|
- apps/demo.ru
|
|
51
51
|
- apps/index.rb
|
|
52
|
+
- apps/keynav+root_key.ru
|
|
52
53
|
- apps/keynav.ru
|
|
53
54
|
- apps/keyset.ru
|
|
54
55
|
- apps/keyset_sequel.ru
|