pagy 8.0.0 → 9.0.0

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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/apps/calendar.ru +745 -0
  3. data/{lib/apps → apps}/demo.ru +35 -52
  4. data/apps/keyset_ar.ru +236 -0
  5. data/apps/keyset_s.ru +238 -0
  6. data/{lib/apps → apps}/rails.ru +40 -33
  7. data/{lib/apps → apps}/repro.ru +33 -24
  8. data/apps/tmp/calendar.sqlite3 +0 -0
  9. data/apps/tmp/calendar.sqlite3-shm +0 -0
  10. data/apps/tmp/calendar.sqlite3-wal +0 -0
  11. data/apps/tmp/local_secret.txt +1 -0
  12. data/apps/tmp/pagy-keyset-ar.sqlite3 +0 -0
  13. data/apps/tmp/pagy-keyset-ar.sqlite3-shm +0 -0
  14. data/apps/tmp/pagy-keyset-ar.sqlite3-wal +0 -0
  15. data/apps/tmp/pagy-keyset-s.sqlite3 +0 -0
  16. data/{lib/bin → bin}/pagy +36 -17
  17. data/{lib/config → config}/pagy.rb +37 -68
  18. data/javascripts/pagy-module.js +100 -0
  19. data/javascripts/pagy.js +4 -0
  20. data/javascripts/pagy.min.js +4 -0
  21. data/javascripts/pagy.min.js.map +10 -0
  22. data/javascripts/pagy.mjs +100 -0
  23. data/lib/optimist.rb +1 -1
  24. data/lib/pagy/b64.rb +33 -0
  25. data/lib/pagy/backend.rb +24 -15
  26. data/lib/pagy/calendar/day.rb +5 -4
  27. data/lib/pagy/calendar/month.rb +5 -4
  28. data/lib/pagy/calendar/quarter.rb +5 -4
  29. data/lib/pagy/calendar/unit.rb +103 -0
  30. data/lib/pagy/calendar/week.rb +4 -4
  31. data/lib/pagy/calendar/year.rb +5 -4
  32. data/lib/pagy/calendar.rb +55 -99
  33. data/lib/pagy/console.rb +2 -2
  34. data/lib/pagy/countless.rb +17 -16
  35. data/lib/pagy/extras/arel.rb +8 -10
  36. data/lib/pagy/extras/array.rb +4 -6
  37. data/lib/pagy/extras/bootstrap.rb +7 -7
  38. data/lib/pagy/extras/bulma.rb +13 -9
  39. data/lib/pagy/extras/calendar.rb +35 -6
  40. data/lib/pagy/extras/countless.rb +7 -14
  41. data/lib/pagy/extras/elasticsearch_rails.rb +15 -15
  42. data/lib/pagy/extras/gearbox.rb +36 -35
  43. data/lib/pagy/extras/headers.rb +26 -25
  44. data/lib/pagy/extras/i18n.rb +1 -1
  45. data/lib/pagy/extras/js_tools.rb +12 -9
  46. data/lib/pagy/extras/jsonapi.rb +27 -17
  47. data/lib/pagy/extras/keyset.rb +26 -0
  48. data/lib/pagy/extras/limit.rb +63 -0
  49. data/lib/pagy/extras/meilisearch.rb +11 -11
  50. data/lib/pagy/extras/metadata.rb +7 -3
  51. data/lib/pagy/extras/overflow.rb +9 -8
  52. data/lib/pagy/extras/pagy.rb +18 -18
  53. data/lib/pagy/extras/searchkick.rb +11 -11
  54. data/lib/pagy/extras/size.rb +40 -0
  55. data/lib/pagy/extras/standalone.rb +8 -8
  56. data/lib/pagy/extras/trim.rb +3 -3
  57. data/lib/pagy/frontend.rb +39 -37
  58. data/lib/pagy/i18n.rb +1 -1
  59. data/lib/pagy/keyset/active_record.rb +38 -0
  60. data/lib/pagy/keyset/sequel.rb +51 -0
  61. data/lib/pagy/keyset.rb +99 -0
  62. data/lib/pagy/url_helpers.rb +11 -11
  63. data/lib/pagy.rb +96 -120
  64. data/{lib/locales → locales}/ar.yml +9 -10
  65. data/{lib/locales → locales}/be.yml +2 -2
  66. data/{lib/locales → locales}/bg.yml +2 -2
  67. data/{lib/locales → locales}/bs.yml +2 -2
  68. data/{lib/locales → locales}/ca.yml +5 -7
  69. data/{lib/locales → locales}/ckb.yml +2 -2
  70. data/{lib/locales → locales}/cs.yml +2 -2
  71. data/{lib/locales → locales}/da.yml +5 -7
  72. data/{lib/locales → locales}/de.yml +2 -2
  73. data/{lib/locales → locales}/en.yml +2 -2
  74. data/{lib/locales → locales}/es.yml +2 -2
  75. data/{lib/locales → locales}/fr.yml +2 -2
  76. data/{lib/locales → locales}/hr.yml +2 -2
  77. data/{lib/locales → locales}/id.yml +2 -2
  78. data/{lib/locales → locales}/it.yml +2 -2
  79. data/{lib/locales → locales}/ja.yml +2 -2
  80. data/{lib/locales → locales}/km.yml +2 -2
  81. data/{lib/locales → locales}/ko.yml +3 -5
  82. data/{lib/locales → locales}/nb.yml +2 -2
  83. data/{lib/locales → locales}/nl.yml +2 -2
  84. data/{lib/locales → locales}/nn.yml +2 -2
  85. data/{lib/locales → locales}/pl.yml +2 -2
  86. data/{lib/locales → locales}/pt-BR.yml +2 -2
  87. data/{lib/locales → locales}/pt.yml +2 -2
  88. data/{lib/locales → locales}/ru.yml +7 -9
  89. data/{lib/locales → locales}/sr.yml +2 -2
  90. data/{lib/locales → locales}/sv-SE.yml +2 -2
  91. data/{lib/locales → locales}/sv.yml +2 -2
  92. data/{lib/locales → locales}/sw.yml +2 -2
  93. data/{lib/locales → locales}/ta.yml +2 -2
  94. data/{lib/locales → locales}/tr.yml +2 -2
  95. data/{lib/locales → locales}/uk.yml +2 -2
  96. data/{lib/locales → locales}/vi.yml +2 -2
  97. data/{lib/locales → locales}/zh-CN.yml +2 -2
  98. data/{lib/locales → locales}/zh-HK.yml +2 -2
  99. data/{lib/locales → locales}/zh-TW.yml +2 -2
  100. data/pkg/pagy-9.0.0.gem +0 -0
  101. metadata +75 -70
  102. data/lib/apps/calendar.ru +0 -2196
  103. data/lib/javascripts/pagy-dev.js +0 -112
  104. data/lib/javascripts/pagy-module.js +0 -111
  105. data/lib/javascripts/pagy.js +0 -1
  106. data/lib/pagy/calendar/helper.rb +0 -65
  107. data/lib/pagy/extras/foundation.rb +0 -93
  108. data/lib/pagy/extras/items.rb +0 -64
  109. data/lib/pagy/extras/materialize.rb +0 -97
  110. data/lib/pagy/extras/semantic.rb +0 -91
  111. data/lib/pagy/extras/uikit.rb +0 -96
  112. /data/{lib/javascripts/pagy-module.d.ts → javascripts/pagy.d.ts} +0 -0
  113. /data/{lib/stylesheets → stylesheets}/pagy.css +0 -0
  114. /data/{lib/stylesheets → stylesheets}/pagy.scss +0 -0
  115. /data/{lib/stylesheets → stylesheets}/pagy.tailwind.css +0 -0
@@ -0,0 +1,100 @@
1
+ const Pagy = (() => {
2
+ const rjsObserver = new ResizeObserver((entries) => entries.forEach((e) => e.target.querySelectorAll(".pagy-rjs").forEach((el) => el.pagyRender())));
3
+ const initNav = (el, [tokens, sequels, labelSequels, trimParam]) => {
4
+ const container = el.parentElement ?? el;
5
+ const widths = Object.keys(sequels).map((w) => parseInt(w)).sort((a, b) => b - a);
6
+ let lastWidth = -1;
7
+ const fillIn = (a, page, label) => a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);
8
+ (el.pagyRender = function() {
9
+ const width = widths.find((w) => w < container.clientWidth) || 0;
10
+ if (width === lastWidth) {
11
+ return;
12
+ }
13
+ let html = tokens.before;
14
+ const series = sequels[width.toString()];
15
+ const labels = labelSequels?.[width.toString()] ?? series.map((l) => l.toString());
16
+ series.forEach((item, i) => {
17
+ const label = labels[i];
18
+ let filled;
19
+ if (typeof item === "number") {
20
+ filled = fillIn(tokens.a, item.toString(), label);
21
+ } else if (item === "gap") {
22
+ filled = tokens.gap;
23
+ } else {
24
+ filled = fillIn(tokens.current, item, label);
25
+ }
26
+ html += typeof trimParam === "string" && item == 1 ? trim(filled, trimParam) : filled;
27
+ });
28
+ html += tokens.after;
29
+ el.innerHTML = "";
30
+ el.insertAdjacentHTML("afterbegin", html);
31
+ lastWidth = width;
32
+ })();
33
+ if (el.classList.contains("pagy-rjs")) {
34
+ rjsObserver.observe(container);
35
+ }
36
+ };
37
+ const initCombo = (el, [url_token, trimParam]) => initInput(el, (inputValue) => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], trimParam);
38
+ const initSelector = (el, [from, url_token, trimParam]) => {
39
+ initInput(el, (inputValue) => {
40
+ const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();
41
+ const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_items__/, inputValue);
42
+ return [page, url];
43
+ }, trimParam);
44
+ };
45
+ const initInput = (el, getVars, trimParam) => {
46
+ const input = el.querySelector("input");
47
+ const link = el.querySelector("a");
48
+ const initial = input.value;
49
+ const action = function() {
50
+ if (input.value === initial) {
51
+ return;
52
+ }
53
+ const [min, val, max] = [input.min, input.value, input.max].map((n) => parseInt(n) || 0);
54
+ if (val < min || val > max) {
55
+ input.value = initial;
56
+ input.select();
57
+ return;
58
+ }
59
+ let [page, url] = getVars(input.value);
60
+ if (typeof trimParam === "string" && page === "1") {
61
+ url = trim(url, trimParam);
62
+ }
63
+ link.href = url;
64
+ link.click();
65
+ };
66
+ ["change", "focus"].forEach((e) => input.addEventListener(e, () => input.select()));
67
+ input.addEventListener("focusout", action);
68
+ input.addEventListener("keypress", (e) => {
69
+ if (e.key === "Enter") {
70
+ action();
71
+ }
72
+ });
73
+ };
74
+ const trim = (a, param) => a.replace(new RegExp(`[?&]${param}=1\\b(?!&)|\\b${param}=1&`), "");
75
+ return {
76
+ version: "8.6.3",
77
+ init(arg) {
78
+ const target = arg instanceof Element ? arg : document;
79
+ const elements = target.querySelectorAll("[data-pagy]");
80
+ for (const el of elements) {
81
+ try {
82
+ const uint8array = Uint8Array.from(atob(el.getAttribute("data-pagy")), (c) => c.charCodeAt(0));
83
+ const [keyword, ...args] = JSON.parse(new TextDecoder().decode(uint8array));
84
+ if (keyword === "nav") {
85
+ initNav(el, args);
86
+ } else if (keyword === "combo") {
87
+ initCombo(el, args);
88
+ } else if (keyword === "selector") {
89
+ initSelector(el, args);
90
+ } else {
91
+ console.warn("Skipped Pagy.init() for: %o\nUnknown keyword '%s'", el, keyword);
92
+ }
93
+ } catch (err) {
94
+ console.warn("Skipped Pagy.init() for: %o\n%s", el, err);
95
+ }
96
+ }
97
+ }
98
+ };
99
+ })();
100
+ export default Pagy;
@@ -0,0 +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_items__/,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:"8.6.3",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
+
3
+ //# debugId=B9DC02765C7A5B6764756E2164756E21
4
+ //# sourceMappingURL=pagy.min.js.map
@@ -0,0 +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.0.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)}}}})();
2
+
3
+ //# debugId=69AACBBBCEE7711064756E2164756E21
4
+ //# sourceMappingURL=pagy.min.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/pagy.ts"],
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.0.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"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "69AACBBBCEE7711064756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,100 @@
1
+ const Pagy = (() => {
2
+ const rjsObserver = new ResizeObserver((entries) => entries.forEach((e) => e.target.querySelectorAll(".pagy-rjs").forEach((el) => el.pagyRender())));
3
+ const initNav = (el, [tokens, sequels, labelSequels, trimParam]) => {
4
+ const container = el.parentElement ?? el;
5
+ const widths = Object.keys(sequels).map((w) => parseInt(w)).sort((a, b) => b - a);
6
+ let lastWidth = -1;
7
+ const fillIn = (a, page, label) => a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);
8
+ (el.pagyRender = function() {
9
+ const width = widths.find((w) => w < container.clientWidth) || 0;
10
+ if (width === lastWidth) {
11
+ return;
12
+ }
13
+ let html = tokens.before;
14
+ const series = sequels[width.toString()];
15
+ const labels = labelSequels?.[width.toString()] ?? series.map((l) => l.toString());
16
+ series.forEach((item, i) => {
17
+ const label = labels[i];
18
+ let filled;
19
+ if (typeof item === "number") {
20
+ filled = fillIn(tokens.a, item.toString(), label);
21
+ } else if (item === "gap") {
22
+ filled = tokens.gap;
23
+ } else {
24
+ filled = fillIn(tokens.current, item, label);
25
+ }
26
+ html += typeof trimParam === "string" && item == 1 ? trim(filled, trimParam) : filled;
27
+ });
28
+ html += tokens.after;
29
+ el.innerHTML = "";
30
+ el.insertAdjacentHTML("afterbegin", html);
31
+ lastWidth = width;
32
+ })();
33
+ if (el.classList.contains("pagy-rjs")) {
34
+ rjsObserver.observe(container);
35
+ }
36
+ };
37
+ const initCombo = (el, [url_token, trimParam]) => initInput(el, (inputValue) => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], trimParam);
38
+ const initSelector = (el, [from, url_token, trimParam]) => {
39
+ initInput(el, (inputValue) => {
40
+ const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();
41
+ const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);
42
+ return [page, url];
43
+ }, trimParam);
44
+ };
45
+ const initInput = (el, getVars, trimParam) => {
46
+ const input = el.querySelector("input");
47
+ const link = el.querySelector("a");
48
+ const initial = input.value;
49
+ const action = function() {
50
+ if (input.value === initial) {
51
+ return;
52
+ }
53
+ const [min, val, max] = [input.min, input.value, input.max].map((n) => parseInt(n) || 0);
54
+ if (val < min || val > max) {
55
+ input.value = initial;
56
+ input.select();
57
+ return;
58
+ }
59
+ let [page, url] = getVars(input.value);
60
+ if (typeof trimParam === "string" && page === "1") {
61
+ url = trim(url, trimParam);
62
+ }
63
+ link.href = url;
64
+ link.click();
65
+ };
66
+ ["change", "focus"].forEach((e) => input.addEventListener(e, () => input.select()));
67
+ input.addEventListener("focusout", action);
68
+ input.addEventListener("keypress", (e) => {
69
+ if (e.key === "Enter") {
70
+ action();
71
+ }
72
+ });
73
+ };
74
+ const trim = (a, param) => a.replace(new RegExp(`[?&]${param}=1\\b(?!&)|\\b${param}=1&`), "");
75
+ return {
76
+ version: "9.0.0",
77
+ init(arg) {
78
+ const target = arg instanceof Element ? arg : document;
79
+ const elements = target.querySelectorAll("[data-pagy]");
80
+ for (const el of elements) {
81
+ try {
82
+ const uint8array = Uint8Array.from(atob(el.getAttribute("data-pagy")), (c) => c.charCodeAt(0));
83
+ const [keyword, ...args] = JSON.parse(new TextDecoder().decode(uint8array));
84
+ if (keyword === "nav") {
85
+ initNav(el, args);
86
+ } else if (keyword === "combo") {
87
+ initCombo(el, args);
88
+ } else if (keyword === "selector") {
89
+ initSelector(el, args);
90
+ } else {
91
+ console.warn("Skipped Pagy.init() for: %o\nUnknown keyword '%s'", el, keyword);
92
+ }
93
+ } catch (err) {
94
+ console.warn("Skipped Pagy.init() for: %o\n%s", el, err);
95
+ }
96
+ }
97
+ }
98
+ };
99
+ })();
100
+ export default Pagy;
data/lib/optimist.rb CHANGED
@@ -137,7 +137,7 @@ module Optimist
137
137
  ## There's one ambiguous case to be aware of: when +:multi+: is true and a
138
138
  ## +:default+ is set to an array (of something), it's ambiguous whether this
139
139
  ## is a multi-value argument as well as a multi-occurrence argument.
140
- ## In thise case, Optimist assumes that it's not a multi-value argument.
140
+ ## In this case, Optimist assumes that it's not a multi-value argument.
141
141
  ## If you want a multi-value, multi-occurrence argument with a default
142
142
  ## value, you must specify +:type+ as well.
143
143
 
data/lib/pagy/b64.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy # :nodoc:
4
+ # Cheap Base64 specialized methods to avoid dependencies
5
+ module B64
6
+ module_function
7
+
8
+ def encode(bin)
9
+ [bin].pack('m0')
10
+ end
11
+
12
+ def decode(str)
13
+ str.unpack1('m0')
14
+ end
15
+
16
+ def urlsafe_encode(bin)
17
+ str = encode(bin)
18
+ str.chomp!('==') or str.chomp!('=')
19
+ str.tr!('+/', '-_')
20
+ str
21
+ end
22
+
23
+ def urlsafe_decode(str)
24
+ if !str.end_with?('=') && str.length % 4 != 0
25
+ str = str.ljust((str.length + 3) & ~3, '=')
26
+ str.tr!('-_', '+/')
27
+ else
28
+ str = str.tr('-_', '+/')
29
+ end
30
+ decode(str)
31
+ end
32
+ end
33
+ end
data/lib/pagy/backend.rb CHANGED
@@ -8,32 +8,41 @@ class Pagy
8
8
  module Backend
9
9
  private
10
10
 
11
- # Return Pagy object and paginated items/results
12
- def pagy(collection, vars = {})
13
- pagy = Pagy.new(pagy_get_vars(collection, vars))
11
+ # Return Pagy object and paginated results
12
+ def pagy(collection, **vars)
13
+ pagy = Pagy.new(**pagy_get_vars(collection, vars))
14
14
  [pagy, pagy_get_items(collection, pagy)]
15
15
  end
16
16
 
17
- # Sub-method called only by #pagy: here for easy customization of variables by overriding
18
- # You may need to override the count call for non AR collections
19
- def pagy_get_vars(collection, vars)
20
- pagy_set_items_from_params(vars) if defined?(ItemsExtra)
17
+ # Get the count from the collection
18
+ def pagy_get_count(collection, vars)
21
19
  count_args = vars[:count_args] || DEFAULT[:count_args]
22
- vars[:count] ||= (count = collection.count(*count_args)).is_a?(Hash) ? count.size : count
23
- vars[:page] ||= pagy_get_page(vars)
24
- vars
20
+ (count = collection.count(*count_args)).is_a?(Hash) ? count.size : count
21
+ end
22
+
23
+ # Sub-method called only by #pagy: here for easy customization of fetching by overriding
24
+ # You may need to override this method for collections without offset|limit
25
+ def pagy_get_items(collection, pagy)
26
+ collection.offset(pagy.offset).limit(pagy.limit)
25
27
  end
26
28
 
29
+ # Override for limit extra
30
+ def pagy_get_limit(vars); end
31
+
27
32
  # Get the page integer from the params
28
33
  # Overridable by the jsonapi extra
29
34
  def pagy_get_page(vars)
30
- (params[vars[:page_param] || DEFAULT[:page_param]] || 1).to_i
35
+ params[vars[:page_param] || DEFAULT[:page_param]]
31
36
  end
32
37
 
33
- # Sub-method called only by #pagy: here for easy customization of record-extraction by overriding
34
- # You may need to override this method for collections without offset|limit
35
- def pagy_get_items(collection, pagy)
36
- collection.offset(pagy.offset).limit(pagy.items)
38
+ # Sub-method called only by #pagy: here for easy customization of variables by overriding
39
+ # You may need to override the count call for non AR collections
40
+ def pagy_get_vars(collection, vars)
41
+ vars.tap do |v|
42
+ v[:count] ||= pagy_get_count(collection, v)
43
+ v[:limit] ||= pagy_get_limit(v)
44
+ v[:page] ||= pagy_get_page(v)
45
+ end
37
46
  end
38
47
  end
39
48
  end
@@ -3,20 +3,21 @@
3
3
 
4
4
  class Pagy # :nodoc:
5
5
  class Calendar # :nodoc:
6
- # Calendar day subclass
7
- class Day < Calendar
6
+ # Day unit subclass
7
+ class Day < Unit
8
8
  DEFAULT = { size: 31, # rubocop:disable Style/MutableConstant
9
+ ends: false,
9
10
  order: :asc,
10
11
  format: '%d' }
11
12
 
12
13
  protected
13
14
 
14
15
  # Setup the calendar variables
15
- def setup_unit_vars
16
+ def assign_unit_vars
16
17
  super
17
18
  @initial = @starting.beginning_of_day
18
19
  @final = @ending.tomorrow.beginning_of_day
19
- @pages = @last = page_offset(@initial, @final)
20
+ @last = page_offset(@initial, @final)
20
21
  @from = starting_time_for(@page)
21
22
  @to = @from.tomorrow
22
23
  end
@@ -3,20 +3,21 @@
3
3
 
4
4
  class Pagy # :nodoc:
5
5
  class Calendar # :nodoc:
6
- # Calendar month subclass
7
- class Month < Calendar
6
+ # Month unit subclass
7
+ class Month < Unit
8
8
  DEFAULT = { size: 12, # rubocop:disable Style/MutableConstant
9
+ ends: false,
9
10
  order: :asc,
10
11
  format: '%b' }
11
12
 
12
13
  protected
13
14
 
14
15
  # Setup the calendar variables
15
- def setup_unit_vars
16
+ def assign_unit_vars
16
17
  super
17
18
  @initial = @starting.beginning_of_month
18
19
  @final = @ending.next_month.beginning_of_month
19
- @pages = @last = (months_in(@final) - months_in(@initial))
20
+ @last = (months_in(@final) - months_in(@initial))
20
21
  @from = starting_time_for(@page)
21
22
  @to = @from.next_month
22
23
  end
@@ -3,9 +3,10 @@
3
3
 
4
4
  class Pagy # :nodoc:
5
5
  class Calendar # :nodoc:
6
- # Calendar quarter subclass
7
- class Quarter < Calendar
6
+ # Quarter unit subclass
7
+ class Quarter < Unit
8
8
  DEFAULT = { size: 4, # rubocop:disable Style/MutableConstant
9
+ ends: false,
9
10
  order: :asc,
10
11
  format: 'Q%q' } # '%q' token
11
12
 
@@ -19,11 +20,11 @@ class Pagy # :nodoc:
19
20
  protected
20
21
 
21
22
  # Setup the calendar variables
22
- def setup_unit_vars
23
+ def assign_unit_vars
23
24
  super
24
25
  @initial = @starting.beginning_of_quarter
25
26
  @final = @ending.next_quarter.beginning_of_quarter
26
- @pages = @last = (months_in(@final) - months_in(@initial)) / 3
27
+ @last = (months_in(@final) - months_in(@initial)) / 3
27
28
  @from = starting_time_for(@page)
28
29
  @to = @from.next_quarter
29
30
  end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext/time'
5
+ require 'active_support/core_ext/date_and_time/calculations'
6
+ require 'active_support/core_ext/numeric/time'
7
+ require 'active_support/core_ext/integer/time'
8
+
9
+ class Pagy # :nodoc:
10
+ class Calendar < Hash # :nodoc:
11
+ # Base class for time units subclasses (Year, Quarter, Month, Week, Day)
12
+ class Unit < Pagy
13
+ attr_reader :order, :from, :to
14
+
15
+ # Merge and validate the options, do some simple arithmetic and set a few instance variables
16
+ def initialize(**vars) # rubocop:disable Lint/MissingSuper
17
+ raise InternalError, 'Pagy::Calendar::Unit is a base class; use one of its subclasses' \
18
+ if instance_of?(Pagy::Calendar::Unit)
19
+
20
+ assign_vars({ **Pagy::DEFAULT, **self.class::DEFAULT }, vars)
21
+ assign_and_check(page: 1)
22
+ assign_unit_vars
23
+ check_overflow
24
+ assign_prev_and_next
25
+ end
26
+
27
+ # The label for the current page (it can pass along the I18n gem opts when it's used with the i18n extra)
28
+ def label(opts = {})
29
+ label_for(@page, opts)
30
+ end
31
+
32
+ # The label for any page (it can pass along the I18n gem opts when it's used with the i18n extra)
33
+ def label_for(page, opts = {})
34
+ opts[:format] ||= @vars[:format]
35
+ localize(starting_time_for(page.to_i), opts) # page could be a string
36
+ end
37
+
38
+ protected
39
+
40
+ # The page that includes time
41
+ # In case of out of range time, the :fit_time option avoids the outOfRangeError
42
+ # and returns the closest page to the passed time argument (first or last page)
43
+ def page_at(time, **opts)
44
+ fit_time = time
45
+ fit_final = @final - 1
46
+ unless time.between?(@initial, fit_final)
47
+ raise OutOfRangeError.new(self, :time, "between #{@initial} and #{fit_final}", time) unless opts[:fit_time]
48
+
49
+ if time < @final
50
+ fit_time = @initial
51
+ ordinal = 'first'
52
+ else
53
+ fit_time = fit_final
54
+ ordinal = 'last'
55
+ end
56
+ warn "Pagy::Calendar#page_at: Rescued #{time} out of range by returning the #{ordinal} page."
57
+ end
58
+ offset = page_offset_at(fit_time) # offset starts from 0
59
+ @order == :asc ? offset + 1 : @last - offset
60
+ end
61
+
62
+ # Base class method for the setup of the unit variables (subclasses must implement it and call super)
63
+ def assign_unit_vars
64
+ raise VariableError.new(self, :format, 'to be a strftime format', @vars[:format]) unless @vars[:format].is_a?(String)
65
+ raise VariableError.new(self, :order, 'to be in [:asc, :desc]', @order) \
66
+ unless %i[asc desc].include?(@order = @vars[:order])
67
+
68
+ @starting, @ending = @vars[:period]
69
+ raise VariableError.new(self, :period, 'to be a an Array of min and max TimeWithZone instances', @vars[:period]) \
70
+ unless @starting.is_a?(ActiveSupport::TimeWithZone) \
71
+ && @ending.is_a?(ActiveSupport::TimeWithZone) && @starting <= @ending
72
+ end
73
+
74
+ # Apply the strftime format to the time (overridden by the i18n extra when localization is required)
75
+ def localize(time, opts)
76
+ time.strftime(opts[:format])
77
+ end
78
+
79
+ # Number of time units to offset from the @initial time, in order to get the ordered starting time for the page.
80
+ # Used in starting_time_for(page) where page starts from 1 (e.g. page to starting_time means subtracting 1)
81
+ def time_offset_for(page)
82
+ @order == :asc ? page - 1 : @last - page
83
+ end
84
+
85
+ # Period of the active page (used internally for nested units)
86
+ def active_period
87
+ [[@starting, @from].max, [@to - 1, @ending].min] # -1 sec: include only last unit day
88
+ end
89
+
90
+ # :nocov:
91
+ # This method must be implemented by the unit subclass
92
+ def starting_time_for(*)
93
+ raise NoMethodError, 'the starting_time_for method must be implemented by the unit subclass'
94
+ end
95
+
96
+ # This method must be implemented by the unit subclass
97
+ def page_offset_at(*)
98
+ raise NoMethodError, 'the page_offset_at method must be implemented by the unit subclass'
99
+ end
100
+ # :nocov:
101
+ end
102
+ end
103
+ end
@@ -3,19 +3,19 @@
3
3
 
4
4
  class Pagy # :nodoc:
5
5
  class Calendar # :nodoc:
6
- # Calendar week subclass
7
- class Week < Calendar
6
+ # Week unit subclass
7
+ class Week < Unit
8
8
  DEFAULT = { order: :asc, # rubocop:disable Style/MutableConstant
9
9
  format: '%Y-%W' }
10
10
 
11
11
  protected
12
12
 
13
13
  # Setup the calendar variables
14
- def setup_unit_vars
14
+ def assign_unit_vars
15
15
  super
16
16
  @initial = @starting.beginning_of_week
17
17
  @final = @ending.next_week.beginning_of_week
18
- @pages = @last = page_offset(@initial, @final)
18
+ @last = page_offset(@initial, @final)
19
19
  @from = starting_time_for(@page)
20
20
  @to = @from.next_week
21
21
  end
@@ -3,20 +3,21 @@
3
3
 
4
4
  class Pagy # :nodoc:
5
5
  class Calendar # :nodoc:
6
- # Calendar year subclass
7
- class Year < Calendar
6
+ # Year unit subclass
7
+ class Year < Unit
8
8
  DEFAULT = { size: 10, # rubocop:disable Style/MutableConstant
9
+ ends: false,
9
10
  order: :asc,
10
11
  format: '%Y' }
11
12
 
12
13
  protected
13
14
 
14
15
  # Setup the calendar variables
15
- def setup_unit_vars
16
+ def assign_unit_vars
16
17
  super
17
18
  @initial = @starting.beginning_of_year
18
19
  @final = @ending.next_year.beginning_of_year
19
- @pages = @last = @final.year - @initial.year
20
+ @last = @final.year - @initial.year
20
21
  @from = starting_time_for(@page)
21
22
  @to = @from.next_year
22
23
  end