@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
253 lines (224 loc) • 8.69 kB
JavaScript
import { getQueryParams, setQueryParams, listenQueryParamsChange } from './Router.js';
class AgPagination extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._gridId = null;
let queryParams = getQueryParams();
this._currentPage = parseInt(queryParams.page, 10) || 1;
this._limit = parseInt(queryParams.limit, 10) || 10;
this._totalPages = 1;
this._totalItems = 0;
this._limitOptions = [10, 20, 50, 100]; // Default options
this.handlePageChange = this.handlePageChange.bind(this);
this.handleLimitChange = this.handleLimitChange.bind(this);
this.handleQueryParamsChange = this.handleQueryParamsChange.bind(this);
}
static get observedAttributes() {
return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit', 'limit-options'];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case 'grid-id':
this._gridId = newValue;
break;
case 'current-page':
this._currentPage = parseInt(newValue, 10) || this._currentPage;
break;
case 'total-pages':
this._totalPages = parseInt(newValue, 10) || 1;
break;
case 'total-items':
this._totalItems = parseInt(newValue, 10) || 0;
break;
case 'limit':
this._limit = parseInt(newValue, 10) || this._limit;
break;
case 'limit-options':
try {
const parsed = JSON.parse(newValue);
if (Array.isArray(parsed) && parsed.length > 0) {
this._limitOptions = parsed.map((v) => parseInt(v, 10)).filter((v) => !isNaN(v) && v > 0);
}
} catch (e) {
console.warn('Invalid limit-options format, using defaults');
}
break;
}
this.update();
}
connectedCallback() {
this.render();
this.addEventListeners();
// Subscribe to query parameter changes once the component is connected and _gridId is potentially set.
listenQueryParamsChange({
id: `ag-pagination-${this._gridId || 'default'}`, // Use _gridId for a unique listener ID
event: this.handleQueryParamsChange,
});
// The initial state is already set in the constructor from getQueryParams().
// The `listenQueryParamsChange` in `Router.js` will trigger an immediate (setTimeout) call
// to `handleQueryParamsChange`, which will re-sync the state if needed and update the component.
this.update(); // Keep the initial update for rendering based on constructor's initial state.
}
disconnectedCallback() {
// If Router.js held strong references to queryParamsChangeListeners, you might need to unsubscribe here.
// However, for this example, we'll assume the component's lifecycle or weak references handle cleanup.
}
handleQueryParamsChange(queryParams) {
const newPage = parseInt(queryParams.page, 10) || 1;
const newLimit = parseInt(queryParams.limit, 10) || 10;
let shouldUpdate = false;
if (newPage !== this._currentPage) {
this._currentPage = newPage;
shouldUpdate = true;
}
if (newLimit !== this._limit) {
this._limit = newLimit;
shouldUpdate = true;
}
if (shouldUpdate) {
this.update();
this.dispatchEvent(
new CustomEvent('pagination-query-params-change', { detail: { page: this._currentPage, limit: this._limit } }),
);
}
}
handlePageChange(newPage) {
if (newPage < 1 || newPage > this._totalPages || newPage === this._currentPage) {
return;
}
this._currentPage = newPage;
setQueryParams({ page: newPage, limit: this._limit }, { replace: false }); // Use pushState
this.dispatchEvent(new CustomEvent('page-change', { detail: { page: newPage } }));
}
handleLimitChange(event) {
const newLimit = parseInt(event.target.value, 10);
if (newLimit === this._limit) {
return;
}
this._limit = newLimit;
this._currentPage = 1; // Reset to first page on limit change
setQueryParams({ page: 1, limit: newLimit }, { replace: false }); // Use pushState
this.dispatchEvent(new CustomEvent('limit-change', { detail: { limit: newLimit } }));
}
update() {
if (!this.shadowRoot.querySelector('#page-info')) return;
const isFirstPage = this._currentPage === 1;
const isLastPage = this._currentPage === this._totalPages;
this.shadowRoot.querySelector('#first-page').disabled = isFirstPage;
this.shadowRoot.querySelector('#prev-page').disabled = isFirstPage;
this.shadowRoot.querySelector('#next-page').disabled = isLastPage;
this.shadowRoot.querySelector('#last-page').disabled = isLastPage;
const startItem = this._totalItems > 0 ? (this._currentPage - 1) * this._limit + 1 : 0;
const endItem = Math.min(this._currentPage * this._limit, this._totalItems);
this.shadowRoot.querySelector('#summary-info').textContent = `${startItem} - ${endItem} of ${this._totalItems}`;
this.shadowRoot.querySelector('#page-info').textContent = `Page ${this._currentPage} of ${this._totalPages}`;
const limitSelector = this.shadowRoot.querySelector('#limit-selector');
if (limitSelector && limitSelector.value != this._limit) {
// Added null check for limitSelector
limitSelector.value = this._limit;
}
this.renderPageButtons();
}
renderPageButtons() {
const pageButtonsContainer = this.shadowRoot.querySelector('#page-buttons');
pageButtonsContainer.innerHTML = '';
const maxButtons = 5;
let startPage = Math.max(1, this._currentPage - Math.floor(maxButtons / 2));
let endPage = Math.min(this._totalPages, startPage + maxButtons - 1);
if (endPage - startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
for (let i = startPage; i <= endPage; i++) {
const button = document.createElement('button');
button.textContent = i;
button.disabled = i === this._currentPage;
if (i === this._currentPage) {
button.classList.add('active');
}
button.addEventListener('click', () => this.handlePageChange(i));
pageButtonsContainer.appendChild(button);
}
}
addEventListeners() {
this.shadowRoot.querySelector('#first-page').addEventListener('click', () => this.handlePageChange(1));
this.shadowRoot
.querySelector('#prev-page')
.addEventListener('click', () => this.handlePageChange(this._currentPage - 1));
this.shadowRoot
.querySelector('#next-page')
.addEventListener('click', () => this.handlePageChange(this._currentPage + 1));
this.shadowRoot
.querySelector('#last-page')
.addEventListener('click', () => this.handlePageChange(this._totalPages));
this.shadowRoot.querySelector('#limit-selector').addEventListener('change', this.handleLimitChange);
}
render() {
const limitOptionsHtml = this._limitOptions.map((value) => `<option value="${value}">${value}</option>`).join('');
this.shadowRoot.innerHTML = html`
<style>
:host {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
font-family: sans-serif;
font-size: 14px;
gap: 8px;
}
button {
border: 1px solid #ccc;
background-color: #f0f0f0;
padding: 6px 12px;
cursor: pointer;
border-radius: 4px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
button.active {
border-color: #007bff;
background-color: #007bff;
color: white;
}
#page-info {
min-width: 80px;
text-align: center;
}
#page-buttons {
display: flex;
gap: 4px;
}
.summary-panel,
.page-summary-panel {
display: flex;
align-items: center;
gap: 8px;
}
#limit-selector {
border: 1px solid #ccc;
background-color: #f0f0f0;
padding: 6px 12px;
border-radius: 4px;
}
</style>
<div class="summary-panel">
<span id="summary-info"></span>
</div>
<button id="first-page">First</button>
<button id="prev-page">Previous</button>
<div class="page-summary-panel">
<div id="page-buttons"></div>
<span id="page-info"></span>
</div>
<button id="next-page">Next</button>
<button id="last-page">Last</button>
<select id="limit-selector">
${limitOptionsHtml}
</select>
`;
}
}
customElements.define('ag-pagination', AgPagination);
export { AgPagination };