/** * Enhanced Table Component * Features: * - Sortable columns * - Filterable data * - Pagination * - Responsive design * - Loading states * - Empty states */ export class EnhancedTable { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = { columns: options.columns || [], data: options.data || [], sortable: options.sortable !== false, filterable: options.filterable !== false, paginated: options.paginated !== false, pageSize: options.pageSize || 10, emptyMessage: options.emptyMessage || 'No data available', onRowClick: options.onRowClick || null, ...options }; this.currentPage = 1; this.sortColumn = null; this.sortDirection = 'asc'; this.filterQuery = ''; this.filteredData = []; this.init(); } /** * Initialize table */ init() { if (!this.container) { console.error('[EnhancedTable] Container not found'); return; } this.filterData(); this.render(); } /** * Set data */ setData(data) { this.options.data = data || []; this.currentPage = 1; this.filterData(); this.render(); } /** * Filter data based on query */ filterData() { if (!this.filterQuery) { this.filteredData = [...this.options.data]; } else { const query = this.filterQuery.toLowerCase(); this.filteredData = this.options.data.filter(row => { return this.options.columns.some(col => { const value = this.getCellValue(row, col.field); return String(value).toLowerCase().includes(query); }); }); } // Apply sorting if (this.sortColumn) { this.applySorting(); } } /** * Apply sorting */ applySorting() { const column = this.options.columns.find(col => col.field === this.sortColumn); if (!column) return; this.filteredData.sort((a, b) => { const aVal = this.getCellValue(a, this.sortColumn); const bVal = this.getCellValue(b, this.sortColumn); let comparison = 0; if (typeof aVal === 'number' && typeof bVal === 'number') { comparison = aVal - bVal; } else { comparison = String(aVal).localeCompare(String(bVal)); } return this.sortDirection === 'asc' ? comparison : -comparison; }); } /** * Get cell value from row */ getCellValue(row, field) { if (typeof field === 'function') { return field(row); } return row[field]; } /** * Render table */ render() { if (!this.container) return; const html = ` ${this.options.filterable ? this.renderFilterBar() : ''}
${this.filteredData.length === 0 ? this.renderEmpty() : this.renderTable()}
${this.options.paginated ? this.renderPagination() : ''} `; this.container.innerHTML = html; this.attachEventListeners(); } /** * Render filter bar */ renderFilterBar() { return `
Showing ${this.filteredData.length} of ${this.options.data.length} items
`; } /** * Render table */ renderTable() { const start = (this.currentPage - 1) * this.options.pageSize; const end = this.options.paginated ? start + this.options.pageSize : this.filteredData.length; const pageData = this.filteredData.slice(start, end); return ` ${this.options.columns.map(col => this.renderHeaderCell(col)).join('')} ${pageData.map((row, index) => this.renderRow(row, start + index)).join('')}
`; } /** * Render header cell */ renderHeaderCell(column) { const sortable = this.options.sortable && column.sortable !== false; const isSorted = this.sortColumn === column.field; const sortIcon = isSorted ? (this.sortDirection === 'asc' ? '↑' : '↓') : ''; return `
${column.label} ${sortable ? `${sortIcon}` : ''}
`; } /** * Render row */ renderRow(row, index) { const clickable = this.options.onRowClick ? 'clickable' : ''; return ` ${this.options.columns.map(col => this.renderCell(row, col)).join('')} `; } /** * Render cell */ renderCell(row, column) { const value = this.getCellValue(row, column.field); const formatted = column.formatter ? column.formatter(value, row) : value; return ` ${formatted} `; } /** * Render empty state */ renderEmpty() { return `
📋
${this.options.emptyMessage}
`; } /** * Render pagination */ renderPagination() { const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize); if (totalPages <= 1) return ''; const pages = this.getPaginationPages(totalPages); return `
${pages.map(page => { if (page === '...') { return '...'; } return ` `; }).join('')}
`; } /** * Get pagination pages to display */ getPaginationPages(totalPages) { const delta = 2; const pages = []; for (let i = 1; i <= totalPages; i++) { if ( i === 1 || i === totalPages || (i >= this.currentPage - delta && i <= this.currentPage + delta) ) { pages.push(i); } else if (pages[pages.length - 1] !== '...') { pages.push('...'); } } return pages; } /** * Attach event listeners */ attachEventListeners() { this.container.addEventListener('click', (e) => { const action = e.target.closest('[data-action]')?.dataset.action; if (action === 'sort') { this.handleSort(e); } else if (action === 'prev-page') { this.handlePrevPage(); } else if (action === 'next-page') { this.handleNextPage(); } else if (action === 'goto-page') { this.handleGotoPage(e); } else if (action === 'row-click') { this.handleRowClick(e); } }); this.container.addEventListener('input', (e) => { if (e.target.dataset.action === 'filter') { this.handleFilter(e); } }); } /** * Handle sort */ handleSort(e) { const th = e.target.closest('th'); const field = th.dataset.field; if (this.sortColumn === field) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.sortColumn = field; this.sortDirection = 'asc'; } this.filterData(); this.render(); } /** * Handle filter */ handleFilter(e) { this.filterQuery = e.target.value; this.currentPage = 1; this.filterData(); this.render(); } /** * Handle previous page */ handlePrevPage() { if (this.currentPage > 1) { this.currentPage--; this.render(); } } /** * Handle next page */ handleNextPage() { const totalPages = Math.ceil(this.filteredData.length / this.options.pageSize); if (this.currentPage < totalPages) { this.currentPage++; this.render(); } } /** * Handle goto page */ handleGotoPage(e) { const page = parseInt(e.target.dataset.page); if (page && page !== this.currentPage) { this.currentPage = page; this.render(); } } /** * Handle row click */ handleRowClick(e) { const row = e.target.closest('tr'); const index = parseInt(row.dataset.index); const data = this.filteredData[index]; if (this.options.onRowClick && data) { this.options.onRowClick(data, index); } } /** * Destroy table */ destroy() { if (this.container) { this.container.innerHTML = ''; } } } export default EnhancedTable;