PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.1.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.1.0
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
subscription / assets / js / admin-components / pagination.js

pagination.js in Subscriptions for WooCommerce with Stripe Recurring Payments 2.1.0, at assets/js/admin-components/pagination.js

444 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPSubsPager — paginator footer (prev / next / numbers / ellipsis) that
3 * controls a slice of rows inside a card. Single source of truth so the
4 * server-rendered markup from `wpsubs_render_pager()` and the client
5 * re-renderings stay byte-identical.
6 *
7 * Usage:
8 * PHP: wpsubs_render_pager( $args ) // emits a .wpsubs-pager[data-wpsubs-pager]
9 * JS: WPSubsPager.init() // auto-inits those elements
10 *
11 * Markup contract (data-* on .wpsubs-pager):
12 * data-current — current page (1-indexed, int)
13 * data-total — total pages (int)
14 * data-per-page — items per page (int)
15 * data-link-mode — 'url' (default) | 'cb' (callback / data-page buttons)
16 * data-info-format — optional sprintf string for the "Showing X–Y of Z" text
17 *
18 * Row scope:
19 * data-wpsubs-pager-scope (on the pager OR an ancestor) — a CSS selector for
20 * the container holding the rows to paginate. Defaults to the closest <table>
21 * inside the pager's card. Rows are direct children matched by `row_selector`
22 * on the scope element (default 'tbody tr'). Pro's non-table layouts can
23 * override both with data attributes.
24 *
25 * Events fired on the pager root (bubbles):
26 * wpsubs:pager:change — { page, total, pages, perPage }
27 */
28 (function () {
29 "use strict";
30
31 var MONTHS = [
32 "January",
33 "February",
34 "March",
35 "April",
36 "May",
37 "June",
38 "July",
39 "August",
40 "September",
41 "October",
42 "November",
43 "December",
44 ];
45
46 // Same algorithm as PHP wpsubs_pager_page_range(): first, last, current ± 1,
47 // gaps collapse to a single ellipsis (gap == 2 surfaces the missing page).
48 function pageRange(current, total) {
49 var first = 1;
50 var last = total;
51 current = Math.max(1, Math.min(total, current));
52 var nearStart = Math.max(2, current - 1);
53 var nearEnd = Math.min(last - 1, current + 1);
54 var nearby = [];
55 for (var i = nearStart; i <= nearEnd; i++) nearby.push(i);
56 var seen = {};
57 var parts = [];
58 function push(n) {
59 if (!seen[n]) {
60 seen[n] = true;
61 parts.push(n);
62 }
63 }
64 push(first);
65 nearby.forEach(push);
66 if (last > first) push(last);
67 var range = [];
68 for (var j = 0; j < parts.length; j++) {
69 var p = parts[j];
70 if (j > 0) {
71 var gap = p - parts[j - 1];
72 if (gap === 2) range.push(parts[j - 1] + 1);
73 else if (gap > 2) range.push(null);
74 }
75 range.push(p);
76 }
77 return range;
78 }
79
80 function el(tag, className, attrs) {
81 var node = document.createElement(tag);
82 if (className) node.className = className;
83 if (attrs) {
84 for (var k in attrs)
85 if (Object.prototype.hasOwnProperty.call(attrs, k)) {
86 if (k === "html") node.innerHTML = attrs[k];
87 else if (k === "text") node.textContent = attrs[k];
88 else if (k === "data") {
89 for (var dk in attrs.data) node.setAttribute("data-" + dk, attrs.data[dk]);
90 } else node.setAttribute(k, attrs[k]);
91 }
92 }
93 return node;
94 }
95
96 function readInt(el, name, fallback) {
97 var v = el.getAttribute(name);
98 if (v === null || v === "") return fallback;
99 var n = parseInt(v, 10);
100 return isNaN(n) ? fallback : n;
101 }
102
103 function i18nInfo(format, start, end, total) {
104 if (!format) return "";
105 if (typeof wp !== "undefined" && wp.i18n && typeof wp.i18n.__ === "function") {
106 return wp.i18n
107 .__(format, "subscription")
108 .replace("%1$s", String(start))
109 .replace("%2$s", String(end))
110 .replace("%3$s", String(total));
111 }
112 return format.replace("%1$s", String(start)).replace("%2$s", String(end)).replace("%3$s", String(total));
113 }
114
115 function formatInfo(format, start, end, total) {
116 if (typeof wp !== "undefined" && wp.i18n && typeof wp.i18n.sprintf === "function") {
117 return wp.i18n.sprintf(format, String(start), String(end), String(total));
118 }
119 return format.replace("%1$s", String(start)).replace("%2$s", String(end)).replace("%3$s", String(total));
120 }
121
122 /**
123 * @param {HTMLElement} root .wpsubs-pager[data-wpsubs-pager]
124 */
125 function WPSubsPager(root) {
126 this.root = root;
127 this.scope = root.getAttribute("data-wpsubs-pager-scope")
128 ? document.querySelector(root.getAttribute("data-wpsubs-pager-scope"))
129 : root.closest("[data-wpsubs-pager-scope]") || this.findScope();
130 this.rowSelector = root.getAttribute("data-pager-row-selector") || "tbody tr";
131 this.dateCol = readInt(root, "data-date-col", -1);
132 this.dateAdv = null;
133 this.perPageAdv = null;
134 this.linkMode = root.getAttribute("data-link-mode") || "url";
135 this.infoFormat = root.getAttribute("data-info-format") || "";
136 this.card = root.closest(".subscrpt-card, .wpsubs-table-card, [data-wpsubs-pager-card]") || root.parentElement;
137 this.rows = this.collectRows();
138 this.page = readInt(root, "data-current", 1);
139 this.total = readInt(root, "data-total", 1);
140 this.perPage = readInt(root, "data-per-page", 10);
141 this.emptyRow = null;
142 this.colSpan = 1;
143 this._bind();
144 }
145
146 WPSubsPager.prototype.findScope = function () {
147 // Default: closest <table> within the same card.
148 var table = this.root.closest(".subscrpt-card, .wpsubs-table-card, [data-wpsubs-pager-card]");
149 if (table) {
150 var t = table.querySelector("table");
151 if (t) return t;
152 }
153 return this.root.parentElement;
154 };
155
156 WPSubsPager.prototype.collectRows = function () {
157 if (!this.scope) return [];
158 return Array.prototype.slice.call(this.scope.querySelectorAll(this.rowSelector));
159 };
160
161 WPSubsPager.prototype._bind = function () {
162 var self = this;
163 if (this.card) {
164 this.dateAdv = this.card.querySelector(".subscrpt-filter-date");
165 this.perPageAdv = this.card.querySelector(".subscrpt-filter-perpage");
166 var table = this.card.querySelector("table");
167 if (table) {
168 this.colSpan = table.querySelectorAll("thead th").length || 1;
169 }
170 }
171
172 this.root.addEventListener("click", function (e) {
173 var btn = e.target.closest(".wpsubs-pagination__btn");
174 if (!btn || !self.root.contains(btn)) return;
175 if (btn.classList.contains("wpsubs-pagination__btn--ellipsis")) return;
176 if (btn.classList.contains("wpsubs-pagination__btn--disabled")) return;
177 if (btn.classList.contains("wpsubs-pagination__btn--active")) return;
178 var page = parseInt(btn.getAttribute("data-page"), 10);
179 if (!isNaN(page) && page !== self.page) {
180 self.page = page;
181 self.render();
182 }
183 });
184
185 if (this.dateAdv) {
186 this.dateAdv.addEventListener("wpsubs:select", function () {
187 self.page = 1;
188 self.render();
189 });
190 }
191 if (this.perPageAdv) {
192 this.perPageAdv.addEventListener("wpsubs:select", function () {
193 var input = self.perPageAdv.querySelector('input[type="hidden"]');
194 self.perPage = parseInt(input ? input.value : self.perPage, 10) || self.perPage;
195 self.page = 1;
196 self.render();
197 });
198 }
199 };
200
201 WPSubsPager.prototype._monthKey = function (tr) {
202 if (!tr || this.dateCol < 0) return "";
203 var cell = tr.children[this.dateCol];
204 if (!cell) return "";
205 var d = new Date(cell.textContent.replace(" - ", " ").trim());
206 if (isNaN(d.getTime())) return "";
207 return d.getFullYear() + "-" + ("0" + (d.getMonth() + 1)).slice(-2);
208 };
209
210 WPSubsPager.prototype._injectMonthOptions = function (rows) {
211 if (!this.dateAdv || this.dateCol < 0) return;
212 var menu = this.dateAdv.querySelector(".wpsubs-adv-select__menu");
213 if (!menu) return;
214 // Remove previously injected month options (any non-original item).
215 var injected = menu.querySelectorAll("[data-pager-month]");
216 injected.forEach(function (n) {
217 n.parentNode.removeChild(n);
218 });
219 var seen = {};
220 var months = [];
221 rows.forEach(function (tr) {
222 var key = this._monthKey(tr);
223 if (key && !seen[key]) {
224 seen[key] = true;
225 months.push(key);
226 }
227 }, this);
228 months
229 .sort()
230 .reverse()
231 .forEach(function (key) {
232 var parts = key.split("-");
233 var btn = document.createElement("button");
234 btn.type = "button";
235 btn.setAttribute("data-pager-month", "1");
236 btn.className = "wpsubs-adv-select__item";
237 btn.setAttribute("data-value", key);
238 btn.setAttribute("role", "option");
239 var span = document.createElement("span");
240 span.className = "wpsubs-adv-select__item-label";
241 span.textContent = MONTHS[parseInt(parts[1], 10) - 1] + " " + parts[0];
242 btn.appendChild(span);
243 menu.appendChild(btn);
244 }, this);
245 };
246
247 WPSubsPager.prototype._advValue = function (adv) {
248 if (!adv) return "";
249 var input = adv.querySelector('input[type="hidden"]');
250 return input ? input.value : "";
251 };
252
253 WPSubsPager.prototype._filtered = function () {
254 var dm = this._advValue(this.dateAdv);
255 if (!dm || this.dateCol < 0) return this.rows;
256 return this.rows.filter(function (tr) {
257 return this._monthKey(tr) === dm;
258 }, this);
259 };
260
261 WPSubsPager.prototype._makeBtn = function (label, target, isDisabled, isActive) {
262 if (isDisabled) {
263 return el("span", "wpsubs-pagination__btn wpsubs-pagination__btn--disabled", {
264 "aria-hidden": "true",
265 html: label,
266 });
267 }
268 if (isActive) {
269 return el("span", "wpsubs-pagination__btn wpsubs-pagination__btn--active", {
270 "aria-current": "page",
271 text: label,
272 });
273 }
274 if (this.linkMode === "cb") {
275 return el("button", "wpsubs-pagination__btn", {
276 type: "button",
277 "data-page": String(target),
278 text: label,
279 });
280 }
281 return el("a", "wpsubs-pagination__btn", { "data-page": String(target), text: label });
282 };
283
284 WPSubsPager.prototype._makeEllipsis = function () {
285 return el("span", "wpsubs-pagination__btn wpsubs-pagination__btn--ellipsis", {
286 "aria-hidden": "true",
287 text: "…",
288 });
289 };
290
291 WPSubsPager.prototype._buildPager = function (pages) {
292 var frag = document.createDocumentFragment();
293 frag.appendChild(this._makeBtn("‹", this.page - 1, this.page <= 1, false));
294 var range = pageRange(this.page, pages);
295 for (var i = 0; i < range.length; i++) {
296 var p = range[i];
297 frag.appendChild(p === null ? this._makeEllipsis() : this._makeBtn(String(p), p, false, p === this.page));
298 }
299 frag.appendChild(this._makeBtn("›", this.page + 1, this.page >= pages, false));
300 return frag;
301 };
302
303 WPSubsPager.prototype._updateInfo = function (start, end, total) {
304 if (!this.infoFormat) return;
305 var span = this.root.querySelector(".wpsubs-pagination__info");
306 if (!span) return;
307 span.textContent = formatInfo(this.infoFormat, total ? start + 1 : 0, Math.min(end, total), total);
308 };
309
310 WPSubsPager.prototype._showEmpty = function (show) {
311 if (!this.scope) return;
312 if (show) {
313 if (!this.emptyRow) {
314 var tr = document.createElement("tr");
315 tr.className = "subscrpt-empty-row";
316 var td = document.createElement("td");
317 td.colSpan = this.colSpan;
318 td.style.textAlign = "center";
319 td.style.padding = "18px";
320 td.textContent =
321 typeof wp !== "undefined" && wp.i18n && typeof wp.i18n.__ === "function"
322 ? wp.i18n.__("No matching records.", "subscription")
323 : "No matching records.";
324 tr.appendChild(td);
325 if (this.scope.tagName === "TBODY" || this.scope.tagName === "TABLE") {
326 this.scope.appendChild(tr);
327 } else {
328 this.scope.appendChild(tr);
329 }
330 this.emptyRow = tr;
331 }
332 this.emptyRow.style.display = "";
333 } else if (this.emptyRow) {
334 this.emptyRow.style.display = "none";
335 }
336 };
337
338 WPSubsPager.prototype.render = function () {
339 var list = this._filtered();
340 var pages = Math.max(1, Math.ceil(list.length / this.perPage));
341 if (this.page > pages) this.page = pages;
342 var start = (this.page - 1) * this.perPage;
343 var end = start + this.perPage;
344 this.total = pages;
345
346 this.rows.forEach(function (tr) {
347 tr.style.display = "none";
348 });
349 list.slice(start, end).forEach(function (tr) {
350 tr.style.display = "";
351 });
352
353 this._showEmpty(list.length === 0);
354
355 // Rebuild just the nav portion, leave the root attributes alone.
356 var existingNav = this.root.querySelector(".wpsubs-pagination__nav");
357 if (existingNav) existingNav.parentNode.removeChild(existingNav);
358 var nav = el("div", "wpsubs-pagination__nav");
359 nav.appendChild(this._buildPager(pages));
360 this.root.appendChild(nav);
361
362 // Keep current/total data-* in sync so subsequent re-inits read the right state.
363 this.root.setAttribute("data-current", String(this.page));
364 this.root.setAttribute("data-total", String(pages));
365
366 this._updateInfo(start, end, list.length);
367
368 this.root.dispatchEvent(
369 new CustomEvent("wpsubs:pager:change", {
370 bubbles: true,
371 detail: { page: this.page, total: list.length, pages: pages, perPage: this.perPage },
372 }),
373 );
374 };
375
376 WPSubsPager.prototype.refresh = function () {
377 // External hook: re-collect rows (Pro renders new content) and re-paginate.
378 this.rows = this.collectRows();
379 this._injectMonthOptions(this.rows);
380 this.render();
381 };
382
383 WPSubsPager.prototype.goTo = function (page) {
384 page = parseInt(page, 10);
385 if (isNaN(page)) return;
386 var pages = Math.max(1, Math.ceil(this.rows.length / this.perPage));
387 this.page = Math.max(1, Math.min(pages, page));
388 this.render();
389 };
390
391 /**
392 * Initialise all un-initialised .wpsubs-pager elements under root.
393 * Skips pagers in `link-mode="url"` — those are server-rendered (e.g. the
394 * subscriptions list page) and own all the data they need; touching them
395 * from JS would re-derive totals from the current page's rows and clobber
396 * the correct pagination (e.g. show only "1" of 3 pages).
397 *
398 * @param {Document|HTMLElement} [root]
399 */
400 function init(root) {
401 (root || document)
402 .querySelectorAll(".wpsubs-pager[data-wpsubs-pager][data-link-mode='cb']:not([data-pager-init])")
403 .forEach(function (el) {
404 el.setAttribute("data-pager-init", "1");
405 var pager = new WPSubsPager(el);
406 pager._injectMonthOptions(pager.rows);
407 pager.render();
408 });
409 }
410
411 if (document.readyState === "loading") {
412 document.addEventListener("DOMContentLoaded", function () {
413 init();
414 });
415 } else {
416 init();
417 }
418
419 // Public API
420 window.WPSubsPager = {
421 init: init,
422 /**
423 * Re-collect rows for every initialised cb-mode pager under root and
424 * re-render. Useful when a callback injects new rows (e.g. Pro activities).
425 */
426 refresh: function (root) {
427 (root || document).querySelectorAll(".wpsubs-pager[data-link-mode='cb'][data-pager-init]").forEach(function (el) {
428 el.removeAttribute("data-pager-init");
429 init(el.parentNode || document);
430 });
431 },
432 /**
433 * Force a single pager to re-collect rows (Pro hook) and re-render.
434 * Skips url-mode pagers.
435 */
436 refreshOne: function (el) {
437 if (!el) return;
438 if (el.getAttribute("data-link-mode") !== "cb") return;
439 el.removeAttribute("data-pager-init");
440 init(el.parentNode || document);
441 },
442 };
443 })();
444