Skip to main content

javascript/chives-pagination.js

1// javascript/chives-pagination.js
2// chives version: 52
4// ChivesPagination manages the pagination of a collection of items.
5//
6// Usage:
7//
8// const pagination = new ChivesPagination(
9// { perPage: 200 }, new URLSearchParams(window.location.search)
10// );
11//
12// const itemsToShow = pagination.apply(items);
13//
14// const paginationState = pagination.getState(items.length);
15//
16// if (paginationState.totalPages > 1) {
17// const paginationElem = document.createElement("chives-pagination-nav");
18// paginationElem.state = paginationState;
19// paginationElem.searchParams = urlParams;
20// document.querySelector("#pagination").appendChild(paginationElem);
21// }
22//
23class ChivesPagination {
24 #perPage;
25 #searchParams;
27 // All my sites use "page" as the URL parameter for the current page.
28 #paramKey = "page";
30 constructor(config, searchParams) {
31 this.#perPage = config.perPage || 50;
32 this.#searchParams = searchParams;
33 }
35 // Returns the current page number (1-indexed).
36 getCurrentPage() {
37 const page = parseInt(this.#searchParams.get(this.#paramKey), 10);
38 return (!isNaN(page) && page > 0) ? page : 1;
39 }
41 // Returns the state of the pagination.
42 getState(totalItems) {
43 const currentPage = this.getCurrentPage();
44 const totalPages = Math.max(1, Math.ceil(totalItems / this.#perPage));
46 return {
47 currentPage,
48 totalPages,
49 // TODO: Allow callers to pass a perPage query parameter in
50 // the URL to customise page sizes.
51 perPage: this.#perPage,
52 paramKey: this.#paramKey,
53 }
54 }
56 // Extract the slice of items that should be shown on the current page.
57 apply(items) {
58 const { currentPage, perPage } = this.getState(items.length);
60 // Page numbers are 1-indexed, so page 1 corresponds to
61 // the indices 0…(perPage - 1).
62 const start = (currentPage - 1) * perPage;
63 const end = currentPage * perPage;
65 return items.slice(start, end);
66 }
69// ChivesPaginationNav provides a <chives-pagination-nav> component that
70// renders navigation controls for pagination.
71//
72// It uses the state provided by ChivesPagination.
73class ChivesPaginationNav extends HTMLElement {
74 #state;
75 #searchParams;
77 set state(state) {
78 if (!state) return;
79 this.#state = state;
80 this.#render();
81 }
83 set searchParams(params) {
84 if (!params) return;
85 this.#searchParams = params;
86 this.#render();
87 }
89 #render() {
90 if (!this.#state || !this.#searchParams) {
91 return null;
92 }
94 const { currentPage, totalPages } = this.#state;
96 if (totalPages === 1) {
97 return null;
98 }
100 const wrapper = document.createElement("nav");
101 wrapper.classList.add("chives_pagination");
103 if (currentPage > 1) {
104 const prevLink = document.createElement("a");
105 prevLink.innerHTML = "&larr; previous";
106 // If you're paginated too far, clamp you to the max page
107 prevLink.href = this.#updatePage(Math.min(currentPage - 1, totalPages));
108 wrapper.appendChild(prevLink);
109 }
111 const indicator = document.createElement("span");
112 indicator.classList.add("page_indicator");
113 indicator.innerText = `Page ${currentPage} of ${totalPages}`;
114 wrapper.appendChild(indicator);
116 if (currentPage < totalPages) {
117 const nextLink = document.createElement("a");
118 nextLink.innerHTML = "next &rarr;";
119 nextLink.href = this.#updatePage(currentPage + 1);
120 wrapper.appendChild(nextLink);
121 }
123 this.replaceChildren(wrapper);
124 }
126 // updatePage returns a copy of the URL search parameters with the
127 // new value for `page`.
128 #updatePage(page) {
129 let params = new URLSearchParams(this.#searchParams);
130 params.delete(this.#state.paramKey);
131 params.append(this.#state.paramKey, page);
132 return "?" + params.toString();
133 }
136// Register global styles for the <chives-pagination-nav> element.
137if (typeof document !== "undefined" &&
138 !document.getElementById("chives-pagination-styles")) {
139 const style = document.createElement("style");
140 style.id = "chives-pagination-styles";
141 style.textContent = `
142 .chives_pagination {
143 a + .page_indicator::before,
144 .page_indicator:has(+ a)::after {
145 content: " · ";
146 }
147 }
148 `;
149 document.head.appendChild(style);
152// Register the custom HTML tag <chives-pagination-nav>
153if (!customElements.get("chives-pagination-nav")) {
154 customElements.define("chives-pagination-nav", ChivesPaginationNav);
157// renderPaginationNav renders the pagination navigation as
158// a child of `querySelector`.
159function renderPaginationNav({ querySelector, pagination, searchParams, totalItems }) {
160 state = pagination.getState(totalItems);
162 if (state.totalPages > 1) {
163 const elem = document.createElement("chives-pagination-nav");
164 elem.state = state;
165 elem.searchParams = searchParams;
166 document.querySelector(querySelector).appendChild(elem);
167 }