Add buildPaginationModel and ctx.assetHref, and bump the plugin contract to 0.5.0
CI / full-gate (push) Failing after 5s
CI / full-gate (push) Failing after 5s
This commit is contained in:
+37
-1
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { paginate, type PageModel } from "./paginate.ts";
|
||||
import { buildPaginationModel, paginate, type PageModel } from "./paginate.ts";
|
||||
|
||||
// Compact view of the page sequence: ellipsis → "…", current → "[n]", else the number.
|
||||
const shape = (m: PageModel): (number | string | null)[] =>
|
||||
@@ -41,3 +41,39 @@ test("paginate windows the sequence: single gaps fill, wider gaps ellipsize, sib
|
||||
assert.deepEqual(shape(paginate(200, 10, 10, { siblings: 2 })), [1, "…", 8, 9, "[10]", 11, 12, "…", 20]);
|
||||
assert.deepEqual(shape(paginate(200, 10, 10, { boundaries: 2 })), [1, 2, "…", 9, "[10]", 11, "…", 19, 20]);
|
||||
});
|
||||
|
||||
test("buildPaginationModel maps a page to the pagination partial's model, every href from hrefFor", () => {
|
||||
const hrefFor = (n: number): string => `/users?q=ann&sort=-name${n > 1 ? `&page=${n}` : ""}`;
|
||||
assert.deepEqual(buildPaginationModel(paginate(100, 2, 25), hrefFor, { label: "User pages", pageSizes: [25, 50] }), {
|
||||
label: "User pages",
|
||||
next: { href: "/users?q=ann&sort=-name&page=3" },
|
||||
pages: [
|
||||
{ href: "/users?q=ann&sort=-name", label: "1" },
|
||||
{ current: true, label: "2" },
|
||||
{ href: "/users?q=ann&sort=-name&page=3", label: "3" },
|
||||
{ href: "/users?q=ann&sort=-name&page=4", label: "4" },
|
||||
],
|
||||
prev: { href: "/users?q=ann&sort=-name" },
|
||||
rows: { hidden: [{ name: "q", value: "ann" }, { name: "sort", value: "-name" }], name: "pageSize", options: [25, 50], value: 25 },
|
||||
summary: { from: 26, to: 50, total: 100 },
|
||||
});
|
||||
});
|
||||
|
||||
test("buildPaginationModel disables the step off either end, keeps ellipses, and omits what wasn't asked for", () => {
|
||||
const m = buildPaginationModel(paginate(100, 1, 10), (n) => `/l?page=${n}`);
|
||||
assert.deepEqual(m.prev, {});
|
||||
assert.deepEqual(m.pages[m.pages.length - 2], { ellipsis: true });
|
||||
assert.equal("label" in m, false);
|
||||
assert.equal("rows" in m, false);
|
||||
assert.deepEqual(buildPaginationModel(paginate(100, 10, 10), (n) => `/l?page=${n}`).next, {});
|
||||
});
|
||||
|
||||
test("buildPaginationModel's rows form carries the links' list state, minus page, page size and locale", () => {
|
||||
const m = buildPaginationModel(paginate(10, 1, 50), () => "/l?status=active&status=locked&page=2&pageSize=50&locale=sv-SE", { pageSizes: [50] });
|
||||
assert.deepEqual(m.rows?.hidden, [{ name: "status", value: "active" }, { name: "status", value: "locked" }]);
|
||||
});
|
||||
|
||||
test("buildPaginationModel follows renamed page and page-size params, as parseListQuery takes them", () => {
|
||||
const m = buildPaginationModel(paginate(10, 1, 5), () => "/l?q=x&p=2&size=5", { pageParam: "p", pageSizeParam: "size", pageSizes: [5] });
|
||||
assert.deepEqual(m.rows, { hidden: [{ name: "q", value: "x" }], name: "size", options: [5], value: 5 });
|
||||
});
|
||||
|
||||
+46
-4
@@ -1,7 +1,7 @@
|
||||
// paginate: pagination math → the model pagination.ejs renders. Pure and
|
||||
// URL-free (README signature `paginate(total, page, pageSize)`); the caller maps each
|
||||
// page number to an href. Inputs are clamped/guarded so it never produces a broken model:
|
||||
// page is pinned to [1, pageCount], total/pageSize coerced to sane integers.
|
||||
// paginate: pagination math → a PageModel. Pure and URL-free (README signature
|
||||
// `paginate(total, page, pageSize)`); buildPaginationModel then maps it, with the caller's
|
||||
// page → href function, to the model pagination.ejs renders. Inputs are clamped/guarded so it never
|
||||
// produces a broken model: page is pinned to [1, pageCount], total/pageSize coerced to sane integers.
|
||||
|
||||
export interface PageItem {
|
||||
current: boolean;
|
||||
@@ -45,6 +45,48 @@ export function paginate(total: number, page: number, pageSize: number, options:
|
||||
};
|
||||
}
|
||||
|
||||
export interface PaginationModelOptions {
|
||||
label?: string; // the nav's aria-label; the partial defaults it
|
||||
pageParam?: string; // as parseListQuery's options (default "page")
|
||||
pageSizeParam?: string; // as parseListQuery's options (default "pageSize")
|
||||
pageSizes?: number[]; // the rows-per-page choices; omitted ⇒ no rows-per-page form
|
||||
}
|
||||
|
||||
export interface PaginationModel {
|
||||
label?: string;
|
||||
next: { href?: string };
|
||||
pages: ({ ellipsis: true } | { current: true; label: string } | { href: string; label: string })[];
|
||||
prev: { href?: string };
|
||||
rows?: { hidden: { name: string; value: string }[]; name: string; options: number[]; value: number };
|
||||
summary: { from: number; to: number; total: number };
|
||||
}
|
||||
|
||||
// `hrefFor(n)` is the list's URL on page n, carrying all its other state. The rows-per-page form is a
|
||||
// GET, which replaces the whole query string, so its hidden inputs are read off that same URL: the
|
||||
// form then carries exactly the state the links do. `locale` is left to the partial, which adds it.
|
||||
export function buildPaginationModel(page: PageModel, hrefFor: (page: number) => string, options: PaginationModelOptions = {}): PaginationModel {
|
||||
const step = (n: number | null): { href?: string } => (n === null ? {} : { href: hrefFor(n) });
|
||||
const model: PaginationModel = {
|
||||
next: step(page.next),
|
||||
pages: page.pages.map((p) =>
|
||||
p.page === null ? { ellipsis: true as const }
|
||||
: p.current ? { current: true as const, label: String(p.page) }
|
||||
: { href: hrefFor(p.page), label: String(p.page) }),
|
||||
prev: step(page.prev),
|
||||
summary: { from: page.from, to: page.to, total: page.total },
|
||||
};
|
||||
if (options.label !== undefined) model.label = options.label;
|
||||
if (options.pageSizes) {
|
||||
const name = options.pageSizeParam ?? "pageSize";
|
||||
const omit = new Set([options.pageParam ?? "page", name, "locale"]);
|
||||
const hidden = [...new URL(hrefFor(page.page), "http://x").searchParams]
|
||||
.filter(([key]) => !omit.has(key))
|
||||
.map(([key, value]) => ({ name: key, value }));
|
||||
model.rows = { hidden, name, options: options.pageSizes, value: page.pageSize };
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
const item = (page: number, current = false): PageItem => ({ current, ellipsis: false, page });
|
||||
const gap = (): PageItem => ({ current: false, ellipsis: true, page: null });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user