From 6e53decfe2457e2391abce0ea4410fce11112911 Mon Sep 17 00:00:00 2001
From: Domizio Demichelis
Date: Mon, 23 Dec 2024 16:37:57 +0700
Subject: [PATCH] Add final things
---
gem/javascripts/pagy.min.js | 4 +--
gem/javascripts/pagy.min.js.map | 6 ++--
gem/javascripts/pagy.mjs | 34 ++++++------------
gem/lib/pagy/extras/bootstrap.rb | 2 +-
gem/lib/pagy/extras/bulma.rb | 2 +-
gem/lib/pagy/extras/keyset_for_ui.rb | 9 ++---
gem/lib/pagy/extras/pagy.rb | 2 +-
gem/lib/pagy/keyset_for_ui.rb | 16 +++++----
src/pagy.ts | 50 +++++++++++++-------------
test/mock_helpers/app.rb | 13 ++++---
test/pagy/extras/keyset_for_ui_test.rb | 12 +++----
test/pagy/keyset_for_ui_test.rb | 19 +++++-----
12 files changed, 79 insertions(+), 90 deletions(-)
diff --git a/gem/javascripts/pagy.min.js b/gem/javascripts/pagy.min.js
index 547dca2c7..145ec7e48 100644
--- a/gem/javascripts/pagy.min.js
+++ b/gem/javascripts/pagy.min.js
@@ -1,4 +1,4 @@
-window.Pagy=(()=>{const Y=sessionStorage,j=new BroadcastChannel("pagy"),x=Date.now();j.addEventListener("message",(z)=>{if(z.data.from){const A=Y.getItem(z.data.key);if(A)j.postMessage({to:z.data.from,key:z.data.key,cutoffs:A})}else if(z.data.to){if(z.data.to==x)Y.setItem(z.data.key,z.data.cutoffs)}});const U=new ResizeObserver((z)=>z.forEach((A)=>{A.target.querySelectorAll(".pagy-rjs").forEach((D)=>D.pagyRender()),A.target.querySelectorAll(".pagy-keyset").forEach((D)=>D.completeUrls())})),$={encode:(z)=>btoa(String.fromCharCode(...new TextEncoder().encode(z))),toSafe:(z)=>z.replace(/=/g,"").replace(/[+/]/g,(A)=>A=="+"?"-":"_"),safeEncode:(z)=>$.toSafe($.encode(z)),decode:(z)=>new TextDecoder().decode(Uint8Array.from(atob(z),(A)=>A.charCodeAt(0)))},C=(z,[A])=>{N(z,A)},N=async(z,A)=>{if(!A||!Array.isArray(A.update)||!A.cutoffs_param||!A.page_param)return;const D=document.cookie.split(/;\s+/).find((L)=>L.startsWith("pagy="))?.split("=")[1]||Math.floor(Math.random()*46656).toString(36);document.cookie="pagy="+D;let[B,E,G]=A.update;if(B&&!(B in Y))j.postMessage({from:x,key:B}),await new Promise((L)=>setTimeout(()=>L(""),100));B||="pagy-"+Date.now().toString(36);const R=Y.getItem(B),K=R?JSON.parse(R):[null];if(E&&G)K[E]=G,Y.setItem(B,JSON.stringify(K));(z.completeUrls=()=>{for(let L of z.querySelectorAll("a[href]")){const H=L.href,M=new RegExp(`(?<=\\?.*)\\b${A.page_param}=([\\d]+)`),Q=parseInt(H.match(M)?.[1]),Z=$.safeEncode(JSON.stringify([D,B,K.length,K[Q-1],K[Q]]));L.href=H+`&${A.cutoffs_param}=${Z}`}})()},W=(z,[A,D,B,E])=>{const G=z.parentElement??z,R=Object.keys(D).map((H)=>parseInt(H)).sort((H,M)=>M-H);let K=-1;const L=(H,M,Q)=>H.replace(/__pagy_page__/g,M).replace(/__pagy_label__/g,Q);if((z.pagyRender=()=>{const H=R.find((T)=>TT.toString());Q.forEach((T,q)=>{const P=Z[q];let X;if(typeof T=="number"){if(X=L(A.a,T.toString(),P),typeof E?.page_param=="string"&&T==1)X=O(X,E.page_param)}else if(T=="gap")X=A.gap;else X=L(A.current,T,P);M+=X}),M+=A.after,z.innerHTML="",z.insertAdjacentHTML("afterbegin",M),K=H})(),z.classList.contains("pagy-rjs"))U.observe(G)},J=(z,[A,D])=>F(z,(B)=>[B,A.replace(/__pagy_page__/,B)],D),_=(z,[A,D,B])=>{F(z,(E)=>{const G=Math.max(Math.ceil(A/parseInt(E)),1).toString(),R=D.replace(/__pagy_page__/,G).replace(/__pagy_limit__/,E);return[G,R]},B)},F=(z,A,D)=>{const B=z.querySelector("input"),E=z.querySelector("a"),G=B.value,R=()=>{if(B.value===G)return;const[K,L,H]=[B.min,B.value,B.max].map((Z)=>parseInt(Z)||0);if(LH){B.value=G,B.select();return}let[M,Q]=A(B.value);if(typeof D?.page_param=="string"&&M==="1")Q=O(Q,D.page_param);E.href=Q,E.click()};["change","focus"].forEach((K)=>B.addEventListener(K,()=>B.select())),B.addEventListener("focusout",R),B.addEventListener("keypress",(K)=>{if(K.key=="Enter")R()})},O=(z,A)=>z.replace(new RegExp(`[?&]${A}=1\\b(?!&)|\\b${A}=1&`),"");return{version:"9.3.3",init(z){const D=(z instanceof Element?z:document).querySelectorAll("[data-pagy]");for(let B of D)try{const[E,...G]=JSON.parse($.decode(B.getAttribute("data-pagy")));if(E=="nav")C(B,G);else if(E=="nav_js")W(B,G);else if(E=="combo_js")J(B,G);else if(E=="selector_js")_(B,G)}catch(E){console.warn("Failed Pagy.init(): %o\n%s",B,E)}}}})();
+window.Pagy=(()=>{const Z=sessionStorage,j=new BroadcastChannel("pagy"),F=Date.now();j.addEventListener("message",(z)=>{if(z.data.from){const B=Z.getItem(z.data.key);if(B)j.postMessage({to:z.data.from,key:z.data.key,cutoffs:B})}else if(z.data.to){if(z.data.to==F)Z.setItem(z.data.key,z.data.cutoffs)}});const A=new ResizeObserver((z)=>z.forEach((B)=>{B.target.querySelectorAll(".pagy-rjs").forEach((E)=>E.pagyRender()),B.target.querySelectorAll(".pagy-keyset").forEach((E)=>E.completeUrls())})),$={encode:(z)=>btoa(String.fromCharCode(...new TextEncoder().encode(z))),toSafe:(z)=>z.replace(/=/g,"").replace(/[+/]/g,(B)=>B=="+"?"-":"_"),safeEncode:(z)=>$.toSafe($.encode(z)),decode:(z)=>new TextDecoder().decode(Uint8Array.from(atob(z),(B)=>B.charCodeAt(0)))},C=(z,[B])=>{N(z,B)},N=async(z,B)=>{if(!B||!Array.isArray(B.update)||!B.cutoffs_param||!B.page_param)return;const E=document.cookie.split(/;\s+/).find((K)=>K.startsWith("pagy="))?.split("=")[1]||Math.floor(Math.random()*46656).toString(36);document.cookie="pagy="+E;let[D,...G]=B.update;if(D&&!(D in Z))j.postMessage({from:F,key:D}),await new Promise((K)=>setTimeout(()=>K(""),100));D||="pagy-"+Date.now().toString(36);const H=Z.getItem(D),M=H?JSON.parse(H):[null];if(G)M.splice(...G),Z.setItem(D,JSON.stringify(M));(z.completeUrls=()=>{for(let K of z.querySelectorAll("a[href]")){const X=K.href,L=new RegExp(`(?<=\\?.*)\\b${B.page_param}=([\\d]+)`),Q=parseInt(X.match(L)?.[1]),R=$.safeEncode(JSON.stringify([E,D,M.length,M[Q-1],M[Q]]));K.href=X+`&${B.cutoffs_param}=${R}`}})()},W=(z,[B,E,D,G])=>{const H=z.parentElement??z,M=Object.keys(E).map((L)=>parseInt(L)).sort((L,Q)=>Q-L);let K=-1;const X=(L,Q,R)=>L.replace(/__pagy_page__/g,Q).replace(/__pagy_label__/g,R);if((z.pagyRender=()=>{const L=M.find((T)=>TT.toString());R.forEach((T,q)=>{const U=x[q];let Y;if(typeof T=="number"){if(Y=X(B.a,T.toString(),U),typeof G?.page_param=="string"&&T==1)Y=P(Y,G.page_param)}else if(T=="gap")Y=B.gap;else Y=X(B.current,T,U);Q+=Y}),Q+=B.after,z.innerHTML="",z.insertAdjacentHTML("afterbegin",Q),K=L})(),z.classList.contains("pagy-rjs"))A.observe(H)},J=(z,[B,E])=>O(z,(D)=>[D,B.replace(/__pagy_page__/,D)],E),_=(z,[B,E,D])=>{O(z,(G)=>{const H=Math.max(Math.ceil(B/parseInt(G)),1).toString(),M=E.replace(/__pagy_page__/,H).replace(/__pagy_limit__/,G);return[H,M]},D)},O=(z,B,E)=>{const D=z.querySelector("input"),G=z.querySelector("a"),H=D.value,M=()=>{if(D.value===H)return;const[K,X,L]=[D.min,D.value,D.max].map((x)=>parseInt(x)||0);if(XL){D.value=H,D.select();return}let[Q,R]=B(D.value);if(typeof E?.page_param=="string"&&Q==="1")R=P(R,E.page_param);G.href=R,G.click()};["change","focus"].forEach((K)=>D.addEventListener(K,()=>D.select())),D.addEventListener("focusout",M),D.addEventListener("keypress",(K)=>{if(K.key=="Enter")M()})},P=(z,B)=>z.replace(new RegExp(`[?&]${B}=1\\b(?!&)|\\b${B}=1&`),"");return{version:"9.3.3",init(z){const B=z instanceof Element?z:document,E=B.querySelectorAll("[data-pagy]");for(let D of E)try{const[G,...H]=JSON.parse($.decode(D.getAttribute("data-pagy")));if(G=="nav")C(D,H);else if(G=="nav_js")W(D,H);else if(G=="combo_js")J(D,H);else if(G=="selector_js")_(D,H)}catch(G){console.warn("Failed Pagy.init(): %o\n%s",D,G)}}}})();
-//# debugId=9AB674AC438688CC64756E2164756E21
+//# debugId=F0DEAFFC2E0A2A4464756E2164756E21
//# sourceMappingURL=pagy.min.js.map
diff --git a/gem/javascripts/pagy.min.js.map b/gem/javascripts/pagy.min.js.map
index 40067487b..21f46c189 100644
--- a/gem/javascripts/pagy.min.js.map
+++ b/gem/javascripts/pagy.min.js.map
@@ -2,9 +2,9 @@
"version": 3,
"sources": ["../../src/pagy.ts"],
"sourcesContent": [
- "type InitArgs = [\"nav\", NavArgs] | [\"nav_js\", NavJsArgs] | [\"combo_js\", ComboJsArgs] | [\"selector_js\", SelectorJsArgs]\ntype NavArgs = readonly [OptionArgs?]\ntype NavJsArgs = readonly [Tokens, Sequels, null | LabelSequels, OptionArgs?]\ntype ComboJsArgs = readonly [string, OptionArgs?]\ntype SelectorJsArgs = readonly [number, string, OptionArgs?]\ntype Cutoff = readonly [string | number | boolean]\ntype Update = [string, number, Cutoff] | [string]\ntype Cutoffs = [null, ...Cutoff[]]\ntype CutoffsParam = [string, string, number, null | Cutoff, Cutoff | undefined]\n\ninterface SyncData {\n from?: number\n to?: number\n key: string\n cutoffs?: string\n}\n\ninterface OptionArgs {\n readonly page_param?: string\n readonly cutoffs_param?: string\n readonly update?: Update\n}\n\ninterface Tokens {\n readonly before: string\n readonly a: string\n readonly current: string\n readonly gap: string\n readonly after: string\n}\n\ninterface Sequels {readonly [width:string]:(string | number)[]}\ninterface LabelSequels {readonly [width:string]:string[]}\ninterface NavJsElement extends Element {pagyRender():void}\ninterface CutoffElement extends NavJsElement, Element {completeUrls():void}\n\nconst Pagy = (() => {\n\n // Sync the sessionStorage keys for the cutoffs used in the new tab/window\n // e.g. copy/paste the page number link in a new window or page link right-click \"Open in a new tab/window\"\n const sS = sessionStorage; // shorten the .min.js\n const sync = new BroadcastChannel(\"pagy\");\n const tabId = Date.now();\n\n sync.addEventListener(\"message\", (e: MessageEvent) => {\n if (e.data.from) { // request cutoffs\n const cutoffs = sS.getItem(e.data.key);\n if (cutoffs) { sync.postMessage({to: e.data.from, key: e.data.key, cutoffs: cutoffs}) } // send response\n } else if (e.data.to) { // receive cutoffs\n if (e.data.to == tabId) {\n sS.setItem(e.data.key, e.data.cutoffs);\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(\".pagy-rjs\").forEach(el => el.pagyRender());\n e.target.querySelectorAll(\".pagy-keyset\").forEach(el => el.completeUrls());\n }));\n\n const b64 = {\n encode: (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode))),\n toSafe: (unsafe:string) => unsafe.replace(/=/g, \"\").replace(/[+/]/g, (match) => match == \"+\" ? \"-\" : \"_\"),\n safeEncode: (unicode:string) => b64.toSafe(b64.encode(unicode)),\n decode: (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0))),\n // toUnsafe: (safe:string) => safe.replace(/[-_]/g, (match) => match == \"-\" ? \"+\" : \"/\"),\n // safeDecode: (base64:string) => b64.decode(b64.toUnsafe(base64))\n };\n\n // Init the *_nav helpers\n const initNav = (el:Element, [opts]:NavArgs) => {\n void initCutoff(el, opts);\n };\n\n// Init the Cutoff features\n const initCutoff = async (el:CutoffElement, opts?:OptionArgs) => {\n if (!opts || !Array.isArray(opts.update) // not enabled\n || !opts.cutoffs_param || !opts.page_param) { // Bad opts\n // console.warn(\"Failed Pagy.initCutoff():%o\\n Bad opts \\n%o\", el, opts);\n return;\n }\n const pagyId = document.cookie.split(/;\\s+/) // it works even if malformed\n .find((row) => row.startsWith(\"pagy=\"))\n ?.split(\"=\")[1] || Math.floor(Math.random() * 36 ** 3).toString(36);\n document.cookie = \"pagy=\" + pagyId;\n\n // eslint-disable-next-line prefer-const\n let [key, last, latest] = opts.update;\n if (key && !(key in sS)) {\n // Sync the sessiongStorage from other tabs/windows (e.g. open page in new tab/window)_\n sync.postMessage({ from: tabId, key: key });\n // Wait for the listener to copy the cutoffs in the current sessionStorage\n await new Promise((resolve) => setTimeout(() => resolve(\"\"), 100) );\n }\n key ||= \"pagy-\" + Date.now().toString(36);\n const cs = sS.getItem(key);\n const cutoffs = (cs ? JSON.parse(cs) : [null]);\n if (last && latest) {\n cutoffs[last] = latest;\n sS.setItem(key, JSON.stringify(cutoffs));\n }\n (el.completeUrls = () => {\n for (const a of el.querySelectorAll('a[href]')) {\n const url = a.href;\n const re = new RegExp(`(?<=\\\\?.*)\\\\b${opts.page_param}=([\\\\d]+)`); // find the numeric page\n const page = parseInt(url.match(re)?.[1]); // sure that page=\\d+ is in href\n const value = b64.safeEncode(JSON.stringify([pagyId,\n key,\n cutoffs.length, // actual cutoffs + 1 (first null)\n cutoffs[page - 1],\n cutoffs[page]]));\n a.href = url + `&${opts.cutoffs_param}=${value}`; // \"&\" because the query_string is always present\n }\n })();\n };\n\n // Init the *_nav_js helpers\n const initNavJs = (el:NavJsElement, [tokens, sequels, labelSequels, opts]:NavJsArgs) => {\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) =>\n a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);\n (el.pagyRender = () => {\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 by ruby 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 if (typeof opts?.page_param == \"string\" && item == 1) { filled = trim(filled, opts.page_param) }\n } else if (item == \"gap\") {\n filled = tokens.gap;\n } else { // active page\n filled = fillIn(tokens.current, item, label);\n }\n html += filled;\n });\n html += tokens.after; // already trimmed by ruby in html\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 initComboJs = (el:Element, [url_token, opts]:ComboJsArgs) =>\n initInput(el, inputValue => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], opts);\n\n // Init the limit_selector_js helper\n const initSelectorJs = (el:Element, [from, url_token, opts]:SelectorJsArgs) => {\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 }, opts);\n };\n\n // Init the input element\n const initInput = (el:Element, getVars:(v:string) => [string, string], opts?:OptionArgs) => {\n const input = el.querySelector(\"input\") as HTMLInputElement;\n const link = el.querySelector(\"a\") as HTMLAnchorElement;\n const initial = input.value;\n const 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 let [page, url] = getVars(input.value); // eslint-disable-line prefer-const\n if (typeof opts?.page_param == \"string\" && page === \"1\") { url = trim(url, opts.page_param) }\n link.href = url;\n link.click();\n };\n [\"change\", \"focus\"].forEach(e => input.addEventListener(e, () => input.select())); // auto-select\n input.addEventListener(\"focusout\", action); // trigger action\n input.addEventListener(\"keypress\", e => { if (e.key == \"Enter\") { action() } }); // trigger action\n };\n\n // Trim the ${page-param}=1 params in links\n const trim = (a:string, param:string) =>\n a.replace(new RegExp(`[?&]${param}=1\\\\b(?!&)|\\\\b${param}=1&`), \"\");\n\n // Public interface\n return {\n version: \"9.3.3\",\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 [keyword, ...args] = JSON.parse(b64.decode(el.getAttribute(\"data-pagy\")));\n if (keyword == \"nav\") {\n initNav(el, args);\n } else if (keyword == \"nav_js\") {\n initNavJs(el, args);\n } else if (keyword == \"combo_js\") {\n initComboJs(el, args);\n } else if (keyword == \"selector_js\") {\n initSelectorJs(el, args);\n }\n //else { console.warn(\"Failed Pagy.init(): %o\\nUnknown keyword '%s'\", el, keyword) }\n } catch (err) { console.warn(\"Failed Pagy.init(): %o\\n%s\", el, err) }\n }\n }\n };\n})();\n\nexport default Pagy;\n"
+ "type InitArgs = [\"nav\", NavArgs] | [\"nav_js\", NavJsArgs] | [\"combo_js\", ComboJsArgs] | [\"selector_js\", SelectorJsArgs]\ntype NavArgs = readonly [OptionArgs?]\ntype NavJsArgs = readonly [Tokens, Sequels, null | LabelSequels, OptionArgs?]\ntype ComboJsArgs = readonly [string, OptionArgs?]\ntype SelectorJsArgs = readonly [number, string, OptionArgs?]\ntype Cutoff = readonly [string | number | boolean]\ntype SpliceArgs = readonly [number, number, ...Cutoff[]] | [number, number]\ntype Update = [string, SpliceArgs] | [string]\ntype Cutoffs = [null, ...Cutoff[]]\ntype CutoffsParam = [string, string, number, null | Cutoff, Cutoff | undefined]\n\ninterface SyncData {\n from?: number\n to?: number\n key: string\n cutoffs?: string\n}\n\ninterface OptionArgs {\n readonly page_param?: string\n readonly cutoffs_param?: string\n readonly update?: Update\n}\n\ninterface Tokens {\n readonly before: string\n readonly a: string\n readonly current: string\n readonly gap: string\n readonly after: string\n}\n\ninterface Sequels {readonly [width:string]:(string | number)[]}\ninterface LabelSequels {readonly [width:string]:string[]}\ninterface NavJsElement extends Element {pagyRender():void}\ninterface CutoffElement extends NavJsElement, Element {completeUrls():void}\n\nconst Pagy = (() => {\n\n // Sync the sessionStorage keys for the cutoffs used in the new tab/window\n // e.g. copy/paste the page number link in a new window or page link right-click \"Open in a new tab/window\"\n const sS = sessionStorage, // shorten the .min.js\n sync = new BroadcastChannel(\"pagy\"),\n tabId = Date.now();\n\n sync.addEventListener(\"message\", (e: MessageEvent) => {\n if (e.data.from) { // request cutoffs\n const cutoffs = sS.getItem(e.data.key);\n if (cutoffs) { sync.postMessage({to: e.data.from, key: e.data.key, cutoffs: cutoffs}) } // send response\n } else if (e.data.to) { // receive cutoffs\n if (e.data.to == tabId) {\n sS.setItem(e.data.key, e.data.cutoffs);\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(\".pagy-rjs\").forEach(el => el.pagyRender());\n e.target.querySelectorAll(\".pagy-keyset\").forEach(el => el.completeUrls());\n }));\n\n const b64 = {\n encode: (unicode:string) => btoa(String.fromCharCode(...(new TextEncoder).encode(unicode))),\n toSafe: (unsafe:string) => unsafe.replace(/=/g, \"\").replace(/[+/]/g, (match) => match == \"+\" ? \"-\" : \"_\"),\n safeEncode: (unicode:string) => b64.toSafe(b64.encode(unicode)),\n decode: (base64:string) => (new TextDecoder()).decode(Uint8Array.from(atob(base64), c => c.charCodeAt(0))),\n // toUnsafe: (safe:string) => safe.replace(/[-_]/g, (match) => match == \"-\" ? \"+\" : \"/\"),\n // safeDecode: (base64:string) => b64.decode(b64.toUnsafe(base64))\n };\n\n // Init the *_nav helpers\n const initNav = (el:Element, [opts]:NavArgs) => {\n void initCutoff(el, opts);\n };\n\n// Init the Cutoff features\n const initCutoff = async (el:CutoffElement, opts?:OptionArgs) => {\n if (!opts || !Array.isArray(opts.update) // not enabled\n || !opts.cutoffs_param || !opts.page_param) { // Bad opts\n // console.warn(\"Failed Pagy.initCutoff():%o\\n Bad opts \\n%o\", el, opts);\n return;\n }\n const pagyId = document.cookie.split(/;\\s+/) // it works even if malformed\n .find((row) => row.startsWith(\"pagy=\"))\n ?.split(\"=\")[1] || Math.floor(Math.random() * 36 ** 3).toString(36);\n document.cookie = \"pagy=\" + pagyId;\n\n // eslint-disable-next-line prefer-const\n let [key, ...spliceArgs] = opts.update;\n if (key && !(key in sS)) {\n // Sync the sessiongStorage from other tabs/windows (e.g. open page in new tab/window)_\n sync.postMessage({ from: tabId, key: key });\n // Wait for the listener to copy the cutoffs in the current sessionStorage\n await new Promise((resolve) => setTimeout(() => resolve(\"\"), 100) );\n }\n key ||= \"pagy-\" + Date.now().toString(36);\n const cs = sS.getItem(key),\n cutoffs = (cs ? JSON.parse(cs) : [null]);\n if (spliceArgs) {\n // @ts-expect-error: spliceArgs should be a tuple type or passed to a rest param, but it contains all the args\n cutoffs.splice(...spliceArgs);\n sS.setItem(key, JSON.stringify(cutoffs));\n }\n (el.completeUrls = () => {\n for (const a of el.querySelectorAll('a[href]')) {\n const url = a.href,\n re = new RegExp(`(?<=\\\\?.*)\\\\b${opts.page_param}=([\\\\d]+)`), // find the numeric page\n page = parseInt(url.match(re)?.[1]), // sure that page=\\d+ is in href\n value = b64.safeEncode(JSON.stringify([pagyId,\n key,\n cutoffs.length, // actual cutoffs + 1 (first null)\n cutoffs[page - 1],\n cutoffs[page]]));\n a.href = url + `&${opts.cutoffs_param}=${value}`; // \"&\" because the query_string is always present\n }\n })();\n };\n\n // Init the *_nav_js helpers\n const initNavJs = (el:NavJsElement, [tokens, sequels, labelSequels, opts]:NavJsArgs) => {\n const container = el.parentElement ?? el,\n 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) =>\n a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);\n (el.pagyRender = () => {\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 by ruby in html\n const series = sequels[width.toString()],\n 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 if (typeof opts?.page_param == \"string\" && item == 1) { filled = trim(filled, opts.page_param) }\n } else if (item == \"gap\") {\n filled = tokens.gap;\n } else { // active page\n filled = fillIn(tokens.current, item, label);\n }\n html += filled;\n });\n html += tokens.after; // already trimmed by ruby in html\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 initComboJs = (el:Element, [url_token, opts]:ComboJsArgs) =>\n initInput(el, inputValue => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], opts);\n\n // Init the limit_selector_js helper\n const initSelectorJs = (el:Element, [from, url_token, opts]:SelectorJsArgs) => {\n initInput(el, inputValue => {\n const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString(),\n url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);\n return [page, url];\n }, opts);\n };\n\n // Init the input element\n const initInput = (el:Element, getVars:(v:string) => [string, string], opts?:OptionArgs) => {\n const input = el.querySelector(\"input\") as HTMLInputElement,\n link = el.querySelector(\"a\") as HTMLAnchorElement,\n initial = input.value;\n const 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 let [page, url] = getVars(input.value); // eslint-disable-line prefer-const\n if (typeof opts?.page_param == \"string\" && page === \"1\") { url = trim(url, opts.page_param) }\n link.href = url;\n link.click();\n };\n [\"change\", \"focus\"].forEach(e => input.addEventListener(e, () => input.select())); // auto-select\n input.addEventListener(\"focusout\", action); // trigger action\n input.addEventListener(\"keypress\", e => { if (e.key == \"Enter\") { action() } }); // trigger action\n };\n\n // Trim the ${page-param}=1 params in links\n const trim = (a:string, param:string) =>\n a.replace(new RegExp(`[?&]${param}=1\\\\b(?!&)|\\\\b${param}=1&`), \"\");\n\n // Public interface\n return {\n version: \"9.3.3\",\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 elements = target.querySelectorAll(\"[data-pagy]\");\n for (const el of elements) {\n try {\n const [keyword, ...args] = JSON.parse(b64.decode(el.getAttribute(\"data-pagy\")));\n if (keyword == \"nav\") {\n initNav(el, args);\n } else if (keyword == \"nav_js\") {\n initNavJs(el, args);\n } else if (keyword == \"combo_js\") {\n initComboJs(el, args);\n } else if (keyword == \"selector_js\") {\n initSelectorJs(el, args);\n }\n //else { console.warn(\"Failed Pagy.init(): %o\\nUnknown keyword '%s'\", el, keyword) }\n } catch (err) { console.warn(\"Failed Pagy.init(): %o\\n%s\", el, err) }\n }\n }\n };\n})();\n\nexport default Pagy;\n"
],
- "mappings": "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",
- "debugId": "9AB674AC438688CC64756E2164756E21",
+ "mappings": "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",
+ "debugId": "F0DEAFFC2E0A2A4464756E2164756E21",
"names": []
}
\ No newline at end of file
diff --git a/gem/javascripts/pagy.mjs b/gem/javascripts/pagy.mjs
index 9124227a5..9816f0b8f 100644
--- a/gem/javascripts/pagy.mjs
+++ b/gem/javascripts/pagy.mjs
@@ -1,7 +1,5 @@
const Pagy = (() => {
- const sS = sessionStorage;
- const sync = new BroadcastChannel("pagy");
- const tabId = Date.now();
+ const sS = sessionStorage, sync = new BroadcastChannel("pagy"), tabId = Date.now();
sync.addEventListener("message", (e) => {
if (e.data.from) {
const cutoffs = sS.getItem(e.data.key);
@@ -33,24 +31,20 @@ const Pagy = (() => {
}
const pagyId = document.cookie.split(/;\s+/).find((row) => row.startsWith("pagy="))?.split("=")[1] || Math.floor(Math.random() * 36 ** 3).toString(36);
document.cookie = "pagy=" + pagyId;
- let [key, last, latest] = opts.update;
+ let [key, ...spliceArgs] = opts.update;
if (key && !(key in sS)) {
sync.postMessage({ from: tabId, key });
await new Promise((resolve) => setTimeout(() => resolve(""), 100));
}
key ||= "pagy-" + Date.now().toString(36);
- const cs = sS.getItem(key);
- const cutoffs = cs ? JSON.parse(cs) : [null];
- if (last && latest) {
- cutoffs[last] = latest;
+ const cs = sS.getItem(key), cutoffs = cs ? JSON.parse(cs) : [null];
+ if (spliceArgs) {
+ cutoffs.splice(...spliceArgs);
sS.setItem(key, JSON.stringify(cutoffs));
}
(el.completeUrls = () => {
for (const a of el.querySelectorAll("a[href]")) {
- const url = a.href;
- const re = new RegExp(`(?<=\\?.*)\\b${opts.page_param}=([\\d]+)`);
- const page = parseInt(url.match(re)?.[1]);
- const value = b64.safeEncode(JSON.stringify([
+ const url = a.href, re = new RegExp(`(?<=\\?.*)\\b${opts.page_param}=([\\d]+)`), page = parseInt(url.match(re)?.[1]), value = b64.safeEncode(JSON.stringify([
pagyId,
key,
cutoffs.length,
@@ -62,8 +56,7 @@ const Pagy = (() => {
})();
};
const initNavJs = (el, [tokens, sequels, labelSequels, opts]) => {
- const container = el.parentElement ?? el;
- const widths = Object.keys(sequels).map((w) => parseInt(w)).sort((a, b) => b - a);
+ const container = el.parentElement ?? el, widths = Object.keys(sequels).map((w) => parseInt(w)).sort((a, b) => b - a);
let lastWidth = -1;
const fillIn = (a, page, label) => a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);
(el.pagyRender = () => {
@@ -72,8 +65,7 @@ const Pagy = (() => {
return;
}
let html = tokens.before;
- const series = sequels[width.toString()];
- const labels = labelSequels?.[width.toString()] ?? series.map((l) => l.toString());
+ const series = sequels[width.toString()], labels = labelSequels?.[width.toString()] ?? series.map((l) => l.toString());
series.forEach((item, i) => {
const label = labels[i];
let filled;
@@ -101,15 +93,12 @@ const Pagy = (() => {
const initComboJs = (el, [url_token, opts]) => initInput(el, (inputValue) => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], opts);
const initSelectorJs = (el, [from, url_token, opts]) => {
initInput(el, (inputValue) => {
- const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();
- const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);
+ const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString(), url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);
return [page, url];
}, opts);
};
const initInput = (el, getVars, opts) => {
- const input = el.querySelector("input");
- const link = el.querySelector("a");
- const initial = input.value;
+ const input = el.querySelector("input"), link = el.querySelector("a"), initial = input.value;
const action = () => {
if (input.value === initial) {
return;
@@ -139,8 +128,7 @@ const Pagy = (() => {
return {
version: "9.3.3",
init(arg) {
- const target = arg instanceof Element ? arg : document;
- const elements = target.querySelectorAll("[data-pagy]");
+ const target = arg instanceof Element ? arg : document, elements = target.querySelectorAll("[data-pagy]");
for (const el of elements) {
try {
const [keyword, ...args] = JSON.parse(b64.decode(el.getAttribute("data-pagy")));
diff --git a/gem/lib/pagy/extras/bootstrap.rb b/gem/lib/pagy/extras/bootstrap.rb
index b59b1b051..20faddb8f 100644
--- a/gem/lib/pagy/extras/bootstrap.rb
+++ b/gem/lib/pagy/extras/bootstrap.rb
@@ -46,7 +46,7 @@ def pagy_bootstrap_nav_js(pagy, id: nil, classes: 'pagination', aria_label: nil,
'after' => %(#{bootstrap_next_html pagy, a}) }
%()
diff --git a/gem/lib/pagy/extras/bulma.rb b/gem/lib/pagy/extras/bulma.rb
index 0857abadc..6fce72579 100644
--- a/gem/lib/pagy/extras/bulma.rb
+++ b/gem/lib/pagy/extras/bulma.rb
@@ -47,7 +47,7 @@ def pagy_bulma_nav_js(pagy, id: nil, classes: 'pagy-bulma nav-js pagination is-c
'after' => '' }
%()
diff --git a/gem/lib/pagy/extras/keyset_for_ui.rb b/gem/lib/pagy/extras/keyset_for_ui.rb
index 7170c95f1..786b82f02 100644
--- a/gem/lib/pagy/extras/keyset_for_ui.rb
+++ b/gem/lib/pagy/extras/keyset_for_ui.rb
@@ -27,12 +27,9 @@ def get_cutoffs(vars)
cutoffs = JSON.parse(B64.urlsafe_decode(cutoffs))
pagy_id = cutoffs.shift
- return cutoffs if request.cookies['pagy'] == pagy_id
-
- # The url has been requested from another browser, which does not have the same sessionStorage,
- # hence we need to restart the pagination to page 1
- vars[:page] = 1
- KeysetForUI::FIRST_PAGE
+ # No cutoffs if the url has been requested from another browser,
+ # which does not have the same sessionStorage, hence we need to restart the pagination to page 1
+ request.cookies['pagy'] == pagy_id ? cutoffs : nil
end
end
Backend.prepend KeysetForUIExtra
diff --git a/gem/lib/pagy/extras/pagy.rb b/gem/lib/pagy/extras/pagy.rb
index 5ae4dff25..73b7b1d5f 100644
--- a/gem/lib/pagy/extras/pagy.rb
+++ b/gem/lib/pagy/extras/pagy.rb
@@ -21,7 +21,7 @@ def pagy_nav_js(pagy, id: nil, aria_label: nil, **vars)
'after' => next_a(pagy, a) }
%()
diff --git a/gem/lib/pagy/keyset_for_ui.rb b/gem/lib/pagy/keyset_for_ui.rb
index 95bb4b875..fc8316c25 100644
--- a/gem/lib/pagy/keyset_for_ui.rb
+++ b/gem/lib/pagy/keyset_for_ui.rb
@@ -16,7 +16,6 @@ class Sequel < KeysetForUI
# Avoid args conflicts in composite SQL fragments
CUTOFF_PREFIX = 'cutoff_' # Prefix for cutoff_args
- FIRST_PAGE = [nil, 1, nil, nil].freeze
include SharedUIMethods
attr_reader :update
@@ -32,10 +31,13 @@ def initialize(set, **vars)
# Get the cutoff from the client
def assign_cutoffs
- # key, is from the client and sent back as-is in order to id the requests of the same set
- key, @last, @prev_cutoff, @cutoff = @vars[:cutoffs] || FIRST_PAGE
- @update = [key]
- # raise OverflowError.new(self, :page, "in 1..#{@last}", @page) if @page > @last
+ if @vars[:cutoffs]
+ key, @last, @prev_cutoff, @cutoff = @vars[:cutoffs]
+ raise OverflowError.new(self, :page, "in 1..#{@last}", @page) if @page > @last
+ else
+ @page = @last = 1
+ end
+ @update = [key] # key, is from the client and sent back as-is in order to id the requests of the same set
end
# Assign different args to support the AFTER_CUTOFF SQL if @cutoff
@@ -134,8 +136,8 @@ def next
@next ||= (@page + 1).tap do
unless @cutoff
@cutoff = derive_cutoff
- @update.push(@last, @cutoff) # operation arguments for the client cutoffs
- @last += 1 # reflect the added cutoff
+ @update.push(@page, 1, @cutoff) # splice arguments for the client cutoffs
+ @last += 1 # reflect the added cutoff
end
end
end
diff --git a/src/pagy.ts b/src/pagy.ts
index 07757251c..b4b65f44e 100644
--- a/src/pagy.ts
+++ b/src/pagy.ts
@@ -4,7 +4,8 @@ type NavJsArgs = readonly [Tokens, Sequels, null | LabelSequels, OptionArgs
type ComboJsArgs = readonly [string, OptionArgs?]
type SelectorJsArgs = readonly [number, string, OptionArgs?]
type Cutoff = readonly [string | number | boolean]
-type Update = [string, number, Cutoff] | [string]
+type SpliceArgs = readonly [number, number, ...Cutoff[]] | [number, number]
+type Update = [string, SpliceArgs] | [string]
type Cutoffs = [null, ...Cutoff[]]
type CutoffsParam = [string, string, number, null | Cutoff, Cutoff | undefined]
@@ -38,9 +39,9 @@ const Pagy = (() => {
// Sync the sessionStorage keys for the cutoffs used in the new tab/window
// e.g. copy/paste the page number link in a new window or page link right-click "Open in a new tab/window"
- const sS = sessionStorage; // shorten the .min.js
- const sync = new BroadcastChannel("pagy");
- const tabId = Date.now();
+ const sS = sessionStorage, // shorten the .min.js
+ sync = new BroadcastChannel("pagy"),
+ tabId = Date.now();
sync.addEventListener("message", (e: MessageEvent) => {
if (e.data.from) { // request cutoffs
@@ -87,7 +88,7 @@ const Pagy = (() => {
document.cookie = "pagy=" + pagyId;
// eslint-disable-next-line prefer-const
- let [key, last, latest] = opts.update;
+ let [key, ...spliceArgs] = opts.update;
if (key && !(key in sS)) {
// Sync the sessiongStorage from other tabs/windows (e.g. open page in new tab/window)_
sync.postMessage({ from: tabId, key: key });
@@ -95,18 +96,19 @@ const Pagy = (() => {
await new Promise((resolve) => setTimeout(() => resolve(""), 100) );
}
key ||= "pagy-" + Date.now().toString(36);
- const cs = sS.getItem(key);
- const cutoffs = (cs ? JSON.parse(cs) : [null]);
- if (last && latest) {
- cutoffs[last] = latest;
+ const cs = sS.getItem(key),
+ cutoffs = (cs ? JSON.parse(cs) : [null]);
+ if (spliceArgs) {
+ // @ts-expect-error: spliceArgs should be a tuple type or passed to a rest param, but it contains all the args
+ cutoffs.splice(...spliceArgs);
sS.setItem(key, JSON.stringify(cutoffs));
}
(el.completeUrls = () => {
for (const a of el.querySelectorAll('a[href]')) {
- const url = a.href;
- const re = new RegExp(`(?<=\\?.*)\\b${opts.page_param}=([\\d]+)`); // find the numeric page
- const page = parseInt(url.match(re)?.[1]); // sure that page=\d+ is in href
- const value = b64.safeEncode(JSON.stringify([pagyId,
+ const url = a.href,
+ re = new RegExp(`(?<=\\?.*)\\b${opts.page_param}=([\\d]+)`), // find the numeric page
+ page = parseInt(url.match(re)?.[1]), // sure that page=\d+ is in href
+ value = b64.safeEncode(JSON.stringify([pagyId,
key,
cutoffs.length, // actual cutoffs + 1 (first null)
cutoffs[page - 1],
@@ -118,8 +120,8 @@ const Pagy = (() => {
// Init the *_nav_js helpers
const initNavJs = (el:NavJsElement, [tokens, sequels, labelSequels, opts]:NavJsArgs) => {
- const container = el.parentElement ?? el;
- const widths = Object.keys(sequels).map(w => parseInt(w)).sort((a, b) => b - a);
+ const container = el.parentElement ?? el,
+ widths = Object.keys(sequels).map(w => parseInt(w)).sort((a, b) => b - a);
let lastWidth = -1;
const fillIn = (a:string, page:string, label:string) =>
a.replace(/__pagy_page__/g, page).replace(/__pagy_label__/g, label);
@@ -127,8 +129,8 @@ const Pagy = (() => {
const width = widths.find(w => w < container.clientWidth) || 0;
if (width === lastWidth) { return } // no change: abort
let html = tokens.before; // already trimmed by ruby in html
- const series = sequels[width.toString()];
- const labels = labelSequels?.[width.toString()] ?? series.map(l => l.toString());
+ const series = sequels[width.toString()],
+ labels = labelSequels?.[width.toString()] ?? series.map(l => l.toString());
series.forEach((item, i) => {
const label = labels[i];
let filled;
@@ -157,17 +159,17 @@ const Pagy = (() => {
// Init the limit_selector_js helper
const initSelectorJs = (el:Element, [from, url_token, opts]:SelectorJsArgs) => {
initInput(el, inputValue => {
- const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString();
- const url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);
+ const page = Math.max(Math.ceil(from / parseInt(inputValue)), 1).toString(),
+ url = url_token.replace(/__pagy_page__/, page).replace(/__pagy_limit__/, inputValue);
return [page, url];
}, opts);
};
// Init the input element
const initInput = (el:Element, getVars:(v:string) => [string, string], opts?:OptionArgs) => {
- const input = el.querySelector("input") as HTMLInputElement;
- const link = el.querySelector("a") as HTMLAnchorElement;
- const initial = input.value;
+ const input = el.querySelector("input") as HTMLInputElement,
+ link = el.querySelector("a") as HTMLAnchorElement,
+ initial = input.value;
const action = () => {
if (input.value === initial) { return } // not changed
const [min, val, max] = [input.min, input.value, input.max].map(n => parseInt(n) || 0);
@@ -196,8 +198,8 @@ const Pagy = (() => {
// Scan for elements with a "data-pagy" attribute and call their init functions with the decoded args
init(arg?:Element) {
- const target = arg instanceof Element ? arg : document;
- const elements = target.querySelectorAll("[data-pagy]");
+ const target = arg instanceof Element ? arg : document,
+ elements = target.querySelectorAll("[data-pagy]");
for (const el of elements) {
try {
const [keyword, ...args] = JSON.parse(b64.decode(el.getAttribute("data-pagy")));
diff --git a/test/mock_helpers/app.rb b/test/mock_helpers/app.rb
index 84ceb0745..6eaa88223 100644
--- a/test/mock_helpers/app.rb
+++ b/test/mock_helpers/app.rb
@@ -16,15 +16,14 @@ class MockApp
# App params are merged into the @request.params (and are all strings)
# @params are taken from @request.params and merged with app params (which fixes symbols and strings in params)
- def initialize(url: 'http://example.com:3000/foo', params: { page: 3 }, session: {}, cookies: {})
- @request = Rack::Request.new(Rack::MockRequest.env_for(url, params: params))
- @params = ActiveSupport::HashWithIndifferentAccess.new(@request.params).merge(params)
- @response = Rack::Response.new
- @session = session
- @cookies = cookies
+ def initialize(url: 'http://example.com:3000/foo', params: { page: 3 }, cookie: nil)
+ env = Rack::MockRequest.env_for(url, params: params, cookies: cookies)
+ env["HTTP_COOKIE"] = cookie if cookie
+ @request = Rack::Request.new(env)
+ @params = ActiveSupport::HashWithIndifferentAccess.new(@request.params).merge(params)
+ @response = Rack::Response.new
end
-
def test_i18n_call
I18n.t('test')
end
diff --git a/test/pagy/extras/keyset_for_ui_test.rb b/test/pagy/extras/keyset_for_ui_test.rb
index c56b29443..a70096f82 100644
--- a/test/pagy/extras/keyset_for_ui_test.rb
+++ b/test/pagy/extras/keyset_for_ui_test.rb
@@ -19,11 +19,11 @@
_(pagy).must_be_kind_of Pagy::KeysetForUI
_(records.size).must_equal 10
_(pagy.next).must_equal 2
- _(pagy.update).must_equal [nil, 1, [10]]
+ _(pagy.update).must_equal [nil, 1, 1, [10]]
end
it 'works for page 2' do
app = MockApp.new(params: {cutoffs: Pagy::B64.urlsafe_encode(['ppp', 'key', 2, [10]].to_json)},
- cookies: {pagy: 'ppp'})
+ cookie: 'pagy=ppp')
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
page: 2,
@@ -33,11 +33,11 @@
_(records.size).must_equal 10
_(records.first.id).must_equal 11
_(pagy.next).must_equal 3
- _(pagy.update).must_equal ['key', 2, [20]]
+ _(pagy.update).must_equal ['key', 2, 1, [20]]
end
it 'reset pagination for missing cookie' do
app = MockApp.new(params: {cutoffs: Pagy::B64.urlsafe_encode(['zzz', 'key', 2, [10]].to_json)},
- cookies: {pagy: 'ppp'})
+ cookie: 'pagy=ppp')
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
page: 2,
@@ -46,11 +46,11 @@
_(pagy).must_be_kind_of Pagy::KeysetForUI
_(records.size).must_equal 10
_(pagy.next).must_equal 2
- _(pagy.update).must_equal [nil, 1, [10]]
+ _(pagy.update).must_equal [nil, 1, 1, [10]]
end
it 'works for page 5' do
app = MockApp.new(params: {cutoffs: Pagy::B64.urlsafe_encode(['ppp', 'key', 5, [40]].to_json)},
- cookies: {pagy: 'ppp'})
+ cookie: 'pagy=ppp')
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
page: 5,
diff --git a/test/pagy/keyset_for_ui_test.rb b/test/pagy/keyset_for_ui_test.rb
index 6d28cdd2f..df0fa7eb1 100644
--- a/test/pagy/keyset_for_ui_test.rb
+++ b/test/pagy/keyset_for_ui_test.rb
@@ -17,7 +17,7 @@
records = pagy.records
_(records.size).must_equal 10
_(records.first.id).must_equal 13
- _(pagy.update).must_equal ['key', ["dog", "Denis", 44]]
+ _(pagy.update).must_equal ['key', 2, 1, ["dog", "Denis", 44]]
end
it 'uses :jsonify_keyset_attributes' do
pagy = Pagy::KeysetForUI.new(model.order(:id),
@@ -27,7 +27,7 @@
jsonify_keyset_attributes: ->(attr) { attr.values.to_json })
_(pagy.next).must_equal(3)
_(pagy.instance_variable_get(:@cutoff_args)).must_equal(id: 10)
- _(pagy.update).must_equal ['key', [20]]
+ _(pagy.update).must_equal ['key', 2, 1, [20]]
end
end
describe 'handles the page/cut' do
@@ -36,7 +36,7 @@
limit: 10)
_(pagy.instance_variable_get(:@cut)).must_be_nil
_(pagy.next).must_equal 2
- _(pagy.update).must_equal [nil, [10]]
+ _(pagy.update).must_equal [nil, 1, 1, [10]]
end
it 'handles the page/cut for the second page' do
pagy = Pagy::KeysetForUI.new(model.order(:id),
@@ -46,7 +46,7 @@
_(pagy.instance_variable_get(:@cutoff_args)).must_equal(id: 10)
_(pagy.records.first.id).must_equal 11
_(pagy.next).must_equal 3
- _(pagy.update).must_equal ['key', [20]]
+ _(pagy.update).must_equal ['key', 2, 1, [20]]
end
it 'handles the page/cut for the last page' do
pagy = Pagy::KeysetForUI.new(model.order(:id),
@@ -61,11 +61,12 @@
end
describe 'handles overflow' do
it 'reset on overflow' do
- pagy = Pagy::KeysetForUI.new(model.order(:id),
- cutoffs: ['key', 2, [20]],
- limit: 10,
- page: 3)
- _(pagy.update).must_equal [nil, [10]]
+ _ do
+ Pagy::KeysetForUI.new(model.order(:id),
+ cutoffs: ['key', 2, [20]],
+ limit: 10,
+ page: 3)
+ end.must_raise Pagy::OverflowError
end
end
describe 'handles the jumping back' do