// SvxPortal — web front-end for SvxReflector
// Copyright (C) 2026 SA7AUX (Björn)
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version. See the LICENSE file for details.
// pager.js — shared pagination (dmr-web, svx-web).
// Canonical copy: callmgr2/www-mumien/lib/pager.js; other repos carry a COPY.
//
// pageSizeFor('monitorBody', 32, 160) rows that fit below the element (min 5)
// pagerHtml({page, pages, total, noun, cls}) footer « ‹ 1..n › » as HTML;
// buttons carry class `cls` + data-pg, the page wires the click.
// paginate.slice / totalPages / render(container, total, pageSize, page, onChange)
// the container-driven pager (dmr-web lists), 1-based pages.
(function () {
'use strict';
function pageSizeFor(bodyId, rowPx, reservePx) {
const body = document.getElementById(bodyId);
const top = body ? body.getBoundingClientRect().top : 220;
const avail = window.innerHeight - top - (reservePx == null ? 160 : reservePx);
return Math.max(5, Math.floor(avail / (rowPx || 32)));
}
function pagerHtml(o) {
const page = o.page, pages = o.pages, total = o.total, cls = o.cls || 'pg', noun = o.noun || 'rows';
if (pages <= 1) return '';
const span = 3;
let from = Math.max(1, page - span), to = Math.min(pages, page + span);
while (to - from < 6 && (from > 1 || to < pages)) { if (from > 1) from--; else if (to < pages) to++; }
const btn = (label, target, dis, cur) =>
``;
let h = `
`;
h += `${total} ${noun} · page ${page}/${pages}`;
h += btn('«', 1, page === 1) + btn('‹', page - 1, page === 1);
for (let p = from; p <= to; p++) h += btn(String(p), p, false, p === page);
h += btn('›', page + 1, page === pages) + btn('»', pages, page === pages);
return h + `
`;
}
const paginate = {
slice(items, pageSize, page) {
if (!Array.isArray(items)) return [];
if (pageSize <= 0) return items;
const start = Math.max(0, (page - 1) * pageSize);
return items.slice(start, start + pageSize);
},
totalPages(totalItems, pageSize) {
if (pageSize <= 0 || totalItems <= 0) return 1;
return Math.max(1, Math.ceil(totalItems / pageSize));
},
render(container, totalItems, pageSize, page, onChange) {
if (!container) return;
const total = this.totalPages(totalItems, pageSize);
if (total <= 1) { container.innerHTML = ''; return; }
const cur = Math.min(Math.max(1, page), total);
const btn = (label, target, disabled, isCurrent) =>
``;
const span = 2;
let from = Math.max(1, cur - span);
let to = Math.min(total, cur + span);
while (to - from < 4 && (from > 1 || to < total)) {
if (from > 1) from--;
else if (to < total) to++;
}
let nums = '';
for (let p = from; p <= to; p++) nums += btn(String(p), p, false, p === cur);
container.innerHTML =
`
` +
btn('« First', 1, cur === 1, false) +
btn('‹ Prev', cur - 1, cur === 1, false) +
nums +
btn('Next ›', cur + 1, cur === total, false) +
btn('Last »', total, cur === total, false) +
`${cur} / ${total} (${totalItems} rows)` +
`
`;
container.onclick = (e) => {
const b = e.target.closest('button[data-pg]');
if (!b) return;
const p = parseInt(b.dataset.pg, 10);
if (!isNaN(p) && p >= 1 && p <= total && typeof onChange === 'function') {
onChange(p);
}
};
},
};
window.pageSizeFor = pageSizeFor;
window.pagerHtml = pagerHtml;
window.paginate = paginate;
})();