pagy 9.2.0 → 9.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/apps/rails.ru CHANGED
@@ -1,21 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Starting point to reproduce rails related pagy issues
4
-
3
+ # DESCRIPTION
4
+ # Reproduce rails related issues
5
+ #
6
+ # DOC
7
+ # https://ddnexus.github.io/pagy/playground/#2-rails-app
8
+ #
9
+ # BIN HELP
10
+ # bundle exec pagy -h
11
+ #
5
12
  # DEV USAGE
6
- # pagy clone rails
7
- # pagy ./rails.ru
8
-
13
+ # bundle exec pagy clone rails
14
+ # bundle exec pagy ./rails.ru
15
+ #
9
16
  # URL
10
17
  # http://0.0.0.0:8000
11
18
 
12
- # HELP
13
- # pagy -h
14
-
15
- # DOC
16
- # https://ddnexus.github.io/pagy/playground/#2-rails-app
17
-
18
- VERSION = '9.2.0'
19
+ VERSION = '9.3.1'
19
20
 
20
21
  # Gemfile
21
22
  require 'bundler/inline'
@@ -25,10 +26,8 @@ gemfile(ENV['PAGY_INSTALL_BUNDLE'] == 'true') do
25
26
  source 'https://rubygems.org'
26
27
  gem 'oj'
27
28
  gem 'puma'
28
- gem 'rails'
29
- # activerecord/sqlite3_adapter.rb probably useless) constraint !!!
30
- # https://github.com/rails/rails/blame/v7.1.3.4/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L14
31
- gem 'sqlite3', '~> 1.4.0'
29
+ gem 'rails', '~> 8.0'
30
+ gem 'sqlite3'
32
31
  end
33
32
 
34
33
  # require 'rails/all' # too much stuff
data/apps/repro.ru CHANGED
@@ -1,22 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Starting point app to try pagy or reproduce issues
4
-
3
+ # DESCRIPTION
4
+ # Reproduce generic/simple issues
5
+ #
6
+ # DOC
7
+ # https://ddnexus.github.io/pagy/playground/#1-repro-app
8
+ #
9
+ # BIN HELP
10
+ # bundle exec pagy -h
11
+ #
5
12
  # DEV USAGE
6
- # pagy clone repro
7
- # pagy ./repro.ru
8
-
13
+ # bundle exec pagy clone repro
14
+ # bundle exec pagy ./repro.ru
15
+ #
9
16
  # URL
10
17
  # http://0.0.0.0:8000
11
18
 
12
- # HELP
13
- # pagy -h
14
-
15
- # DOC
16
- # https://ddnexus.github.io/pagy/playground/#1-repro-app
17
-
18
- VERSION = '9.2.0'
19
+ VERSION = '9.3.1'
19
20
 
21
+ # Bundle
20
22
  require 'bundler/inline'
21
23
  require 'bundler'
22
24
  Bundler.configure
@@ -25,7 +27,6 @@ gemfile(ENV['PAGY_INSTALL_BUNDLE'] == 'true') do
25
27
  gem 'oj'
26
28
  gem 'puma'
27
29
  gem 'sinatra'
28
- gem 'sinatra-contrib'
29
30
  end
30
31
 
31
32
  # Edit this section adding/removing the extras and Pagy::DEFAULT as needed
@@ -36,12 +37,10 @@ require 'pagy/extras/overflow'
36
37
  Pagy::DEFAULT[:overflow] = :empty_page
37
38
  Pagy::DEFAULT.freeze
38
39
 
40
+ # Sinatra setup
39
41
  require 'sinatra/base'
40
42
  # Sinatra application
41
43
  class PagyRepro < Sinatra::Base
42
- configure do
43
- enable :inline_templates
44
- end
45
44
  include Pagy::Backend
46
45
 
47
46
  get('/javascripts/:file') do
@@ -58,12 +57,103 @@ class PagyRepro < Sinatra::Base
58
57
  get '/' do
59
58
  collection = MockCollection.new
60
59
  @pagy, @records = pagy(collection)
61
- erb :main # template available in the __END__ section as @@ main
60
+ erb :main
62
61
  end
62
+
63
63
  # Edit this section adding your own helpers as needed
64
64
  helpers do
65
65
  include Pagy::Frontend
66
66
  end
67
+
68
+ # Views
69
+ template :layout do
70
+ <<~ERB
71
+ <!DOCTYPE html>
72
+ <html lang="en">
73
+ <html>
74
+ <head>
75
+ <title>Pagy Repro App</title>
76
+ <script src="javascripts/pagy.min.js"></script>
77
+ <script>
78
+ window.addEventListener("load", Pagy.init);
79
+ </script>
80
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
81
+ <style type="text/css">
82
+ @media screen { html, body {
83
+ font-size: 1rem;
84
+ line-height: 1.2s;
85
+ padding: 0;
86
+ margin: 0;
87
+ } }
88
+ body {
89
+ background: white !important;
90
+ margin: 0 !important;
91
+ font-family: sans-serif !important;
92
+ }
93
+ .content {
94
+ padding: 1rem 1.5rem 2rem !important;
95
+ }
96
+
97
+ /* Quick demo for overriding the element style attribute of certain pagy helpers
98
+ .pagy input[style] {
99
+ width: 5rem !important;
100
+ }
101
+ */
102
+
103
+ /*
104
+ If you want to customize the style,
105
+ please replace the line below with the actual file content
106
+ */
107
+ <%= Pagy.root.join('stylesheets', 'pagy.css').read %>
108
+ </style>
109
+ </head>
110
+ <body>
111
+ <%= yield %>
112
+ </body>
113
+ </html>
114
+ ERB
115
+ end
116
+
117
+ template :main do
118
+ <<~ERB
119
+ <div class="content">
120
+ <h1>Pagy Repro App</h1>
121
+ <p> Self-contained, standalone app usable to easily reproduce any pagy issue.</p>
122
+ <p>Please, report the following versions in any new issue.</p>
123
+ <h2>Versions</h4>
124
+ <ul>
125
+ <li>Ruby: <%= RUBY_VERSION %></li>
126
+ <li>Rack: <%= Rack::RELEASE %></li>
127
+ <li>Sinatra: <%= Sinatra::VERSION %></li>
128
+ <li>Pagy: <%= Pagy::VERSION %></li>
129
+ </ul>
130
+
131
+ <h3>Collection</h3>
132
+ <p id="records">@records: <%= @records.join(',') %></p>
133
+
134
+ <hr>
135
+
136
+ <h4>pagy_nav</h4>
137
+ <%= pagy_nav(@pagy, id: 'nav', aria_label: 'Pages nav') %>
138
+
139
+ <h4>pagy_nav_js</h4>
140
+ <%= pagy_nav_js(@pagy, id: 'nav-js', aria_label: 'Pages nav_js') %>
141
+
142
+ <h4>pagy_nav_js</h4>
143
+ <%= pagy_nav_js(@pagy, id: 'nav-js-responsive', aria_label: 'Pages nav_js_responsove',
144
+ steps: { 0 => 5, 500 => 7, 750 => 9, 1000 => 11 }) %>
145
+
146
+ <h4>pagy_combo_nav_js</h4>
147
+ <%= pagy_combo_nav_js(@pagy, id: 'combo-nav-js', aria_label: 'Pages combo_nav_js') %>
148
+
149
+ <h4>pagy_limit_selector_js</h4>
150
+ <%= pagy_limit_selector_js(@pagy, id: 'limit-selector-js') %>
151
+
152
+ <h4>pagy_info</h4>
153
+ <%= pagy_info(@pagy, id: 'pagy-info') %>
154
+ </div>
155
+ ERB
156
+ end
67
157
  end
68
158
 
69
159
  # Simple array-based collection that acts as a standard DB collection.
@@ -90,88 +180,3 @@ class MockCollection < Array
90
180
  end
91
181
 
92
182
  run PagyRepro
93
-
94
- __END__
95
-
96
- @@ layout
97
- <!DOCTYPE html>
98
- <html lang="en">
99
- <html>
100
- <head>
101
- <title>Pagy Repro App</title>
102
- <script src="javascripts/pagy.min.js"></script>
103
- <script>
104
- window.addEventListener("load", Pagy.init);
105
- </script>
106
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
107
- <style type="text/css">
108
- @media screen { html, body {
109
- font-size: 1rem;
110
- line-height: 1.2s;
111
- padding: 0;
112
- margin: 0;
113
- } }
114
- body {
115
- background: white !important;
116
- margin: 0 !important;
117
- font-family: sans-serif !important;
118
- }
119
- .content {
120
- padding: 1rem 1.5rem 2rem !important;
121
- }
122
-
123
- /* Quick demo for overriding the element style attribute of certain pagy helpers
124
- .pagy input[style] {
125
- width: 5rem !important;
126
- }
127
- */
128
-
129
- /*
130
- If you want to customize the style,
131
- please replace the line below with the actual file content
132
- */
133
- <%= Pagy.root.join('stylesheets', 'pagy.css').read %>
134
- </style>
135
- </head>
136
- <body>
137
- <%= yield %>
138
- </body>
139
- </html>
140
-
141
- @@ main
142
- <div class="content">
143
- <h1>Pagy Repro App</h1>
144
- <p> Self-contained, standalone Sinatra app usable to easily reproduce any pagy issue.</p>
145
- <p>Please, report the following versions in any new issue.</p>
146
- <h2>Versions</h4>
147
- <ul>
148
- <li>Ruby: <%= RUBY_VERSION %></li>
149
- <li>Rack: <%= Rack::RELEASE %></li>
150
- <li>Sinatra: <%= Sinatra::VERSION %></li>
151
- <li>Pagy: <%= Pagy::VERSION %></li>
152
- </ul>
153
-
154
- <h3>Collection</h3>
155
- <p id="records">@records: <%= @records.join(',') %></p>
156
-
157
- <hr>
158
-
159
- <h4>pagy_nav</h4>
160
- <%= pagy_nav(@pagy, id: 'nav', aria_label: 'Pages nav') %>
161
-
162
- <h4>pagy_nav_js</h4>
163
- <%= pagy_nav_js(@pagy, id: 'nav-js', aria_label: 'Pages nav_js') %>
164
-
165
- <h4>pagy_nav_js</h4>
166
- <%= pagy_nav_js(@pagy, id: 'nav-js-responsive', aria_label: 'Pages nav_js_responsove',
167
- steps: { 0 => 5, 500 => 7, 750 => 9, 1000 => 11 }) %>
168
-
169
- <h4>pagy_combo_nav_js</h4>
170
- <%= pagy_combo_nav_js(@pagy, id: 'combo-nav-js', aria_label: 'Pages combo_nav_js') %>
171
-
172
- <h4>pagy_limit_selector_js</h4>
173
- <%= pagy_limit_selector_js(@pagy, id: 'limit-selector-js') %>
174
-
175
- <h4>pagy_info</h4>
176
- <%= pagy_info(@pagy, id: 'pagy-info') %>
177
- </div>
data/bin/pagy CHANGED
@@ -1,24 +1,23 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- VERSION = '9.2.0'
5
- APPS = %w[repro rails demo calendar keyset_ar keyset_s].freeze
4
+ VERSION = '9.3.1'
6
5
  LINUX = RbConfig::CONFIG['host_os'].include?('linux')
7
6
  HOST = '0.0.0.0'
8
7
  PORT = '8000'
9
8
 
10
9
  require_relative '../lib/optimist'
10
+ require_relative '../apps/index'
11
+ apps = PagyApps::INDEX
11
12
  opts = Optimist.options do
12
13
  text <<~HEAD
13
14
  Pagy #{VERSION} (https://ddnexus.github.io/pagy/playground)
14
15
  Playground to showcase, clone and develop pagy APPs
15
16
  APPs
16
- repro Reproduce generic/simple issues
17
- rails Reproduce rails related issues
18
- demo Showcase all the helpers and styles
19
- calendar Showcase the calendar; reproduce related issues
20
- keyset_ar Showcase the keyset ActiveRecord pagination
21
- keyset_s Showcase the keyset Sequel pagination
17
+ #{ apps.map do |name, path|
18
+ " #{name}#{' ' * (27 - name.length)}#{File.readlines(path)[3].sub('# ', '').strip}"
19
+ end.join("\n")
20
+ }
22
21
  USAGE
23
22
  pagy APP [options] Showcase APP from the installed gem
24
23
  pagy clone APP Clone APP to the current dir
@@ -29,10 +28,10 @@ opts = Optimist.options do
29
28
  pagy ~/my-repro.ru Develop ~/my-repro.ru at#{HOST}:#{PORT}
30
29
  HEAD
31
30
  text 'Rackup options'
32
- opt :env, 'Environment', default: 'development'
33
- opt :host, 'Host', default: HOST, short: :o
34
- opt :port, 'Port', default: PORT
35
- opt :install, 'Install bundle for users', default: true
31
+ opt :env, 'Environment', default: 'development'
32
+ opt :host, 'Host', default: HOST, short: :o
33
+ opt :port, 'Port', default: PORT
34
+ opt :install, 'Install bundle for users', default: true
36
35
  if LINUX
37
36
  text 'Rerun options'
38
37
  opt :rerun, 'Enable rerun for development', default: true
@@ -46,7 +45,7 @@ Optimist.educate if ARGV.empty?
46
45
 
47
46
  run_from_repo = File.exist?(File.expand_path('../pagy.gemspec', __dir__))
48
47
 
49
- # Handles gems
48
+ # Bundle
50
49
  require 'bundler/inline'
51
50
  require 'bundler'
52
51
  Bundler.configure
@@ -56,28 +55,25 @@ gemfile(opts[:install]) do
56
55
  gem 'rerun' if LINUX
57
56
  end
58
57
 
59
- path = ->(app) { File.expand_path("../apps/#{app}.ru", __dir__) }
60
- arg = ARGV.shift
58
+ arg = ARGV.shift
61
59
  if arg.eql?('clone')
62
- arg = ARGV.shift
63
- Optimist.die("Expected APP to be in [#{APPS.join(', ')}]; got #{arg.inspect}") unless APPS.include?(arg)
64
- file = path.(arg)
65
- name = File.basename(file)
60
+ name = ARGV.shift
61
+ Optimist.die("Expected APP to be in [#{apps.keys.join(', ')}]; got #{arg.inspect}") unless apps.key?(arg)
66
62
  if File.exist?(name)
67
63
  print "Do you want to overwrite the #{name.inspect} file? (y/n)> "
68
64
  answer = gets.chomp
69
65
  Optimist.die("#{name.inspect} file already present") unless answer.start_with?(/y/i)
70
66
  end
71
67
  require 'fileutils'
72
- FileUtils.cp(file, '.', verbose: true)
68
+ FileUtils.cp(apps[arg], '.', verbose: true)
73
69
  else
74
- if APPS.include?(arg) # showcase env
70
+ if apps.key?(arg) # showcase env
75
71
  opts[:env] = 'showcase'
76
72
  opts[:rerun] = false
77
73
  opts[:quiet] = true
78
74
  # Avoid the creation of './tmp/local_secret.txt' for showcase env
79
75
  ENV['SECRET_KEY_BASE'] = 'absolute secret!' if arg.eql?('rails')
80
- file = path.(arg)
76
+ file = apps[arg]
81
77
  else # development env
82
78
  file = arg
83
79
  end
data/config/pagy.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Pagy initializer file (9.2.0)
3
+ # Pagy initializer file (9.3.1)
4
4
  # Customize only what you really need and notice that the core Pagy works also without any of the following lines.
5
5
  # Should you just cherry pick part of this file, please maintain the require-order of the extras
6
6
 
@@ -1,4 +1,4 @@
1
- window.Pagy=(()=>{const j=new ResizeObserver((B)=>B.forEach((D)=>D.target.querySelectorAll(".pagy-rjs").forEach((E)=>E.pagyRender()))),x=(B,[D,E,z,G])=>{const F=B.parentElement??B,K=Object.keys(E).map((H)=>parseInt(H)).sort((H,M)=>M-H);let L=-1;const T=(H,M,R)=>H.replace(/__pagy_page__/g,M).replace(/__pagy_label__/g,R);if((B.pagyRender=function(){const H=K.find((Q)=>Q<F.clientWidth)||0;if(H===L)return;let M=D.before;const R=E[H.toString()],X=z?.[H.toString()]??R.map((Q)=>Q.toString());R.forEach((Q,J)=>{const $=X[J];let U;if(typeof Q==="number")U=T(D.a,Q.toString(),$);else if(Q==="gap")U=D.gap;else U=T(D.current,Q,$);M+=typeof G==="string"&&Q==1?Z(U,G):U}),M+=D.after,B.innerHTML="",B.insertAdjacentHTML("afterbegin",M),L=H})(),B.classList.contains("pagy-rjs"))j.observe(F)},A=(B,[D,E])=>Y(B,(z)=>[z,D.replace(/__pagy_page__/,z)],E),C=(B,[D,E,z])=>{Y(B,(G)=>{const F=Math.max(Math.ceil(D/parseInt(G)),1).toString(),K=E.replace(/__pagy_page__/,F).replace(/__pagy_limit__/,G);return[F,K]},z)},Y=(B,D,E)=>{const z=B.querySelector("input"),G=B.querySelector("a"),F=z.value,K=function(){if(z.value===F)return;const[L,T,H]=[z.min,z.value,z.max].map((X)=>parseInt(X)||0);if(T<L||T>H){z.value=F,z.select();return}let[M,R]=D(z.value);if(typeof E==="string"&&M==="1")R=Z(R,E);G.href=R,G.click()};["change","focus"].forEach((L)=>z.addEventListener(L,()=>z.select())),z.addEventListener("focusout",K),z.addEventListener("keypress",(L)=>{if(L.key==="Enter")K()})},Z=(B,D)=>B.replace(new RegExp(`[?&]${D}=1\\b(?!&)|\\b${D}=1&`),"");return{version:"9.2.0",init(B){const E=(B instanceof Element?B:document).querySelectorAll("[data-pagy]");for(let z of E)try{const G=Uint8Array.from(atob(z.getAttribute("data-pagy")),(L)=>L.charCodeAt(0)),[F,...K]=JSON.parse(new TextDecoder().decode(G));if(F==="nav")x(z,K);else if(F==="combo")A(z,K);else if(F==="selector")C(z,K);else console.warn("Skipped Pagy.init() for: %o\nUnknown keyword '%s'",z,F)}catch(G){console.warn("Skipped Pagy.init() for: %o\n%s",z,G)}}}})();
1
+ window.Pagy=(()=>{const j=new ResizeObserver((B)=>B.forEach((D)=>D.target.querySelectorAll(".pagy-rjs").forEach((E)=>E.pagyRender()))),x=(B,[D,E,z,G])=>{const F=B.parentElement??B,K=Object.keys(E).map((H)=>parseInt(H)).sort((H,M)=>M-H);let L=-1;const T=(H,M,R)=>H.replace(/__pagy_page__/g,M).replace(/__pagy_label__/g,R);if((B.pagyRender=function(){const H=K.find((Q)=>Q<F.clientWidth)||0;if(H===L)return;let M=D.before;const R=E[H.toString()],X=z?.[H.toString()]??R.map((Q)=>Q.toString());R.forEach((Q,J)=>{const $=X[J];let U;if(typeof Q==="number")U=T(D.a,Q.toString(),$);else if(Q==="gap")U=D.gap;else U=T(D.current,Q,$);M+=typeof G==="string"&&Q==1?Z(U,G):U}),M+=D.after,B.innerHTML="",B.insertAdjacentHTML("afterbegin",M),L=H})(),B.classList.contains("pagy-rjs"))j.observe(F)},A=(B,[D,E])=>Y(B,(z)=>[z,D.replace(/__pagy_page__/,z)],E),C=(B,[D,E,z])=>{Y(B,(G)=>{const F=Math.max(Math.ceil(D/parseInt(G)),1).toString(),K=E.replace(/__pagy_page__/,F).replace(/__pagy_limit__/,G);return[F,K]},z)},Y=(B,D,E)=>{const z=B.querySelector("input"),G=B.querySelector("a"),F=z.value,K=function(){if(z.value===F)return;const[L,T,H]=[z.min,z.value,z.max].map((X)=>parseInt(X)||0);if(T<L||T>H){z.value=F,z.select();return}let[M,R]=D(z.value);if(typeof E==="string"&&M==="1")R=Z(R,E);G.href=R,G.click()};["change","focus"].forEach((L)=>z.addEventListener(L,()=>z.select())),z.addEventListener("focusout",K),z.addEventListener("keypress",(L)=>{if(L.key==="Enter")K()})},Z=(B,D)=>B.replace(new RegExp(`[?&]${D}=1\\b(?!&)|\\b${D}=1&`),"");return{version:"9.3.1",init(B){const E=(B instanceof Element?B:document).querySelectorAll("[data-pagy]");for(let z of E)try{const G=Uint8Array.from(atob(z.getAttribute("data-pagy")),(L)=>L.charCodeAt(0)),[F,...K]=JSON.parse(new TextDecoder().decode(G));if(F==="nav")x(z,K);else if(F==="combo")A(z,K);else if(F==="selector")C(z,K);else console.warn("Skipped Pagy.init() for: %o\nUnknown keyword '%s'",z,F)}catch(G){console.warn("Skipped Pagy.init() for: %o\n%s",z,G)}}}})();
2
2
 
3
- //# debugId=709E57C4114925F564756E2164756E21
3
+ //# debugId=57520DFD7BCFB7ED64756E2164756E21
4
4
  //# sourceMappingURL=pagy.min.js.map
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../../src/pagy.ts"],
4
4
  "sourcesContent": [
5
- "type NavArgs = readonly [Tokens, Sequels, null | LabelSequels, string?]\ntype ComboArgs = readonly [string, string?]\ntype SelectorArgs = readonly [number, string, string?]\ntype JsonArgs = ['nav', NavArgs] | ['combo', ComboArgs] | ['selector', SelectorArgs]\n\ninterface Tokens {\n readonly before:string\n readonly a:string\n readonly current:string\n readonly gap:string\n readonly after:string\n}\ninterface Sequels {readonly [width:string]:(string | number)[]}\ninterface LabelSequels {readonly [width:string]:string[]}\ninterface NavElement extends Element {pagyRender():void}\n\nconst Pagy = (() => {\n // The observer instance for responsive navs\n const rjsObserver = new ResizeObserver(\n entries => entries.forEach(e => e.target.querySelectorAll<NavElement>(\".pagy-rjs\")\n .forEach(el => el.pagyRender())));\n // Init the *_nav_js helpers\n const initNav = (el:NavElement, [tokens, sequels, labelSequels, trimParam]:NavArgs) => {\n const container = el.parentElement ?? el;\n const widths = Object.keys(sequels).map(w => parseInt(w)).sort((a, b) => b - a);\n let lastWidth = -1;\n const fillIn = (a:string, page:string, label:string):string =>\n a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);\n (el.pagyRender = function () {\n const width = widths.find(w => w < container.clientWidth) || 0;\n if (width === lastWidth) { return } // no change: abort\n let html = tokens.before; // already trimmed in html\n const series = sequels[width.toString()];\n const labels = labelSequels?.[width.toString()] ?? series.map(l => l.toString());\n series.forEach((item, i) => {\n const label = labels[i];\n let filled;\n if (typeof item === \"number\") {\n filled = fillIn(tokens.a, item.toString(), label);\n } else if (item === \"gap\") {\n filled = tokens.gap;\n } else { // active page\n filled = fillIn(tokens.current, item, label);\n }\n html += (typeof trimParam === \"string\" && item == 1) ? trim(filled, trimParam) : filled;\n });\n html += tokens.after;\n el.innerHTML = \"\";\n el.insertAdjacentHTML(\"afterbegin\", html);\n lastWidth = width;\n })();\n if (el.classList.contains(\"pagy-rjs\")) { rjsObserver.observe(container) }\n };\n\n // Init the *_combo_nav_js helpers\n const initCombo = (el:Element, [url_token, trimParam]:ComboArgs) =>\n initInput(el, inputValue => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], trimParam);\n\n // Init the limit_selector_js helper\n const initSelector = (el:Element, [from, url_token, trimParam]:SelectorArgs) => {\n initInput(el, inputValue => {\n const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();\n const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);\n return [page, url];\n }, trimParam);\n };\n\n // Init the input element\n const initInput = (el:Element, getVars:(v:string) => [string, string], trimParam?:string) => {\n const input = el.querySelector(\"input\") as HTMLInputElement;\n const link = el.querySelector(\"a\") as HTMLAnchorElement;\n const initial = input.value;\n const action = function () {\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 let [page, url] = getVars(input.value); // eslint-disable-line prefer-const\n if (typeof trimParam === \"string\" && page === \"1\") { url = trim(url, trimParam) }\n link.href = url;\n link.click();\n };\n [\"change\", \"focus\"].forEach(e => input.addEventListener(e, () => input.select())); // auto-select\n input.addEventListener(\"focusout\", action); // trigger action\n input.addEventListener(\"keypress\", e => { if (e.key === \"Enter\") { action() } }); // trigger action\n };\n\n // Trim the ${page-param}=1 params in links\n const trim = (a:string, param:string) =>\n a.replace(new RegExp(`[?&]${param}=1\\\\b(?!&)|\\\\b${param}=1&`), \"\");\n\n // Public interface\n return {\n version: \"9.2.0\",\n\n // Scan for elements with a \"data-pagy\" attribute and call their init functions with the decoded args\n init(arg?:Element) {\n const target = arg instanceof Element ? arg : document;\n const elements = target.querySelectorAll(\"[data-pagy]\");\n for (const el of elements) {\n try {\n const uint8array = Uint8Array.from(atob(el.getAttribute(\"data-pagy\") as string), c => c.charCodeAt(0));\n const [keyword, ...args] = JSON.parse((new TextDecoder()).decode(uint8array)) as JsonArgs; // base64-utf8 -> JSON -> Array\n if (keyword === \"nav\") {\n initNav(el as NavElement, args as unknown as NavArgs);\n } else if (keyword === \"combo\") {\n initCombo(el, args as unknown as ComboArgs);\n } else if (keyword === \"selector\") {\n initSelector(el, args as unknown as SelectorArgs);\n } else {\n console.warn(\"Skipped Pagy.init() for: %o\\nUnknown keyword '%s'\", el, keyword);\n }\n } catch (err) { console.warn(\"Skipped Pagy.init() for: %o\\n%s\", el, err) }\n }\n }\n };\n})();\n\nexport default Pagy;\n"
5
+ "type NavArgs = readonly [Tokens, Sequels, null | LabelSequels, string?]\ntype ComboArgs = readonly [string, string?]\ntype SelectorArgs = readonly [number, string, string?]\ntype JsonArgs = ['nav', NavArgs] | ['combo', ComboArgs] | ['selector', SelectorArgs]\n\ninterface Tokens {\n readonly before:string\n readonly a:string\n readonly current:string\n readonly gap:string\n readonly after:string\n}\ninterface Sequels {readonly [width:string]:(string | number)[]}\ninterface LabelSequels {readonly [width:string]:string[]}\ninterface NavElement extends Element {pagyRender():void}\n\nconst Pagy = (() => {\n // The observer instance for responsive navs\n const rjsObserver = new ResizeObserver(\n entries => entries.forEach(e => e.target.querySelectorAll<NavElement>(\".pagy-rjs\")\n .forEach(el => el.pagyRender())));\n // Init the *_nav_js helpers\n const initNav = (el:NavElement, [tokens, sequels, labelSequels, trimParam]:NavArgs) => {\n const container = el.parentElement ?? el;\n const widths = Object.keys(sequels).map(w => parseInt(w)).sort((a, b) => b - a);\n let lastWidth = -1;\n const fillIn = (a:string, page:string, label:string):string =>\n a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);\n (el.pagyRender = function () {\n const width = widths.find(w => w < container.clientWidth) || 0;\n if (width === lastWidth) { return } // no change: abort\n let html = tokens.before; // already trimmed in html\n const series = sequels[width.toString()];\n const labels = labelSequels?.[width.toString()] ?? series.map(l => l.toString());\n series.forEach((item, i) => {\n const label = labels[i];\n let filled;\n if (typeof item === \"number\") {\n filled = fillIn(tokens.a, item.toString(), label);\n } else if (item === \"gap\") {\n filled = tokens.gap;\n } else { // active page\n filled = fillIn(tokens.current, item, label);\n }\n html += (typeof trimParam === \"string\" && item == 1) ? trim(filled, trimParam) : filled;\n });\n html += tokens.after;\n el.innerHTML = \"\";\n el.insertAdjacentHTML(\"afterbegin\", html);\n lastWidth = width;\n })();\n if (el.classList.contains(\"pagy-rjs\")) { rjsObserver.observe(container) }\n };\n\n // Init the *_combo_nav_js helpers\n const initCombo = (el:Element, [url_token, trimParam]:ComboArgs) =>\n initInput(el, inputValue => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], trimParam);\n\n // Init the limit_selector_js helper\n const initSelector = (el:Element, [from, url_token, trimParam]:SelectorArgs) => {\n initInput(el, inputValue => {\n const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();\n const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);\n return [page, url];\n }, trimParam);\n };\n\n // Init the input element\n const initInput = (el:Element, getVars:(v:string) => [string, string], trimParam?:string) => {\n const input = el.querySelector(\"input\") as HTMLInputElement;\n const link = el.querySelector(\"a\") as HTMLAnchorElement;\n const initial = input.value;\n const action = function () {\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 let [page, url] = getVars(input.value); // eslint-disable-line prefer-const\n if (typeof trimParam === \"string\" && page === \"1\") { url = trim(url, trimParam) }\n link.href = url;\n link.click();\n };\n [\"change\", \"focus\"].forEach(e => input.addEventListener(e, () => input.select())); // auto-select\n input.addEventListener(\"focusout\", action); // trigger action\n input.addEventListener(\"keypress\", e => { if (e.key === \"Enter\") { action() } }); // trigger action\n };\n\n // Trim the ${page-param}=1 params in links\n const trim = (a:string, param:string) =>\n a.replace(new RegExp(`[?&]${param}=1\\\\b(?!&)|\\\\b${param}=1&`), \"\");\n\n // Public interface\n return {\n version: \"9.3.1\",\n\n // Scan for elements with a \"data-pagy\" attribute and call their init functions with the decoded args\n init(arg?:Element) {\n const target = arg instanceof Element ? arg : document;\n const elements = target.querySelectorAll(\"[data-pagy]\");\n for (const el of elements) {\n try {\n const uint8array = Uint8Array.from(atob(el.getAttribute(\"data-pagy\") as string), c => c.charCodeAt(0));\n const [keyword, ...args] = JSON.parse((new TextDecoder()).decode(uint8array)) as JsonArgs; // base64-utf8 -> JSON -> Array\n if (keyword === \"nav\") {\n initNav(el as NavElement, args as unknown as NavArgs);\n } else if (keyword === \"combo\") {\n initCombo(el, args as unknown as ComboArgs);\n } else if (keyword === \"selector\") {\n initSelector(el, args as unknown as SelectorArgs);\n } else {\n console.warn(\"Skipped Pagy.init() for: %o\\nUnknown keyword '%s'\", el, keyword);\n }\n } catch (err) { console.warn(\"Skipped Pagy.init() for: %o\\n%s\", el, err) }\n }\n }\n };\n})();\n\nexport default Pagy;\n"
6
6
  ],
7
7
  "mappings": "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",
8
- "debugId": "709E57C4114925F564756E2164756E21",
8
+ "debugId": "57520DFD7BCFB7ED64756E2164756E21",
9
9
  "names": []
10
10
  }
data/javascripts/pagy.mjs CHANGED
@@ -73,7 +73,7 @@ const Pagy = (() => {
73
73
  };
74
74
  const trim = (a, param) => a.replace(new RegExp(`[?&]${param}=1\\b(?!&)|\\b${param}=1&`), "");
75
75
  return {
76
- version: "9.2.0",
76
+ version: "9.3.1",
77
77
  init(arg) {
78
78
  const target = arg instanceof Element ? arg : document;
79
79
  const elements = target.querySelectorAll("[data-pagy]");
@@ -12,7 +12,7 @@ class Pagy # :nodoc:
12
12
 
13
13
  protected
14
14
 
15
- # Setup the calendar variables
15
+ # Set up the calendar variables
16
16
  def assign_unit_vars
17
17
  super
18
18
  @initial = @starting.beginning_of_day
@@ -12,7 +12,7 @@ class Pagy # :nodoc:
12
12
 
13
13
  protected
14
14
 
15
- # Setup the calendar variables
15
+ # Set up the calendar variables
16
16
  def assign_unit_vars
17
17
  super
18
18
  @initial = @starting.beginning_of_month
@@ -19,7 +19,7 @@ class Pagy # :nodoc:
19
19
 
20
20
  protected
21
21
 
22
- # Setup the calendar variables
22
+ # Set up the calendar variables
23
23
  def assign_unit_vars
24
24
  super
25
25
  @initial = @starting.beginning_of_quarter
@@ -10,7 +10,7 @@ class Pagy # :nodoc:
10
10
 
11
11
  protected
12
12
 
13
- # Setup the calendar variables
13
+ # Set up the calendar variables
14
14
  def assign_unit_vars
15
15
  super
16
16
  @initial = @starting.beginning_of_week
@@ -12,7 +12,7 @@ class Pagy # :nodoc:
12
12
 
13
13
  protected
14
14
 
15
- # Setup the calendar variables
15
+ # Set up the calendar variables
16
16
  def assign_unit_vars
17
17
  super
18
18
  @initial = @starting.beginning_of_year
@@ -7,8 +7,8 @@ class Pagy
7
7
  class ActiveRecord < Keyset
8
8
  protected
9
9
 
10
- # Get the keyset attributes of the record
11
- def latest_from(latest_record) = latest_record.slice(*@keyset.keys)
10
+ # Get the keyset attributes from the record
11
+ def keyset_attributes_from(record) = record.slice(*@keyset.keys)
12
12
 
13
13
  # Extract the keyset from the set
14
14
  def extract_keyset
@@ -7,8 +7,8 @@ class Pagy
7
7
  class Sequel < Keyset
8
8
  protected
9
9
 
10
- # Get the keyset attributes of the latest record
11
- def latest_from(latest_record) = latest_record.to_hash.slice(*@keyset.keys)
10
+ # Get the keyset attributes from the record
11
+ def keyset_attributes_from(record) = record.to_hash.slice(*@keyset.keys)
12
12
 
13
13
  # Extract the keyset from the set
14
14
  def extract_keyset
data/lib/pagy/keyset.rb CHANGED
@@ -43,7 +43,7 @@ class Pagy
43
43
  return unless @page
44
44
 
45
45
  latest = JSON.parse(B64.urlsafe_decode(@page)).transform_keys(&:to_sym)
46
- @latest = @vars[:typecast_latest]&.(latest) || typecast_latest(latest)
46
+ @latest = typecast_latest(latest)
47
47
  raise InternalError, 'page and keyset are not consistent' \
48
48
  unless @latest.keys == @keyset.keys
49
49
  end
@@ -53,44 +53,58 @@ class Pagy
53
53
  records
54
54
  return unless @more
55
55
 
56
- @next ||= B64.urlsafe_encode(latest_from(@records.last).to_json)
56
+ @next ||= begin
57
+ hash = keyset_attributes_from(@records.last)
58
+ json = @vars[:jsonify_keyset_attributes]&.(hash) || hash.to_json
59
+ B64.urlsafe_encode(json)
60
+ end
57
61
  end
58
62
 
59
63
  # Fetch the array of records for the current page
60
64
  def records
61
65
  @records ||= begin
62
- @set = apply_select if select?
63
- if @latest
64
- # :nocov:
65
- @set = @vars[:after_latest]&.(@set, @latest) || # deprecated
66
- # :nocov:
67
- @vars[:filter_newest]&.(@set, @latest, @keyset) ||
68
- filter_newest
69
- end
70
- records = @set.limit(@limit + 1).to_a
71
- @more = records.size > @limit && !records.pop.nil?
72
- records
73
- end
66
+ @set = apply_select if select?
67
+ if @latest
68
+ # :nocov:
69
+ @set = @vars[:after_latest]&.(@set, @latest) || # deprecated
70
+ # :nocov:
71
+ @vars[:filter_newest]&.(@set, @latest, @keyset) ||
72
+ filter_newest
73
+ end
74
+ records = @set.limit(@limit + 1).to_a
75
+ @more = records.size > @limit && !records.pop.nil?
76
+ records
77
+ end
74
78
  end
75
79
 
76
80
  protected
77
81
 
78
- # Prepare the literal query to filter the newest records
82
+ # Prepare the literal query string (complete with the placeholders for value interpolation)
83
+ # used to filter the newest records.
84
+ # For example:
85
+ # With a set like Pet.order(animal: :asc, name: :desc, id: :asc) it returns the following string:
86
+ # ( "pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id ) OR
87
+ # ( "pets"."animal" = :animal AND "pets"."name" < :name ) OR
88
+ # ( "pets"."animal" > :animal )
89
+ # When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
90
+ # with a set like Pet.order(:animal, :name, :id) it returns the following string:
91
+ # ( "pets"."animal", "pets"."name", "pets"."id" ) > ( :animal, :name, :id )
79
92
  def filter_newest_query
80
93
  operator = { asc: '>', desc: '<' }
81
94
  directions = @keyset.values
95
+ table = @set.model.table_name
96
+ name = @keyset.to_h { |column| [column, %("#{table}"."#{column}")] }
82
97
  if @vars[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
83
- columns = @keyset.keys
84
- placeholders = columns.map { |column| ":#{column}" }.join(', ')
85
- "( #{columns.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )"
98
+ placeholders = @keyset.keys.map { |column| ":#{column}" }.join(', ')
99
+ "( #{name.values.join(', ')} ) #{operator[directions.first]} ( #{placeholders} )"
86
100
  else
87
101
  keyset = @keyset.to_a
88
102
  where = []
89
103
  until keyset.empty?
90
104
  last_column, last_direction = keyset.pop
91
105
  query = +'( '
92
- query << (keyset.map { |column, _d| "#{column} = :#{column}" } \
93
- << "#{last_column} #{operator[last_direction]} :#{last_column}").join(' AND ')
106
+ query << (keyset.map { |column, _d| "#{name[column]} = :#{column}" } \
107
+ << "#{name[last_column]} #{operator[last_direction]} :#{last_column}").join(' AND ')
94
108
  query << ' )'
95
109
  where << query
96
110
  end
data/lib/pagy.rb CHANGED
@@ -6,7 +6,7 @@ require_relative 'pagy/shared_methods'
6
6
 
7
7
  # Top superclass: it should define only what's common to all the subclasses
8
8
  class Pagy
9
- VERSION = '9.2.0'
9
+ VERSION = '9.3.1'
10
10
 
11
11
  # Core default: constant for easy access, but mutable for customizable defaults
12
12
  DEFAULT = { count_args: [:all], # rubocop:disable Style/MutableConstant