import type { SubmissionItem, SubmissionRendererOptions } from './consts'; /** * @class SubmissionsRenderer * Renders the submissions list and handles pagination. */ export default class SubmissionsRenderer { public paginationDiv: HTMLDivElement; public submissionListDiv: HTMLDivElement; public prevPageLabel: string; public nextPageLabel: string; constructor(public options: SubmissionRendererOptions) { this.options.currentPage = this.options.currentPage || 1; this.submissionListDiv = document.createElement('div'); this.submissionListDiv.className = 'submissions__list'; this.paginationDiv = document.createElement('div'); this.paginationDiv.className = 'submissions__pagination'; this.prevPageLabel = window?.petitionerSubmissionSettings?.labels?.prevPage || 'Previous page'; this.nextPageLabel = window?.petitionerSubmissionSettings?.labels?.nextPage || 'Next page'; if (!this.options.wrapper) { throw new Error('Element not found'); } } /** * Format a MySQL datetime string (e.g. "2026-05-26 13:08:14") as a * human-readable date without time. */ public formatDate(val: string): string { // Take just the date portion to avoid timezone shifts const datePart = val.split(' ')[0]; const date = new Date(datePart + 'T00:00:00'); if (isNaN(date.getTime())) { return val; } return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric', }); } public attachEventListeners() { if (!this.paginationDiv) return; this.paginationDiv.addEventListener('click', async (event) => { const target = (event.target as HTMLElement).closest('button'); if (target && !target.disabled && target.dataset.page) { const page = parseInt(target.dataset.page, 10); if (!isNaN(page) && page !== this.options.currentPage) { this.options.currentPage = page; const newSubmissions = await this.options.onPageChange(page); this.options.submissions = newSubmissions; this.update(); } } }); } public render() { if (!this.options.submissions) { return; } this.options.wrapper.appendChild(this.submissionListDiv); this.options.wrapper.appendChild(this.paginationDiv); this.submissionListDiv.innerHTML = this.renderSubmissionsList(); this.paginationDiv.innerHTML = this.renderPagination(); this.attachEventListeners(); } public update() { // Update the submissions list this.submissionListDiv.innerHTML = this.renderSubmissionsList(); // Update the pagination completely to reflect ellipses and new active states this.paginationDiv.innerHTML = this.renderPagination(); } public renderSubmissionsList() { if ( !this.options.submissions || this.options.submissions.length === 0 ) { return ''; } return this.options.submissions .map((submission) => { return this.renderSubmissionItem(submission); }) .join(', '); } public renderSubmissionItem(submission: SubmissionItem): string { return `${submission.name}`; } private getPaginationRange( totalPages: number, currentPage: number ): (number | string)[] { const adjacentPages = 1; const range: number[] = []; const rangeWithDots: (number | string)[] = []; let lastNum = 0; const start = Math.max(1, currentPage - adjacentPages); const end = Math.min(totalPages, currentPage + adjacentPages); range.push(1); for (let i = start; i <= end; i++) { if (i > 1 && i < totalPages) { range.push(i); } } if (totalPages > 1) { range.push(totalPages); } // Insert ellipses where there are gaps for (const i of range) { if (lastNum > 0 && i - lastNum !== 1) { rangeWithDots.push('...'); } rangeWithDots.push(i); lastNum = i; } return rangeWithDots; } public renderPagination(): string { if ( !this.options.total || !this.options.perPage || !this.options.pagination ) { return ''; } const totalPages = Math.ceil(this.options.total / this.options.perPage); if (totalPages <= 1) { return ''; } const currentPage = this.options.currentPage || 1; let paginationHTML = '
'; // Prev Button if (currentPage > 1) { paginationHTML += ``; } else { paginationHTML += ``; } if (!this.options.hidePageNumbers) { const rangeWithDots = this.getPaginationRange( totalPages, currentPage ); // Render the numbers and dots for (const page of rangeWithDots) { if (page === '...') { paginationHTML += `...`; } else { paginationHTML += ``; } } } // Next Button if (currentPage < totalPages) { paginationHTML += ``; } else { paginationHTML += ``; } paginationHTML += '
'; return paginationHTML; } } export class SubmissionsRendererTable extends SubmissionsRenderer { constructor(public options: SubmissionRendererOptions) { super(options); } public render() { if (!this.options.submissions) { return; } this.options.wrapper.appendChild(this.submissionListDiv); this.options.wrapper.appendChild(this.paginationDiv); this.options.wrapper.style = this.getWrapperStyles(); this.submissionListDiv.innerHTML = this.renderSubmissionsList(); this.paginationDiv.innerHTML = this.renderPagination(); this.attachEventListeners(); } public renderSubmissionsList() { if ( !this.options.submissions || this.options.submissions.length === 0 ) { return ''; } const labels = this.prepareLabels(); const list = this.options.submissions .map((submission) => { return this.renderSubmissionItem(submission); }) .join(''); return `
${labels .map((key) => { return `
${key}
`; }) .join('')}
${list}`; } public prepareLabels() { // get 1 entry from submissions and map out existing fields const labels: string[] = []; const submissionEntry = this.options.submissions[0]; Object.keys(submissionEntry).forEach((key: string) => { if ( !this.options.labels?.[key] || !this.options.fields.includes(key) ) { return; } labels.push(this.options.labels?.[key]); }); return labels; } public renderSubmissionItem(submission: SubmissionItem): string { const filteredKeys = Object.keys(submission).filter( (key) => this.options.labels && key in this.options.labels && this.options.fields.includes(key) ); const isFeatured = submission.is_featured === '1'; const finalClassNames = ['submissions__item']; if (isFeatured) { finalClassNames.push('submissions__item--featured'); } return `
${filteredKeys .map((key) => { const renderedValue = key === 'submitted_at' ? this.formatDate(String(submission[key])) : submission?.[key]; return `
${this.options.labels?.[key] || key}: ${renderedValue ? renderedValue : ''}
`; }) .join('')}
`; } /** * * @returns string final CSS for the wrapper */ public getWrapperStyles() { const labels = this.prepareLabels(); return `--ptr-submission-columns: ${labels.length}`; } }