94 lines
4.5 KiB
JavaScript
94 lines
4.5 KiB
JavaScript
// 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: <dmr-portal>/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) =>
|
||
`<button class="btn btn-sm ${cls} ${cur ? 'btn-primary' : 'btn-outline-secondary'} mx-1"${dis ? ' disabled' : ''}${target != null ? ` data-pg="${target}"` : ''} style="padding:1px 8px;">${label}</button>`;
|
||
let h = `<div class="d-flex align-items-center flex-wrap justify-content-center mt-2" style="gap:2px;">`;
|
||
h += `<span class="small text-secondary me-2">${total} ${noun} · page ${page}/${pages}</span>`;
|
||
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 + `</div>`;
|
||
}
|
||
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) =>
|
||
`<button class="btn btn-sm ${isCurrent ? 'btn-primary' : 'btn-outline-secondary'} mx-1"`
|
||
+ (disabled ? ' disabled' : '')
|
||
+ (target != null ? ` data-pg="${target}"` : '')
|
||
+ `>${label}</button>`;
|
||
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 =
|
||
`<div class="d-flex justify-content-center align-items-center mt-2 small">` +
|
||
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) +
|
||
`<span class="text-secondary ms-2">${cur} / ${total} (${totalItems} rows)</span>` +
|
||
`</div>`;
|
||
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;
|
||
})();
|