pagy 43.1.8 → 43.2.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.
- checksums.yaml +4 -4
- data/apps/calendar.ru +1 -1
- data/apps/demo.ru +1 -1
- data/apps/keynav+root_key.ru +1 -1
- data/apps/keynav.ru +1 -1
- data/apps/keyset.ru +1 -1
- data/apps/keyset_sequel.ru +1 -1
- data/apps/rails.ru +3 -1
- data/apps/rails_page_segment.rb +71 -0
- data/apps/repro.ru +3 -2
- data/bin/pagy +1 -1
- data/config/pagy.rb +1 -1
- data/javascripts/pagy.js +11 -9
- data/javascripts/pagy.js.map +3 -3
- data/javascripts/pagy.min.js +2 -1
- data/javascripts/pagy.mjs +10 -8
- data/javascripts/wand.js +15 -9
- data/lib/pagy/classes/calendar/calendar.rb +2 -2
- data/lib/pagy/classes/calendar/unit.rb +5 -9
- data/lib/pagy/classes/keyset/keynav.rb +4 -3
- data/lib/pagy/classes/keyset/keyset.rb +34 -12
- data/lib/pagy/classes/offset/countish.rb +17 -0
- data/lib/pagy/classes/offset/countless.rb +2 -2
- data/lib/pagy/classes/offset/offset.rb +2 -1
- data/lib/pagy/classes/offset/search.rb +1 -1
- data/lib/pagy/classes/request.rb +5 -1
- data/lib/pagy/modules/abilities/countable.rb +23 -0
- data/lib/pagy/modules/abilities/linkable.rb +19 -9
- data/lib/pagy/toolbox/helpers/data_hash.rb +14 -14
- data/lib/pagy/toolbox/helpers/limit_tag_js.rb +3 -3
- data/lib/pagy/toolbox/helpers/loader.rb +3 -0
- data/lib/pagy/toolbox/helpers/page_url.rb +6 -8
- data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +1 -1
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +1 -1
- data/lib/pagy/toolbox/paginators/countish.rb +37 -0
- data/lib/pagy/toolbox/paginators/countless.rb +1 -1
- data/lib/pagy/toolbox/paginators/method.rb +6 -5
- data/lib/pagy/toolbox/paginators/offset.rb +3 -15
- data/lib/pagy.rb +3 -5
- metadata +6 -6
- data/lib/pagy/classes/keyset/active_record.rb +0 -11
- data/lib/pagy/classes/keyset/keynav/active_record.rb +0 -13
- data/lib/pagy/classes/keyset/keynav/sequel.rb +0 -13
- data/lib/pagy/classes/keyset/sequel.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48e5f5812c5ef4b7f2a655990b494066bfbc0730635463d140d1e2bdd636523c
|
|
4
|
+
data.tar.gz: b68fde5b5b43bb8014ea9664a91703c7453dca9c7fd23fa3bebce40b9d92678d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea47f8b51fd9a8d67df167f37fa6817ae7ec5dc1a509ae973f6b66af6e6e4aba0326d727adf6fcdc7e2a0ab053ce3872cc28e7c8ad4b62f4e08347f15049402e
|
|
7
|
+
data.tar.gz: 33a577b3e6fe5b9b9124bdccbcf5cb4ec8082e9cdf3122ccac941fe2f87b126084032ea0b849b9587ac62a8d547e690c8d571e46c376dc7e396265092b2db148
|
data/apps/calendar.ru
CHANGED
data/apps/demo.ru
CHANGED
data/apps/keynav+root_key.ru
CHANGED
data/apps/keynav.ru
CHANGED
data/apps/keyset.ru
CHANGED
data/apps/keyset_sequel.ru
CHANGED
data/apps/rails.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.2.1'
|
|
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")
|
|
@@ -35,6 +35,7 @@ end
|
|
|
35
35
|
# require 'rails/all' # too much stuff
|
|
36
36
|
require 'action_controller/railtie'
|
|
37
37
|
require 'active_record'
|
|
38
|
+
# require_relative 'rails_page_segment' # Uncomment to test the rails_page_segment.rb override
|
|
38
39
|
|
|
39
40
|
OUTPUT = Rails.env.showcase? ? IO::NULL : $stdout
|
|
40
41
|
|
|
@@ -49,6 +50,7 @@ class PagyRails < Rails::Application # :nodoc:
|
|
|
49
50
|
|
|
50
51
|
routes.draw do
|
|
51
52
|
root to: 'comments#index'
|
|
53
|
+
# get '/comments(/:page)', to: 'comments#index' # Uncomment to test the rails_page_segment.rb override
|
|
52
54
|
get '/javascripts/:file', to: 'pagy#javascripts', file: /.*/
|
|
53
55
|
end
|
|
54
56
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ################# IMPORTANT WARNING #################
|
|
4
|
+
# Use this override ONLY if you strictly need to support the page param as a dynamic segment.
|
|
5
|
+
# (e.g. get '/comments(/:page)', to: 'comments#index').
|
|
6
|
+
#
|
|
7
|
+
# This setup forces Pagy to use the Rails `url_for` method, which is significantly
|
|
8
|
+
# slower (~20x) than Pagy's native URL generation.
|
|
9
|
+
# #####################################################
|
|
10
|
+
|
|
11
|
+
# 1. CONSTANT REDEFINITION
|
|
12
|
+
# We must replace Pagy's internal tokens.
|
|
13
|
+
# Why: Pagy marks its default tokens as "Escaped" to allow spaces into the URL template for safe interpolation.
|
|
14
|
+
# Since we are handing control back to Rails' standard QueryUtils, we need simple, URL-safe placeholders
|
|
15
|
+
# that Rails won't need to escape, to avoid mismatches between non-escaped and escaped tokens.
|
|
16
|
+
Pagy.send(:remove_const, :PAGE_TOKEN)
|
|
17
|
+
Pagy.send(:remove_const, :LIMIT_TOKEN)
|
|
18
|
+
|
|
19
|
+
Pagy::PAGE_TOKEN = '___PAGY_PAGE___'
|
|
20
|
+
Pagy::LIMIT_TOKEN = '___PAGY_LIMIT___'
|
|
21
|
+
|
|
22
|
+
require 'pagy/toolbox/paginators/method'
|
|
23
|
+
require 'pagy/modules/abilities/linkable'
|
|
24
|
+
|
|
25
|
+
class Pagy
|
|
26
|
+
# 2. REQUEST PARAMETERS
|
|
27
|
+
# Why: Pagy defaults to Rack::Request to be framework agnostic.
|
|
28
|
+
# To support dynamic segments (which are routing concepts, not query params),
|
|
29
|
+
# we must switch to the Rails `request.params` method which includes path parameters.
|
|
30
|
+
module RequestOverride
|
|
31
|
+
def get_params(request)
|
|
32
|
+
request.params
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
Request.prepend RequestOverride
|
|
36
|
+
|
|
37
|
+
# 3. CONTEXT INJECTION
|
|
38
|
+
# Why: The Pagy object needs access to the Controller instance to call `url_for`.
|
|
39
|
+
# We intercept the `pagy` method in the controller to inject `self` (the controller)
|
|
40
|
+
# into the Pagy instance as `@context`.
|
|
41
|
+
module MethodOverride
|
|
42
|
+
def pagy(...)
|
|
43
|
+
super.tap do |result|
|
|
44
|
+
# result is [pagy_object, records]
|
|
45
|
+
# We inject the controller (self) directly into the pagy object
|
|
46
|
+
result[0].instance_variable_set(:@context, self)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
Method.prepend MethodOverride
|
|
51
|
+
|
|
52
|
+
# 4. URL GENERATION
|
|
53
|
+
# Why: We override Pagy's optimized string interpolation with Rails' `url_for`.
|
|
54
|
+
# We combine the current Rails parameters with Pagy's options and generate the URL.
|
|
55
|
+
module LinkableOverride
|
|
56
|
+
def compose_url(absolute, _path, params, fragment)
|
|
57
|
+
params[:anchor] = fragment if fragment
|
|
58
|
+
params[:only_path] = !absolute
|
|
59
|
+
|
|
60
|
+
# Call the Rails url_for method from the controller context
|
|
61
|
+
@context.url_for(params)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
Linkable.prepend LinkableOverride
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# USAGE with rails.ru
|
|
68
|
+
|
|
69
|
+
# Search and uncomment the following lines in the rails.ru app:
|
|
70
|
+
# require_relative 'rails_page_segment' # Uncomment to test the rails_page_segment.rb override
|
|
71
|
+
# get '/comments(/:page)', to: 'comments#index' # Uncomment to test the rails_page_segment.rb override
|
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.2.1'
|
|
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")
|
|
@@ -55,8 +55,9 @@ class PagyRepro < Sinatra::Base
|
|
|
55
55
|
# Edit this action as needed
|
|
56
56
|
get '/' do
|
|
57
57
|
collection = MockCollection.new
|
|
58
|
-
@pagy, @records = pagy(collection)
|
|
58
|
+
@pagy, @records = pagy(collection) # simplest form
|
|
59
59
|
# @pagy, @records = pagy(:offset, collection, limit: 7, client_max_limit: 30)
|
|
60
|
+
# @pagy, @records = pagy(:countish, collection, ttl: 20)
|
|
60
61
|
# @pagy, @records = pagy(:countless, collection)
|
|
61
62
|
# @pagy, @records = pagy(Array(1..1000))
|
|
62
63
|
# response.headers.merge!(@pagy.headers_hash)
|
data/bin/pagy
CHANGED
data/config/pagy.rb
CHANGED
data/javascripts/pagy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// pagy.ts
|
|
2
2
|
window.Pagy = (() => {
|
|
3
|
-
const storageSupport = "sessionStorage" in window && "BroadcastChannel" in window
|
|
3
|
+
const storageSupport = "sessionStorage" in window && "BroadcastChannel" in window;
|
|
4
4
|
let pagy = "pagy", storage, sync, tabId;
|
|
5
5
|
if (storageSupport) {
|
|
6
6
|
storage = sessionStorage;
|
|
@@ -67,6 +67,7 @@ window.Pagy = (() => {
|
|
|
67
67
|
};
|
|
68
68
|
const buildNavJs = (nav, [
|
|
69
69
|
[before, anchor, current, gap, after],
|
|
70
|
+
pageToken,
|
|
70
71
|
[widths, series, labels],
|
|
71
72
|
keynavArgs
|
|
72
73
|
]) => {
|
|
@@ -79,7 +80,7 @@ window.Pagy = (() => {
|
|
|
79
80
|
}
|
|
80
81
|
let html = before;
|
|
81
82
|
series[index].forEach((item, i) => {
|
|
82
|
-
html += item == "gap" ? gap : (typeof item == "number" ? anchor.replace(
|
|
83
|
+
html += item == "gap" ? gap : (typeof item == "number" ? anchor.replace(pageToken, item) : current).replace("L<", labels?.[index][i] ?? item + "<");
|
|
83
84
|
});
|
|
84
85
|
html += after;
|
|
85
86
|
nav.innerHTML = "";
|
|
@@ -93,13 +94,13 @@ window.Pagy = (() => {
|
|
|
93
94
|
rjsObserver.observe(parent);
|
|
94
95
|
}
|
|
95
96
|
};
|
|
96
|
-
const initInputNavJs = async (nav, [url_token, keynavArgs]) => {
|
|
97
|
+
const initInputNavJs = async (nav, [url_token, pageToken, keynavArgs]) => {
|
|
97
98
|
const augment = keynavArgs && storageSupport ? await augmentKeynav(nav, keynavArgs) : (page) => page;
|
|
98
|
-
initInput(nav, (inputValue) => url_token.replace(
|
|
99
|
+
initInput(nav, (inputValue) => url_token.replace(pageToken, augment(inputValue)));
|
|
99
100
|
};
|
|
100
|
-
const initLimitTagJs = (span, [from, url_token]) => {
|
|
101
|
+
const initLimitTagJs = (span, [from, url_token, page_token, limitToken]) => {
|
|
101
102
|
initInput(span, (inputValue) => {
|
|
102
|
-
return url_token.replace(
|
|
103
|
+
return url_token.replace(page_token, Math.max(Math.ceil(from / parseInt(inputValue)), 1)).replace(limitToken, inputValue);
|
|
103
104
|
});
|
|
104
105
|
};
|
|
105
106
|
const initInput = (element, getUrl) => {
|
|
@@ -125,7 +126,7 @@ window.Pagy = (() => {
|
|
|
125
126
|
});
|
|
126
127
|
};
|
|
127
128
|
return {
|
|
128
|
-
version: "43.1
|
|
129
|
+
version: "43.2.1",
|
|
129
130
|
init(arg) {
|
|
130
131
|
const target = arg instanceof HTMLElement ? arg : document, elements = target.querySelectorAll("[data-pagy]");
|
|
131
132
|
for (const element of elements) {
|
|
@@ -141,12 +142,13 @@ window.Pagy = (() => {
|
|
|
141
142
|
initLimitTagJs(element, args);
|
|
142
143
|
}
|
|
143
144
|
} catch (err) {
|
|
144
|
-
console.warn(
|
|
145
|
+
console.warn(`Pagy.init: %o
|
|
146
|
+
%s`, element, err);
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
151
|
})();
|
|
150
152
|
|
|
151
|
-
//# debugId=
|
|
153
|
+
//# debugId=F3282E394DA790A164756E2164756E21
|
|
152
154
|
//# 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 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.8\",\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, pageToken: string, NavJsSeries, KeynavArgs?]\ntype NavJsSeries = readonly [widths: number[],\n series: (string | number)[][],\n labels: string[][] | null]\ntype InputNavJsArgs = readonly [urlToken: string,\n pageToken: string,\n KeynavArgs?]\ntype LimitTagJsArgs = readonly [from: number,\n urlToken: string,\n pageToken: string,\n limitToken: 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 // 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], pageToken,\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(pageToken, 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, pageToken, keynavArgs]:InputNavJsArgs) => {\n const augment = keynavArgs && storageSupport\n ? await augmentKeynav(nav, keynavArgs)\n : (page: string) => page;\n initInput(nav, inputValue => url_token.replace(pageToken, augment(inputValue)));\n };\n\n // Init the limit_tag_js helper\n const initLimitTagJs = (span:HTMLSpanElement, [from, url_token, page_token, limitToken]: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(page_token, Math.max(Math.ceil(from / parseInt(inputValue)), 1))\n .replace(limitToken, 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.2.1\",\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": "F3282E394DA790A164756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
data/javascripts/pagy.min.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
window.Pagy=(()=>{
|
|
1
|
+
window.Pagy=(()=>{let B="sessionStorage"in window&&"BroadcastChannel"in window,L="pagy",Z,O,W;if(B)Z=sessionStorage,O=new BroadcastChannel(L),W=Date.now(),O.addEventListener("message",(q)=>{if(q.data.from){let z=Z.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==W)Z.setItem(q.data.key,q.data.str)}});let P=new ResizeObserver((q)=>q.forEach((z)=>{z.target.querySelectorAll(".pagy-rjs").forEach((C)=>C.render())})),V=(q)=>btoa(String.fromCharCode(...new TextEncoder().encode(q))).replace(/[+/=]/g,(z)=>z=="+"?"-":z=="/"?"_":""),_=(q)=>new TextDecoder().decode(Uint8Array.from(atob(q),(z)=>z.charCodeAt(0))),S=()=>Math.floor(Math.random()*46656).toString(36),J=async(q,[z,C,F,H,G])=>{let Q,X=document.cookie.split(/;\s+/).find((M)=>M.startsWith(L+"="))?.split("=")[1]??S();if(document.cookie=L+"="+X,z&&!(z in Z)){if(O.postMessage({from:W,key:z}),await new Promise((M)=>setTimeout(()=>M(""),100)),!(z in Z))Q=(M)=>M+"+"+H}if(!Q){if(!z)do z=S();while(z in Z);let M=Z.getItem(z),Y=M?JSON.parse(M):[void 0];if(G)Y.splice(...G),Z.setItem(z,JSON.stringify(Y));Q=($)=>{let R=parseInt($);return V(JSON.stringify([X,z,R,Y.length,Y[R-1],Y[R]]))}}let D=C?`${C}%5B${F}%5D`:F,E=new RegExp(`(?<=\\?.*)(\\b${D}=)(\\d+)`);for(let M of q.querySelectorAll("a[href]"))M.href=M.href.replace(E,(Y,$,R)=>`${$}${Q(R)}`);return Q},x=(q,[[z,C,F,H,G],Q,[X,D,E],M])=>{let Y=q.parentElement,$=-1;if((q.render=()=>{let R=X.findIndex((j)=>j<Y.clientWidth);if(X[R]===$)return;let U=z;if(D[R].forEach((j,I)=>{U+=j=="gap"?H:(typeof j=="number"?C.replace(Q,j):F).replace("L<",E?.[R][I]??j+"<")}),U+=G,q.innerHTML="",q.insertAdjacentHTML("afterbegin",U),$=X[R],M&&B)J(q,M)})(),q.classList.contains(L+"-rjs"))P.observe(Y)},T=async(q,[z,C,F])=>{let H=F&&B?await J(q,F):(G)=>G;N(q,(G)=>z.replace(C,H(G)))},A=(q,[z,C,F,H])=>{N(q,(G)=>{return C.replace(F,Math.max(Math.ceil(z/parseInt(G)),1)).replace(H,G)})},N=(q,z)=>{let C=q.querySelector("input"),F=q.querySelector("a"),H=C.value,G=()=>{if(C.value===H)return;let[Q,X,D]=[C.min,C.value,C.max].map((E)=>parseInt(E)||0);if(X<Q||X>D){C.value=H,C.select();return}F.href=z(C.value),F.click()};C.addEventListener("focus",()=>C.select()),C.addEventListener("focusout",G),C.addEventListener("keypress",(Q)=>{if(Q.key=="Enter")G()})};return{version:"43.2.1",init(q){let z=q instanceof HTMLElement?q:document,C=z.querySelectorAll("[data-pagy]");for(let F of C)try{let[H,...G]=JSON.parse(_(F.getAttribute("data-pagy")));if(H=="k")J(F,...G);else if(H=="snj")x(F,G);else if(H=="inj")T(F,G);else if(H=="ltj")A(F,G)}catch(H){console.warn(`Pagy.init: %o
|
|
2
|
+
%s`,F,H)}}}})();
|
data/javascripts/pagy.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const Pagy = (() => {
|
|
2
|
-
const storageSupport = "sessionStorage" in window && "BroadcastChannel" in window
|
|
2
|
+
const storageSupport = "sessionStorage" in window && "BroadcastChannel" in window;
|
|
3
3
|
let pagy = "pagy", storage, sync, tabId;
|
|
4
4
|
if (storageSupport) {
|
|
5
5
|
storage = sessionStorage;
|
|
@@ -66,6 +66,7 @@ const Pagy = (() => {
|
|
|
66
66
|
};
|
|
67
67
|
const buildNavJs = (nav, [
|
|
68
68
|
[before, anchor, current, gap, after],
|
|
69
|
+
pageToken,
|
|
69
70
|
[widths, series, labels],
|
|
70
71
|
keynavArgs
|
|
71
72
|
]) => {
|
|
@@ -78,7 +79,7 @@ const Pagy = (() => {
|
|
|
78
79
|
}
|
|
79
80
|
let html = before;
|
|
80
81
|
series[index].forEach((item, i) => {
|
|
81
|
-
html += item == "gap" ? gap : (typeof item == "number" ? anchor.replace(
|
|
82
|
+
html += item == "gap" ? gap : (typeof item == "number" ? anchor.replace(pageToken, item) : current).replace("L<", labels?.[index][i] ?? item + "<");
|
|
82
83
|
});
|
|
83
84
|
html += after;
|
|
84
85
|
nav.innerHTML = "";
|
|
@@ -92,13 +93,13 @@ const Pagy = (() => {
|
|
|
92
93
|
rjsObserver.observe(parent);
|
|
93
94
|
}
|
|
94
95
|
};
|
|
95
|
-
const initInputNavJs = async (nav, [url_token, keynavArgs]) => {
|
|
96
|
+
const initInputNavJs = async (nav, [url_token, pageToken, keynavArgs]) => {
|
|
96
97
|
const augment = keynavArgs && storageSupport ? await augmentKeynav(nav, keynavArgs) : (page) => page;
|
|
97
|
-
initInput(nav, (inputValue) => url_token.replace(
|
|
98
|
+
initInput(nav, (inputValue) => url_token.replace(pageToken, augment(inputValue)));
|
|
98
99
|
};
|
|
99
|
-
const initLimitTagJs = (span, [from, url_token]) => {
|
|
100
|
+
const initLimitTagJs = (span, [from, url_token, page_token, limitToken]) => {
|
|
100
101
|
initInput(span, (inputValue) => {
|
|
101
|
-
return url_token.replace(
|
|
102
|
+
return url_token.replace(page_token, Math.max(Math.ceil(from / parseInt(inputValue)), 1)).replace(limitToken, inputValue);
|
|
102
103
|
});
|
|
103
104
|
};
|
|
104
105
|
const initInput = (element, getUrl) => {
|
|
@@ -124,7 +125,7 @@ const Pagy = (() => {
|
|
|
124
125
|
});
|
|
125
126
|
};
|
|
126
127
|
return {
|
|
127
|
-
version: "43.1
|
|
128
|
+
version: "43.2.1",
|
|
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) {
|
|
@@ -140,7 +141,8 @@ const Pagy = (() => {
|
|
|
140
141
|
initLimitTagJs(element, args);
|
|
141
142
|
}
|
|
142
143
|
} catch (err) {
|
|
143
|
-
console.warn(
|
|
144
|
+
console.warn(`Pagy.init: %o
|
|
145
|
+
%s`, element, err);
|
|
144
146
|
}
|
|
145
147
|
}
|
|
146
148
|
}
|
data/javascripts/wand.js
CHANGED
|
@@ -62,7 +62,7 @@ const fontIsolation = (async () => {
|
|
|
62
62
|
let cssText = await response.text();
|
|
63
63
|
fontMappings.forEach((mapping) => {
|
|
64
64
|
const regex = new RegExp(`(font-family:\\s*['"]?)${mapping.original}(['"]?;?)`, "g");
|
|
65
|
-
cssText = cssText.replace(regex,
|
|
65
|
+
cssText = cssText.replace(regex, `$1${mapping.custom}$2`);
|
|
66
66
|
});
|
|
67
67
|
classMappings.forEach((mapping) => {
|
|
68
68
|
const regex = new RegExp(`\\.(${mapping.original})(?=[\\s\\.{,:])`, "g");
|
|
@@ -70,7 +70,9 @@ const fontIsolation = (async () => {
|
|
|
70
70
|
});
|
|
71
71
|
const fontFaceRegex = /(?:\/\*[\s\S]*?\*\/[\s\n]*)?@font-face\s*\{[^}]*}/g;
|
|
72
72
|
const fontFaceMatches = cssText.match(fontFaceRegex);
|
|
73
|
-
result.fontFaceRules = fontFaceMatches ? fontFaceMatches.join(
|
|
73
|
+
result.fontFaceRules = fontFaceMatches ? fontFaceMatches.join(`
|
|
74
|
+
|
|
75
|
+
`) : "";
|
|
74
76
|
result.componentCss = cssText.replace(fontFaceRegex, "").trim();
|
|
75
77
|
} catch (error) {
|
|
76
78
|
console.error("Failed to process Google Font CSS:", fontCssUrl, error);
|
|
@@ -79,8 +81,10 @@ const fontIsolation = (async () => {
|
|
|
79
81
|
return result;
|
|
80
82
|
}
|
|
81
83
|
const processedResults = await Promise.all(fontDefinitions.map((def) => processGoogleFontCSS(def.url, def.fontMappings, def.classMappings)));
|
|
82
|
-
const allFontFaceRules = processedResults.map((r) => r.fontFaceRules).join(
|
|
83
|
-
|
|
84
|
+
const allFontFaceRules = processedResults.map((r) => r.fontFaceRules).join(`
|
|
85
|
+
`);
|
|
86
|
+
const allComponentCss = processedResults.map((r) => r.componentCss).join(`
|
|
87
|
+
`);
|
|
84
88
|
if (allFontFaceRules) {
|
|
85
89
|
const styleEl = document.createElement("style");
|
|
86
90
|
styleEl.id = "pagy-wand-font-css";
|
|
@@ -696,11 +700,11 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
696
700
|
</dd>
|
|
697
701
|
</dl>
|
|
698
702
|
<h4>Customizing</h4>
|
|
699
|
-
<p
|
|
703
|
+
<p>• You can change Pagy's styling quite radically, by just setting a few CSS Custom Properties:
|
|
700
704
|
the <code><i>pagy.css</i></code> or <code><i>pagy-tailwind.css</i></code> calculates all the other metrics.</p>
|
|
701
|
-
<p
|
|
705
|
+
<p>• Pick a Presets as a starting point, customize it with the controls,
|
|
702
706
|
and copy/paste the CSS Override in your Stylesheet.</p>
|
|
703
|
-
<p
|
|
707
|
+
<p>• Add further customization to the <code>.pagy</code> CSS Override, or override the calculated properties
|
|
704
708
|
for full control over the final style.</p>
|
|
705
709
|
<p><b>Important</b>: Do not link the Pagy CSS file. Copy its customized content in your CSS,
|
|
706
710
|
to avoid unwanted cosmetic changes that could happen on update.</p>
|
|
@@ -973,10 +977,12 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
973
977
|
applyCSS(presetCSS);
|
|
974
978
|
}
|
|
975
979
|
function updateOverride() {
|
|
976
|
-
let override = `.pagy {
|
|
980
|
+
let override = `.pagy {
|
|
981
|
+
`;
|
|
977
982
|
Object.values(controls).forEach((c) => {
|
|
978
983
|
if (c.name !== "") {
|
|
979
|
-
override += ` ${c.name}: ${c.input.value}${c.unit}
|
|
984
|
+
override += ` ${c.name}: ${c.input.value}${c.unit};
|
|
985
|
+
`;
|
|
980
986
|
}
|
|
981
987
|
});
|
|
982
988
|
override += "}";
|
|
@@ -21,17 +21,17 @@ class Pagy
|
|
|
21
21
|
UNITS = %i[year quarter month week day] # rubocop:disable Style/MutableConstant
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
|
-
# :nocov:
|
|
25
24
|
# Localize with rails-i18n in any env
|
|
26
25
|
def localize_with_rails_i18n_gem(*locales)
|
|
27
26
|
Unit.prepend(Module.new { def localize(...) = ::I18n.localize(...) })
|
|
27
|
+
# :nocov:
|
|
28
28
|
raise RailsI18nLoadError, "Pagy: The gem 'rails-i18n' must be installed if you don't use Rails" \
|
|
29
29
|
unless (path = Gem.loaded_specs['rails-i18n']&.full_gem_path)
|
|
30
30
|
|
|
31
|
+
# :nocov:
|
|
31
32
|
path = Pathname.new(path)
|
|
32
33
|
::I18n.load_path += locales.map { |locale| path.join("rails/locale/#{locale}.yml") }
|
|
33
34
|
end
|
|
34
|
-
# :nocov:
|
|
35
35
|
|
|
36
36
|
private
|
|
37
37
|
|
|
@@ -17,7 +17,7 @@ class Pagy
|
|
|
17
17
|
include Rangeable
|
|
18
18
|
include Shiftable
|
|
19
19
|
|
|
20
|
-
def initialize(**)
|
|
20
|
+
def initialize(**)
|
|
21
21
|
assign_options(**)
|
|
22
22
|
assign_and_check(page: 1)
|
|
23
23
|
assign_unit_variables
|
|
@@ -50,14 +50,7 @@ class Pagy
|
|
|
50
50
|
unless time.between?(@initial, fit_final)
|
|
51
51
|
raise RangeError.new(self, :time, "between #{@initial} and #{fit_final}", time) unless options[:fit_time]
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
fit_time = @initial
|
|
55
|
-
ordinal = 'first'
|
|
56
|
-
else
|
|
57
|
-
fit_time = fit_final
|
|
58
|
-
ordinal = 'last'
|
|
59
|
-
end
|
|
60
|
-
warn "Pagy::Calendar#page_at: Rescued #{time} out of range by returning the #{ordinal} page."
|
|
53
|
+
fit_time = time < @final ? @initial : fit_final
|
|
61
54
|
end
|
|
62
55
|
offset = page_offset_at(fit_time) # offset starts from 0
|
|
63
56
|
@order == :asc ? offset + 1 : @last - offset
|
|
@@ -75,7 +68,10 @@ class Pagy
|
|
|
75
68
|
# Apply the strftime format to the time.
|
|
76
69
|
# Localization other than :en, require the rails-I18n gem.
|
|
77
70
|
def localize(time, **options)
|
|
71
|
+
# Impossible to "unprepend" the rails-i18n after it runs localize_with_rails_i18n_gem in test
|
|
72
|
+
# :nocov:
|
|
78
73
|
time.strftime(options[:format])
|
|
74
|
+
# :nocov:
|
|
79
75
|
end
|
|
80
76
|
|
|
81
77
|
# The number of time units to offset from the @initial time, in order to get the ordered starting time for the page.
|
|
@@ -8,9 +8,10 @@ class Pagy
|
|
|
8
8
|
PRIOR_PREFIX = 'prior_'
|
|
9
9
|
PAGE_PREFIX = 'page_'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# Define empty subclasses to allow specific typing without triggering autoload.
|
|
12
|
+
# The .new factory in Keyset will handle mixing in the adapter logic from Pagy::Keyset::Adapters.
|
|
13
|
+
class ActiveRecord < self; end
|
|
14
|
+
class Sequel < self; end
|
|
14
15
|
|
|
15
16
|
# Finalize the instance variables needed for the UI
|
|
16
17
|
def initialize(set, **)
|
|
@@ -6,31 +6,53 @@ require_relative '../../modules/b64'
|
|
|
6
6
|
class Pagy
|
|
7
7
|
# Implement wicked-fast keyset pagination for big data
|
|
8
8
|
class Keyset < Pagy
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
# Autoload adapters: files are loaded only when const_get accesses them
|
|
10
|
+
module Adapters
|
|
11
|
+
path = Pathname.new(__dir__)
|
|
12
|
+
autoload :ActiveRecord, path.join('adapters/active_record')
|
|
13
|
+
autoload :Sequel, path.join('adapters/sequel')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
autoload :Keynav, Pathname.new(__dir__).join('keynav')
|
|
17
|
+
|
|
18
|
+
# Define empty subclasses to allow specific typing without triggering autoload
|
|
19
|
+
class ActiveRecord < self; end
|
|
20
|
+
class Sequel < self; end
|
|
13
21
|
|
|
14
22
|
class TypeError < ::TypeError; end
|
|
15
23
|
|
|
16
|
-
#
|
|
24
|
+
# Factory method: detects the set type, configures the subclass, and instantiates
|
|
17
25
|
def self.new(set, **)
|
|
18
|
-
#
|
|
19
|
-
if /::(?:ActiveRecord|Sequel)$/.match?(name)
|
|
26
|
+
# 1. Handle direct subclass usage (e.g. Pagy::Keyset::ActiveRecord.new)
|
|
27
|
+
if /::(?:ActiveRecord|Sequel)$/.match?(name)
|
|
28
|
+
# Ensure the adapter is mixed in (lazy load)
|
|
29
|
+
mix_in_adapter(name.split('::').last)
|
|
20
30
|
return allocate.tap { |instance| instance.send(:initialize, set, **) }
|
|
21
31
|
end
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
# 2. Handle Factory usage (Pagy::Keyset.new)
|
|
34
|
+
orm_name = if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
|
|
35
|
+
:ActiveRecord
|
|
25
36
|
elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
|
|
26
|
-
|
|
37
|
+
:Sequel
|
|
27
38
|
else
|
|
28
39
|
raise TypeError, "expected an ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
|
|
29
40
|
end
|
|
41
|
+
|
|
42
|
+
# Get the specific subclass (self::ActiveRecord)
|
|
43
|
+
subclass = const_get(orm_name)
|
|
44
|
+
# Ensure the adapter is mixed in (lazy load)
|
|
45
|
+
subclass.mix_in_adapter(orm_name)
|
|
30
46
|
subclass.new(set, **)
|
|
31
47
|
end
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
# Helper to lazy-include the adapter module
|
|
50
|
+
def self.mix_in_adapter(orm_name)
|
|
51
|
+
adapter_module = Pagy::Keyset::Adapters.const_get(orm_name)
|
|
52
|
+
include(adapter_module) unless self < adapter_module
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(set, **)
|
|
34
56
|
assign_options(**)
|
|
35
57
|
assign_and_check(limit: 1)
|
|
36
58
|
@set = set
|
|
@@ -102,7 +124,7 @@ class Pagy
|
|
|
102
124
|
last_column, last_direction = keyset.pop
|
|
103
125
|
intersection = +'('
|
|
104
126
|
intersection << (keyset.map { |column, _d| "#{identifier[column]} = #{placeholder[column]}" } \
|
|
105
|
-
|
|
127
|
+
<< "#{identifier[last_column]} #{operator[last_direction]} #{placeholder[last_column]}").join(' AND ')
|
|
106
128
|
intersection << ')'
|
|
107
129
|
union << intersection
|
|
108
130
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
class Offset
|
|
5
|
+
# Offset pagination with memoized count
|
|
6
|
+
class Countish < Offset
|
|
7
|
+
protected
|
|
8
|
+
|
|
9
|
+
# Return page+count or page+count+epoch
|
|
10
|
+
def compose_page_param(page)
|
|
11
|
+
value = "#{page || 1}+#{@count}"
|
|
12
|
+
value << "+#{@options[:epoch]}" if @options[:epoch]
|
|
13
|
+
EscapedValue.new(value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,7 +4,7 @@ class Pagy
|
|
|
4
4
|
class Offset
|
|
5
5
|
# Offset pagination without a count
|
|
6
6
|
class Countless < Offset
|
|
7
|
-
def initialize(**)
|
|
7
|
+
def initialize(**)
|
|
8
8
|
assign_options(**)
|
|
9
9
|
assign_and_check(limit: 1, page: 1)
|
|
10
10
|
@page = upto_max_pages(@page)
|
|
@@ -55,7 +55,7 @@ class Pagy
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
# Support easy countless page param overriding (for legacy param and behavior)
|
|
58
|
-
def compose_page_param(page) = "#{page || 1}+#{@last}"
|
|
58
|
+
def compose_page_param(page) = EscapedValue.new("#{page || 1}+#{@last}")
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -9,11 +9,12 @@ class Pagy
|
|
|
9
9
|
DEFAULT = { page: 1 }.freeze
|
|
10
10
|
|
|
11
11
|
autoload :Countless, Pathname.new(__dir__).join('countless')
|
|
12
|
+
autoload :Countish, Pathname.new(__dir__).join('countish')
|
|
12
13
|
|
|
13
14
|
include Rangeable
|
|
14
15
|
include Shiftable
|
|
15
16
|
|
|
16
|
-
def initialize(**)
|
|
17
|
+
def initialize(**)
|
|
17
18
|
assign_options(**)
|
|
18
19
|
assign_and_check(limit: 1, count: 0, page: 1)
|
|
19
20
|
assign_last
|
data/lib/pagy/classes/request.rb
CHANGED
|
@@ -11,7 +11,7 @@ class Pagy
|
|
|
11
11
|
if request.is_a?(Hash)
|
|
12
12
|
request.values_at(:base_url, :path, :params, :cookie)
|
|
13
13
|
else
|
|
14
|
-
[request.base_url, request.path,
|
|
14
|
+
[request.base_url, request.path, get_params(request), request.cookies['pagy']]
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
|
|
@@ -32,5 +32,9 @@ class Pagy
|
|
|
32
32
|
|
|
33
33
|
[requested_limit.to_i, @options[:client_max_limit]].min
|
|
34
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def get_params(request) = request.GET.merge(request.POST).to_h
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Pagy
|
|
4
|
+
# Provide the helpers to count a collection
|
|
5
|
+
module Countable
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Get the collection count
|
|
9
|
+
def get_count(collection, options)
|
|
10
|
+
return collection.size if collection.instance_of?(Array)
|
|
11
|
+
return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
|
|
12
|
+
|
|
13
|
+
count = if options[:count_over] && !collection.group_values.empty?
|
|
14
|
+
# COUNT(*) OVER ()
|
|
15
|
+
sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
|
|
16
|
+
collection.unscope(:order).pick(sql).to_i
|
|
17
|
+
else
|
|
18
|
+
collection.count(:all)
|
|
19
|
+
end
|
|
20
|
+
count.is_a?(Hash) ? count.size : count
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require 'uri'
|
|
4
4
|
|
|
5
5
|
class Pagy
|
|
6
|
+
# Support spaces in placeholder params
|
|
7
|
+
class EscapedValue < String; end
|
|
8
|
+
|
|
6
9
|
# Provide the helpers to handle the url and anchor
|
|
7
10
|
module Linkable
|
|
8
11
|
module QueryUtils
|
|
@@ -23,8 +26,8 @@ class Pagy
|
|
|
23
26
|
else
|
|
24
27
|
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
"#{escape(prefix)}=#{
|
|
29
|
+
escaped_value = value.is_a?(EscapedValue) ? value : escape(value)
|
|
30
|
+
"#{escape(prefix)}=#{escaped_value}"
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
@@ -35,19 +38,26 @@ class Pagy
|
|
|
35
38
|
|
|
36
39
|
protected
|
|
37
40
|
|
|
41
|
+
# Overriddable by classes with composite page param
|
|
42
|
+
def compose_page_param(page) = page
|
|
43
|
+
|
|
38
44
|
# Return the URL for the page, relying on the Pagy::Request
|
|
39
|
-
def compose_page_url(page,
|
|
40
|
-
root_key, page_key, limit_key,
|
|
45
|
+
def compose_page_url(page, **options)
|
|
46
|
+
root_key, page_key, limit_key, client_max_limit, limit, querify, absolute, path, fragment =
|
|
41
47
|
@options.merge(options)
|
|
42
|
-
.values_at(:root_key, :page_key, :limit_key, :
|
|
48
|
+
.values_at(:root_key, :page_key, :limit_key, :client_max_limit, :limit, :querify, :absolute, :path, :fragment)
|
|
43
49
|
params = @request.params.clone(freeze: false)
|
|
44
|
-
(root_key ? params[root_key]
|
|
45
|
-
{ page_key =>
|
|
46
|
-
limit_key =>
|
|
50
|
+
(root_key ? params[root_key] = params[root_key]&.clone(freeze: false) || {} : params).tap do |h|
|
|
51
|
+
{ page_key => compose_page_param(page),
|
|
52
|
+
limit_key => client_max_limit && limit }.each { |k, v| v ? h[k] = v : h.delete(k) }
|
|
47
53
|
end
|
|
48
54
|
querify&.(params) # Must modify the params: the returned value is ignored
|
|
55
|
+
fragment &&= "##{fragment.delete_prefix('#')}"
|
|
56
|
+
compose_url(absolute, path, params, fragment)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def compose_url(absolute, path, params, fragment)
|
|
49
60
|
query_string = QueryUtils.build_nested_query(params).sub(/\A(?=.)/, '?')
|
|
50
|
-
fragment &&= "##{fragment.delete_prefix('#')}"
|
|
51
61
|
"#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
|
|
52
62
|
end
|
|
53
63
|
end
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
class Pagy
|
|
4
|
-
DEFAULT_DATA_KEYS = %i[url_template first_url previous_url page_url next_url last_url
|
|
4
|
+
DEFAULT_DATA_KEYS = %i[url_template first_url previous_url current_url page_url next_url last_url
|
|
5
5
|
count page limit last in from to previous next options].freeze
|
|
6
6
|
|
|
7
7
|
# Generate a hash of the wanted internal data
|
|
8
8
|
def data_hash(data_keys: @options[:data_keys] || DEFAULT_DATA_KEYS, **)
|
|
9
|
-
data_keys -= %i[count limit] if calendar?
|
|
10
9
|
template = compose_page_url(PAGE_TOKEN, **)
|
|
11
|
-
to_url = ->(page) { template.sub(PAGE_TOKEN, page.to_s) }
|
|
12
|
-
|
|
10
|
+
to_url = ->(page) { template.sub(PAGE_TOKEN, page.to_s) if page }
|
|
11
|
+
data_keys -= %i[count limit] if calendar?
|
|
13
12
|
data_keys.each_with_object({}) do |key, data|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
value = case key
|
|
14
|
+
when :url_template then template
|
|
15
|
+
when :first_url then compose_page_url(nil, **)
|
|
16
|
+
when :previous_url then to_url.(@previous)
|
|
17
|
+
when :current_url, :page_url then to_url.(@page)
|
|
18
|
+
when :next_url then to_url.(@next)
|
|
19
|
+
when :last_url then to_url.(@last)
|
|
20
|
+
else send(key)
|
|
21
|
+
end
|
|
22
|
+
data[key] = value if value
|
|
23
23
|
rescue NoMethodError
|
|
24
|
-
raise OptionError.new(self, :
|
|
24
|
+
raise OptionError.new(self, :data_keys, 'to contain known keys/methods', key)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -7,12 +7,12 @@ class Pagy
|
|
|
7
7
|
def limit_tag_js(id: nil, item_name: nil, client_max_limit: @options[:client_max_limit], **)
|
|
8
8
|
raise OptionError.new(self, :client_max_limit, 'to be truthy', client_max_limit) unless client_max_limit
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
limit_input = %(<input name="limit" type="number" min="1" max="#{@options[:client_max_limit]}" value="#{
|
|
10
|
+
limit_input = %(<input name="limit" type="number" min="1" max="#{client_max_limit}" value="#{
|
|
12
11
|
@limit}" style="padding: 0; text-align: center; width: #{@limit.to_s.length + 1}rem;">#{A_TAG})
|
|
12
|
+
url_token = compose_page_url(PAGE_TOKEN, limit: LIMIT_TOKEN)
|
|
13
13
|
|
|
14
14
|
%(<span#{%( id="#{id}") if id} class="pagy limit-tag-js" #{
|
|
15
|
-
data_pagy_attribute(:ltj, @from, url_token)
|
|
15
|
+
data_pagy_attribute(:ltj, @from, url_token, PAGE_TOKEN, LIMIT_TOKEN)
|
|
16
16
|
}><label>#{
|
|
17
17
|
I18n.translate('pagy.limit_tag_js',
|
|
18
18
|
item_name: item_name || I18n.translate('pagy.item_name', count: @limit),
|
|
@@ -24,8 +24,11 @@ class Pagy
|
|
|
24
24
|
send(visibility)
|
|
25
25
|
# Load the method, overriding its own alias. Next requests will call the method directly.
|
|
26
26
|
define_method(:"load_#{visibility}") do |*args, **kwargs|
|
|
27
|
+
# Tests shadow the usage of these lines
|
|
28
|
+
# :nocov:
|
|
27
29
|
require_relative methods[__callee__]
|
|
28
30
|
send(__callee__, *args, **kwargs)
|
|
31
|
+
# :nocov:
|
|
29
32
|
end
|
|
30
33
|
methods.each_key { |method| alias_method method, :"load_#{visibility}" }
|
|
31
34
|
end
|
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
class Pagy
|
|
4
4
|
# Return the page url for any page
|
|
5
|
-
# :nocov:
|
|
6
5
|
def page_url(page, **)
|
|
7
6
|
target = case page
|
|
8
|
-
when :first
|
|
9
|
-
when :current
|
|
10
|
-
when :previous
|
|
11
|
-
when :next
|
|
12
|
-
when :last
|
|
13
|
-
else
|
|
7
|
+
when :first then nil
|
|
8
|
+
when :current, :page then @page
|
|
9
|
+
when :previous then @previous
|
|
10
|
+
when :next then @next
|
|
11
|
+
when :last then @last
|
|
12
|
+
else page
|
|
14
13
|
end
|
|
15
14
|
compose_page_url(target, **) if target || page == :first
|
|
16
15
|
end
|
|
17
|
-
# :nocov:
|
|
18
16
|
end
|
|
@@ -12,7 +12,7 @@ class Pagy
|
|
|
12
12
|
def wrap_input_nav_js(html, nav_classes, id: nil, aria_label: nil, **)
|
|
13
13
|
%(<nav#{%( id="#{id}") if id} class="#{nav_classes}" #{
|
|
14
14
|
nav_aria_label_attribute(aria_label:)} #{
|
|
15
|
-
data = [:inj, compose_page_url(PAGE_TOKEN, **)]
|
|
15
|
+
data = [:inj, compose_page_url(PAGE_TOKEN, **), PAGE_TOKEN]
|
|
16
16
|
data.push(@update) if keynav?
|
|
17
17
|
data_pagy_attribute(*data)
|
|
18
18
|
}>#{html}</nav>)
|
|
@@ -29,7 +29,7 @@ class Pagy
|
|
|
29
29
|
nav_classes = "pagy-rjs #{nav_classes}" if sequels[0].size > 1
|
|
30
30
|
%(<nav#{%( id="#{id}") if id} class="#{nav_classes}" #{
|
|
31
31
|
nav_aria_label_attribute(aria_label:)} #{
|
|
32
|
-
data = [:snj, tokens.values, sequels]
|
|
32
|
+
data = [:snj, tokens.values, PAGE_TOKEN, sequels]
|
|
33
33
|
data.push(@update) if keynav?
|
|
34
34
|
data_pagy_attribute(*data)
|
|
35
35
|
}></nav>)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../modules/abilities/countable'
|
|
4
|
+
|
|
5
|
+
class Pagy
|
|
6
|
+
module CountishPaginator
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Return the Offset::Countish instance and records
|
|
10
|
+
def paginate(collection, options)
|
|
11
|
+
options[:page] ||= options[:request].resolve_page(force_integer: false)
|
|
12
|
+
if options[:page].is_a?(String)
|
|
13
|
+
page, count, epoch = options[:page].split.map(&:to_i)
|
|
14
|
+
options[:page] = page
|
|
15
|
+
end
|
|
16
|
+
setup_options(count, epoch, collection, options)
|
|
17
|
+
options[:limit] = options[:request].resolve_limit
|
|
18
|
+
pagy = Offset::Countish.new(**options)
|
|
19
|
+
[pagy, pagy.records(collection)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Get the count from the page and set epoch when ttl (Time To Live) requires it
|
|
23
|
+
def setup_options(count, epoch, collection, options)
|
|
24
|
+
now = Time.now.to_i
|
|
25
|
+
if !options[:count] && count && (!options[:ttl] ||
|
|
26
|
+
(epoch && epoch <= now && now < (epoch + options[:ttl]))) # ongoing
|
|
27
|
+
# puts 'ongoing'
|
|
28
|
+
options[:count] = count
|
|
29
|
+
options[:epoch] = epoch if options[:ttl]
|
|
30
|
+
else # recount
|
|
31
|
+
# puts 'recount'
|
|
32
|
+
options[:count] ||= Countable.get_count(collection, options)
|
|
33
|
+
options[:epoch] = now if options[:ttl]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -8,7 +8,7 @@ class Pagy
|
|
|
8
8
|
def paginate(collection, options)
|
|
9
9
|
options[:page] ||= options[:request].resolve_page(force_integer: false) # accept nil and strings
|
|
10
10
|
if options[:page].is_a?(String)
|
|
11
|
-
page, last = options[:page].split
|
|
11
|
+
page, last = options[:page].split.map(&:to_i) # decoded '+' added by the compose_page_url
|
|
12
12
|
options[:page] = page
|
|
13
13
|
options[:last] = last if last&.positive?
|
|
14
14
|
end
|
|
@@ -5,6 +5,7 @@ require_relative '../../classes/request'
|
|
|
5
5
|
class Pagy
|
|
6
6
|
paginators = { offset: :OffsetPaginator,
|
|
7
7
|
countless: :CountlessPaginator,
|
|
8
|
+
countish: :CountishPaginator,
|
|
8
9
|
keyset: :KeysetPaginator,
|
|
9
10
|
keynav_js: :KeynavJsPaginator,
|
|
10
11
|
calendar: :CalendarPaginator,
|
|
@@ -20,11 +21,11 @@ class Pagy
|
|
|
20
21
|
protected
|
|
21
22
|
|
|
22
23
|
define_method :pagy do |paginator = :offset, collection, **options|
|
|
23
|
-
arguments
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
arguments = if paginator == :calendar
|
|
25
|
+
[self, collection, options]
|
|
26
|
+
else
|
|
27
|
+
[collection, options = Pagy.options.merge(options)]
|
|
28
|
+
end
|
|
28
29
|
options[:root_key] = 'page' if options[:jsonapi] # enforce 'page' root_key for JSON:API
|
|
29
30
|
options[:request] ||= request # user set request or self.request
|
|
30
31
|
options[:request] = Request.new(options) # Pagy::Request
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative '../../modules/abilities/countable'
|
|
4
|
+
|
|
3
5
|
class Pagy
|
|
4
6
|
module OffsetPaginator
|
|
5
7
|
module_function
|
|
@@ -8,23 +10,9 @@ class Pagy
|
|
|
8
10
|
def paginate(collection, options)
|
|
9
11
|
options[:page] ||= options[:request].resolve_page
|
|
10
12
|
options[:limit] = options[:request].resolve_limit
|
|
11
|
-
options[:count] ||=
|
|
13
|
+
options[:count] ||= Countable.get_count(collection, options)
|
|
12
14
|
pagy = Offset.new(**options)
|
|
13
15
|
[pagy, collection.instance_of?(Array) ? collection[pagy.offset, pagy.limit] : pagy.records(collection)]
|
|
14
16
|
end
|
|
15
|
-
|
|
16
|
-
# Get the collection count
|
|
17
|
-
def get_count(collection, options)
|
|
18
|
-
return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
|
|
19
|
-
|
|
20
|
-
count = if options[:count_over] && !collection.group_values.empty?
|
|
21
|
-
# COUNT(*) OVER ()
|
|
22
|
-
sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
|
|
23
|
-
collection.unscope(:order).pick(sql).to_i
|
|
24
|
-
else
|
|
25
|
-
collection.count(:all)
|
|
26
|
-
end
|
|
27
|
-
count.is_a?(Hash) ? count.size : count
|
|
28
|
-
end
|
|
29
17
|
end
|
|
30
18
|
end
|
data/lib/pagy.rb
CHANGED
|
@@ -8,13 +8,11 @@ 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
|
-
|
|
12
|
-
|
|
13
|
-
VERSION = '43.1.8'
|
|
11
|
+
VERSION = '43.2.1'
|
|
14
12
|
ROOT = Pathname.new(__dir__).parent.freeze
|
|
15
13
|
DEFAULT = { limit: 20, limit_key: 'limit', page_key: 'page' }.freeze
|
|
16
|
-
PAGE_TOKEN =
|
|
17
|
-
LIMIT_TOKEN =
|
|
14
|
+
PAGE_TOKEN = EscapedValue.new('P ')
|
|
15
|
+
LIMIT_TOKEN = EscapedValue.new('L ')
|
|
18
16
|
LABEL_TOKEN = 'L'
|
|
19
17
|
A_TAG = '<a style="display: none;">#</a>'
|
|
20
18
|
|
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.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Domizio Demichelis
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- apps/keyset.ru
|
|
55
55
|
- apps/keyset_sequel.ru
|
|
56
56
|
- apps/rails.ru
|
|
57
|
+
- apps/rails_page_segment.rb
|
|
57
58
|
- apps/repro.ru
|
|
58
59
|
- bin/pagy
|
|
59
60
|
- config/pagy.rb
|
|
@@ -74,20 +75,18 @@ files:
|
|
|
74
75
|
- lib/pagy/classes/calendar/week.rb
|
|
75
76
|
- lib/pagy/classes/calendar/year.rb
|
|
76
77
|
- lib/pagy/classes/exceptions.rb
|
|
77
|
-
- lib/pagy/classes/keyset/active_record.rb
|
|
78
78
|
- lib/pagy/classes/keyset/adapters/active_record.rb
|
|
79
79
|
- lib/pagy/classes/keyset/adapters/sequel.rb
|
|
80
80
|
- lib/pagy/classes/keyset/keynav.rb
|
|
81
|
-
- lib/pagy/classes/keyset/keynav/active_record.rb
|
|
82
|
-
- lib/pagy/classes/keyset/keynav/sequel.rb
|
|
83
81
|
- lib/pagy/classes/keyset/keyset.rb
|
|
84
|
-
- lib/pagy/classes/
|
|
82
|
+
- lib/pagy/classes/offset/countish.rb
|
|
85
83
|
- lib/pagy/classes/offset/countless.rb
|
|
86
84
|
- lib/pagy/classes/offset/offset.rb
|
|
87
85
|
- lib/pagy/classes/offset/search.rb
|
|
88
86
|
- lib/pagy/classes/request.rb
|
|
89
87
|
- lib/pagy/console.rb
|
|
90
88
|
- lib/pagy/modules/abilities/configurable.rb
|
|
89
|
+
- lib/pagy/modules/abilities/countable.rb
|
|
91
90
|
- lib/pagy/modules/abilities/linkable.rb
|
|
92
91
|
- lib/pagy/modules/abilities/rangeable.rb
|
|
93
92
|
- lib/pagy/modules/abilities/shiftable.rb
|
|
@@ -130,6 +129,7 @@ files:
|
|
|
130
129
|
- lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb
|
|
131
130
|
- lib/pagy/toolbox/helpers/urls_hash.rb
|
|
132
131
|
- lib/pagy/toolbox/paginators/calendar.rb
|
|
132
|
+
- lib/pagy/toolbox/paginators/countish.rb
|
|
133
133
|
- lib/pagy/toolbox/paginators/countless.rb
|
|
134
134
|
- lib/pagy/toolbox/paginators/elasticsearch_rails.rb
|
|
135
135
|
- lib/pagy/toolbox/paginators/keynav_js.rb
|
|
@@ -202,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
202
202
|
- !ruby/object:Gem::Version
|
|
203
203
|
version: '0'
|
|
204
204
|
requirements: []
|
|
205
|
-
rubygems_version: 3.6.
|
|
205
|
+
rubygems_version: 3.6.9
|
|
206
206
|
specification_version: 4
|
|
207
207
|
summary: "Pagy \U0001F438 The Leaping Gem!"
|
|
208
208
|
test_files: []
|