mcp-web-ui
Version:
Ultra-lightweight vanilla JavaScript framework for MCP servers - Zero dependencies, perfect security, 2-3KB bundle size
1,118 lines (989 loc) • 36 kB
JavaScript
/**
* TableComponent - Advanced Data Table Implementation
*
* This component provides a complete data table interface with:
* - Sortable columns with multiple sort types
* - Real-time filtering and search
* - Pagination for large datasets
* - Row selection and bulk actions
* - Responsive design for mobile devices
* - Export functionality
* - Customizable column types and formatting
*
* SECURITY FEATURES:
* - All data is sanitized through BaseComponent
* - XSS protection for all cell content
* - Safe HTML rendering in custom cells
* - Input validation for filter queries
* - Rate limiting on sort/filter operations
*
* AI INTEGRATION READY:
* - Handles dynamic data from LLM sources
* - Context-aware sanitization for different column types
* - Flexible column configuration for AI-generated schemas
* - Comprehensive logging for debugging
*
* Usage:
* const table = new TableComponent(element, data, config);
*
* Config options:
* - columns: Array of column definitions
* - sortable: Enable sorting (default: true)
* - filterable: Enable filtering (default: true)
* - pageSize: Items per page (default: 20)
* - selectable: Enable row selection (default: false)
* - exportable: Enable data export (default: false)
*/
class TableComponent extends BaseComponent {
/**
* Initialize TableComponent with table-specific state
* @param {HTMLElement} element - DOM element to attach to
* @param {Array} data - Initial table data
* @param {Object} config - Configuration options
*/
constructor(element, data, config) {
super(element, data, config);
// Table-specific configuration
this.tableConfig = {
columns: [],
sortable: true,
filterable: true,
pageSize: 20,
selectable: false,
exportable: false,
responsive: true,
maxCellLength: 200,
...config.table
};
// Table state management
this.tableState = {
currentPage: 1,
sortColumn: null,
sortDirection: 'asc', // 'asc' or 'desc'
filterQuery: '',
selectedRows: new Set(),
isLoading: false
};
// Filtered and sorted data cache
this.processedData = [];
this.filteredData = [];
// Column type handlers
this.columnTypes = {
text: this.renderTextCell.bind(this),
number: this.renderNumberCell.bind(this),
date: this.renderDateCell.bind(this),
badge: this.renderBadgeCell.bind(this),
checkbox: this.renderCheckboxCell.bind(this),
actions: this.renderActionsCell.bind(this),
custom: this.renderCustomCell.bind(this)
};
// Re-render now that config is properly set
this.render();
// Bind events after everything is set up
this.bindEvents();
this.log('INFO', 'TableComponent initialized with advanced features');
}
/**
* Override init to prevent premature rendering during construction
*/
init() {
if (this.isDestroyed) return;
try {
// Don't call bindEvents here - let constructor handle it after config is set
this.startPolling();
this.log('INFO', `Component initialized on element: ${this.element.id || this.element.className}`);
} catch (error) {
this.log('ERROR', `Failed to initialize component: ${error.message}`);
this.handleError(error);
}
}
/**
* Render the complete table interface
*/
render() {
if (this.isDestroyed) return;
// Process data for display
this.processData();
this.element.innerHTML = this.html`
<div class="component component-table">
${this.trustedHtml(this.renderHeader())}
${this.trustedHtml(this.renderFilters())}
${this.trustedHtml(this.renderTableContainer())}
${this.trustedHtml(this.renderPagination())}
${this.trustedHtml(this.renderBulkActions())}
${this.trustedHtml(this.renderErrorMessage())}
</div>
`;
}
/**
* Render table header with title and controls
*/
renderHeader() {
const stats = this.calculateStats();
return this.html`
<div class="table-header">
<h2>${this.config.title || 'Data Table'}</h2>
<div class="table-stats">
<span class="stat-item">
<strong>${stats.filtered}</strong> of <strong>${stats.total}</strong> items
</span>
${this.tableState.selectedRows.size > 0 ? this.html`
<span class="stat-item stat-selected">
<strong>${this.tableState.selectedRows.size}</strong> selected
</span>
` : ''}
</div>
<div class="table-controls">
${this.config.actions ? this.trustedHtml(this.config.actions.filter(action => action.type === 'button').map(action => `
<button class="btn-action btn-${action.type}"
data-action="global-action"
data-action-id="${action.id}"
title="${action.label}">
${action.icon || ''} ${action.label}
</button>
`).join('')) : ''}
${this.tableConfig.exportable ? this.html`
<button class="btn-export" data-action="export" title="Export data">
Export
</button>
` : ''}
<button class="btn-refresh" data-action="refresh" title="Refresh data">
↻
</button>
</div>
</div>
`;
}
/**
* Render filter controls
*/
renderFilters() {
if (!this.tableConfig.filterable) return '';
return this.html`
<div class="table-filters">
<div class="filter-search">
<input
type="text"
class="filter-input"
placeholder="Search table..."
value="${this.tableState.filterQuery}"
data-action="filter"
>
${this.tableState.filterQuery ? this.html`
<button class="btn-clear-filter" data-action="clear-filter" title="Clear filter">
×
</button>
` : ''}
</div>
<div class="filter-info">
${this.tableState.filterQuery ? this.html`
<span class="filter-active">
Filtering by: "${this.tableState.filterQuery}"
</span>
` : ''}
</div>
</div>
`;
}
/**
* Render the main table container
*/
renderTableContainer() {
if (this.tableState.isLoading) {
return this.html`
<div class="table-loading">
<div class="loading-spinner"></div>
<p>Loading data...</p>
</div>
`;
}
if (this.filteredData.length === 0) {
return this.renderEmptyState();
}
const pageData = this.getCurrentPageData();
return this.html`
<div class="table-container">
<table class="data-table">
${this.trustedHtml(this.renderTableHeader())}
${this.trustedHtml(this.renderTableBody(pageData))}
</table>
</div>
`;
}
/**
* Render table header with column titles and sort controls
*/
renderTableHeader() {
return this.html`
<thead>
<tr>
${this.tableConfig.selectable ? this.html`
<th class="select-column">
<input
type="checkbox"
class="select-all-checkbox"
data-action="select-all"
${this.isAllSelected() ? 'checked' : ''}
>
</th>
` : ''}
${this.trustedHtml(this.tableConfig.columns.map(column => `
<th class="column-header ${column.sortable !== false && this.tableConfig.sortable ? 'sortable' : ''} ${this.tableState.sortColumn === column.key ? 'sorted' : ''}"
data-column="${column.key}"
data-action="${column.sortable !== false && this.tableConfig.sortable ? 'sort' : ''}">
<div class="column-header-content">
<span class="column-title">${column.label || column.key}</span>
${column.sortable !== false && this.tableConfig.sortable ? `
<span class="sort-indicator ${this.tableState.sortColumn === column.key ? this.tableState.sortDirection : ''}">
↕
</span>
` : ''}
</div>
</th>
`).join(''))}
</tr>
</thead>
`;
}
/**
* Render table body with data rows
* @param {Array} data - Page data to render
*/
renderTableBody(data) {
return this.html`
<tbody>
${this.trustedHtml(data.map((row, index) => this.renderTableRow(row, index)).join(''))}
</tbody>
`;
}
/**
* Render a single table row
* @param {Object} row - Row data
* @param {number} index - Row index
*/
renderTableRow(row, index) {
const rowId = row.id || index;
const isSelected = this.tableState.selectedRows.has(rowId);
return this.html`
<tr class="table-row ${isSelected ? 'selected' : ''}" data-row-id="${rowId}">
${this.tableConfig.selectable ? this.html`
<td class="select-cell">
<input
type="checkbox"
class="row-checkbox"
data-action="select-row"
data-row-id="${rowId}"
${isSelected ? 'checked' : ''}
>
</td>
` : ''}
${this.trustedHtml(this.tableConfig.columns.map(column => `
<td class="table-cell cell-${column.key} cell-type-${column.type || 'text'}"
data-column="${column.key}">
${this.renderCell(row, column)}
</td>
`).join(''))}
</tr>
`;
}
/**
* Render a table cell based on column type
* @param {Object} row - Row data
* @param {Object} column - Column configuration
*/
renderCell(row, column) {
const value = this.getCellValue(row, column.key);
const cellType = column.type || 'text';
if (this.columnTypes[cellType]) {
return this.columnTypes[cellType](value, row, column);
} else {
return this.renderTextCell(value, row, column);
}
}
/**
* Get cell value from row data
* @param {Object} row - Row data
* @param {string} key - Column key (supports dot notation)
*/
getCellValue(row, key) {
if (key.includes('.')) {
// Support nested object keys like 'user.name'
return key.split('.').reduce((obj, k) => obj?.[k], row);
}
return row[key];
}
/**
* Render text cell
*/
renderTextCell(value, row, column) {
if (value === null || value === undefined) {
return '<span class="cell-empty">—</span>';
}
const displayValue = String(value);
const truncated = displayValue.length > this.tableConfig.maxCellLength;
const cellContent = truncated
? displayValue.substring(0, this.tableConfig.maxCellLength) + '...'
: displayValue;
return this.html`
<span class="cell-text ${truncated ? 'truncated' : ''}"
${truncated ? `title="${displayValue}"` : ''}>
${cellContent}
</span>
`;
}
/**
* Render number cell with formatting
*/
renderNumberCell(value, row, column) {
if (value === null || value === undefined || isNaN(value)) {
return '<span class="cell-empty">—</span>';
}
const formatted = column.format
? column.format(value)
: Number(value).toLocaleString();
return this.html`
<span class="cell-number">${formatted}</span>
`;
}
/**
* Render date cell with formatting
*/
renderDateCell(value, row, column) {
if (!value) {
return '<span class="cell-empty">—</span>';
}
try {
const date = new Date(value);
const formatted = column.format
? column.format(date)
: date.toLocaleDateString();
return this.html`
<span class="cell-date" title="${date.toISOString()}">${formatted}</span>
`;
} catch {
return this.html`<span class="cell-error">Invalid Date</span>`;
}
}
/**
* Render badge cell with color coding
*/
renderBadgeCell(value, row, column) {
if (!value) {
return '<span class="cell-empty">—</span>';
}
let badgeText = value;
let badgeColor = null;
// Handle complex badge objects
if (typeof value === 'object' && value !== null) {
badgeText = value.text || value.label || String(value);
badgeColor = value.color;
}
// Use configured color mapping or direct color from object
let badgeStyle = '';
if (badgeColor) {
badgeStyle = `style="background-color: ${badgeColor}; color: white;"`;
} else if (column.badgeConfig?.colorMap?.[badgeText]) {
const configColor = column.badgeConfig.colorMap[badgeText];
badgeStyle = `style="background-color: ${configColor}; color: white;"`;
}
return this.html`
<span class="cell-badge" ${badgeStyle}>${badgeText}</span>
`;
}
/**
* Render checkbox cell
*/
renderCheckboxCell(value, row, column) {
const isChecked = Boolean(value);
const rowId = row.id || row.index;
return this.html`
<input
type="checkbox"
class="cell-checkbox"
data-action="toggle-cell"
data-row-id="${rowId}"
data-column="${column.key}"
${isChecked ? 'checked' : ''}
${column.readonly ? 'disabled' : ''}
>
`;
}
/**
* Render actions cell with buttons
*/
renderActionsCell(value, row, column) {
const actions = column.actions || [];
return this.html`
<div class="cell-actions">
${this.trustedHtml(actions.map(action => `
<button
class="btn-action btn-${action.type || 'default'}"
data-action="row-action"
data-row-id="${row.id || row.index}"
data-action-type="${action.type}"
title="${action.label}"
>
${action.icon || action.label}
</button>
`).join(''))}
</div>
`;
}
/**
* Render custom cell using provided renderer
*/
renderCustomCell(value, row, column) {
if (column.renderer && typeof column.renderer === 'function') {
try {
return column.renderer(value, row, column);
} catch (error) {
this.log('ERROR', `Custom cell renderer error: ${error.message}`);
return '<span class="cell-error">Render Error</span>';
}
}
return this.renderTextCell(value, row, column);
}
/**
* Render empty state
*/
renderEmptyState() {
const message = this.tableState.filterQuery
? 'No results found for your search.'
: 'No data available.';
return this.html`
<div class="table-empty">
<div class="empty-icon">📊</div>
<p class="empty-message">${message}</p>
${this.tableState.filterQuery ? this.html`
<button class="btn-clear-filter" data-action="clear-filter">
Clear Filter
</button>
` : ''}
</div>
`;
}
/**
* Render pagination controls
*/
renderPagination() {
if (!this.shouldShowPagination()) return '';
const totalPages = Math.ceil(this.filteredData.length / this.tableConfig.pageSize);
const currentPage = this.tableState.currentPage;
return this.html`
<div class="table-pagination">
<div class="pagination-info">
Page ${currentPage} of ${totalPages}
(${this.getPageRangeText()})
</div>
<div class="pagination-controls">
<button
class="btn-page btn-first"
data-action="page"
data-page="1"
${currentPage === 1 ? 'disabled' : ''}
title="First page"
>
⇤
</button>
<button
class="btn-page btn-prev"
data-action="page"
data-page="${currentPage - 1}"
${currentPage === 1 ? 'disabled' : ''}
title="Previous page"
>
←
</button>
${this.renderPageNumbers(currentPage, totalPages)}
<button
class="btn-page btn-next"
data-action="page"
data-page="${currentPage + 1}"
${currentPage === totalPages ? 'disabled' : ''}
title="Next page"
>
→
</button>
<button
class="btn-page btn-last"
data-action="page"
data-page="${totalPages}"
${currentPage === totalPages ? 'disabled' : ''}
title="Last page"
>
⇥
</button>
</div>
</div>
`;
}
/**
* Render page number buttons
*/
renderPageNumbers(currentPage, totalPages) {
const pages = [];
const maxButtons = 5;
let start = Math.max(1, currentPage - Math.floor(maxButtons / 2));
let end = Math.min(totalPages, start + maxButtons - 1);
if (end - start + 1 < maxButtons) {
start = Math.max(1, end - maxButtons + 1);
}
for (let i = start; i <= end; i++) {
pages.push(this.html`
<button
class="btn-page btn-number ${i === currentPage ? 'current' : ''}"
data-action="page"
data-page="${i}"
${i === currentPage ? 'disabled' : ''}
>
${i}
</button>
`);
}
return pages.join('');
}
/**
* Render bulk actions for selected rows
*/
renderBulkActions() {
if (!this.tableConfig.selectable || this.tableState.selectedRows.size === 0) {
return '';
}
const selectedCount = this.tableState.selectedRows.size;
return this.html`
<div class="bulk-actions">
<div class="bulk-info">
${selectedCount} item${selectedCount > 1 ? 's' : ''} selected
</div>
<div class="bulk-controls">
<button class="btn-bulk btn-deselect" data-action="deselect-all">
Deselect All
</button>
${this.trustedHtml(this.config.bulkActions?.map(action => `
<button
class="btn-bulk btn-${action.type}"
data-action="bulk-action"
data-action-type="${action.type}"
>
${action.label}
</button>
`).join('') || '')}
</div>
</div>
`;
}
/**
* Render error message area
*/
renderErrorMessage() {
return this.html`
<div class="error-message" style="display: none;"></div>
`;
}
/**
* Bind all event listeners
*/
bindEvents() {
// Sorting
this.on('click', '[data-action="sort"]', (e) => {
const column = e.target.closest('[data-column]').dataset.column;
this.handleSort(column);
});
// Filtering
this.on('input', '[data-action="filter"]', (e) => {
this.handleFilter(e.target.value);
});
this.on('click', '[data-action="clear-filter"]', () => {
this.clearFilter();
});
// Pagination
this.on('click', '[data-action="page"]', (e) => {
const page = parseInt(e.target.dataset.page);
this.goToPage(page);
});
// Row selection
if (this.tableConfig.selectable) {
this.on('change', '[data-action="select-all"]', (e) => {
this.handleSelectAll(e.target.checked);
});
this.on('change', '[data-action="select-row"]', (e) => {
const rowId = e.target.dataset.rowId;
this.handleSelectRow(rowId, e.target.checked);
});
}
// Cell actions
this.on('change', '[data-action="toggle-cell"]', async (e) => {
const rowId = e.target.dataset.rowId;
const column = e.target.dataset.column;
const value = e.target.checked;
await this.handleCellToggle(rowId, column, value);
});
this.on('click', '[data-action="row-action"]', async (e) => {
const rowId = e.target.dataset.rowId;
const actionType = e.target.dataset.actionType;
await this.handleRowAction(rowId, actionType);
});
// Bulk actions
this.on('click', '[data-action="deselect-all"]', () => {
this.deselectAll();
});
this.on('click', '[data-action="bulk-action"]', async (e) => {
const actionType = e.target.dataset.actionType;
await this.handleBulkAction(actionType);
});
// Global actions
this.on('click', '[data-action="global-action"]', async (e) => {
const actionId = e.target.dataset.actionId;
await this.handleGlobalAction(actionId);
});
// Controls
this.on('click', '[data-action="refresh"]', () => {
this.fetchData();
});
if (this.tableConfig.exportable) {
this.on('click', '[data-action="export"]', () => {
this.exportData();
});
}
}
/**
* Process data for sorting and filtering
*/
processData() {
let processed = [...this.data];
// Apply filtering
if (this.tableState.filterQuery) {
processed = this.filterData(processed, this.tableState.filterQuery);
}
// Apply sorting
if (this.tableState.sortColumn) {
processed = this.sortData(processed, this.tableState.sortColumn, this.tableState.sortDirection);
}
this.filteredData = processed;
}
/**
* Filter data based on search query
*/
filterData(data, query) {
const searchTerm = query.toLowerCase().trim();
if (!searchTerm) return data;
return data.filter(row => {
return this.tableConfig.columns.some(column => {
const value = this.getCellValue(row, column.key);
if (value === null || value === undefined) return false;
return String(value).toLowerCase().includes(searchTerm);
});
});
}
/**
* Sort data by column
*/
sortData(data, column, direction) {
return [...data].sort((a, b) => {
const aVal = this.getCellValue(a, column);
const bVal = this.getCellValue(b, column);
// Handle null/undefined values
if (aVal === null || aVal === undefined) return direction === 'asc' ? 1 : -1;
if (bVal === null || bVal === undefined) return direction === 'asc' ? -1 : 1;
// Type-specific sorting
if (typeof aVal === 'number' && typeof bVal === 'number') {
return direction === 'asc' ? aVal - bVal : bVal - aVal;
}
if (aVal instanceof Date && bVal instanceof Date) {
return direction === 'asc' ? aVal - bVal : bVal - aVal;
}
// String comparison
const aStr = String(aVal).toLowerCase();
const bStr = String(bVal).toLowerCase();
if (direction === 'asc') {
return aStr.localeCompare(bStr);
} else {
return bStr.localeCompare(aStr);
}
});
}
/**
* Get current page data
*/
getCurrentPageData() {
const start = (this.tableState.currentPage - 1) * this.tableConfig.pageSize;
const end = start + this.tableConfig.pageSize;
return this.filteredData.slice(start, end);
}
/**
* Handle column sorting
*/
handleSort(column) {
if (this.tableState.sortColumn === column) {
// Toggle direction
this.tableState.sortDirection = this.tableState.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
// New column
this.tableState.sortColumn = column;
this.tableState.sortDirection = 'asc';
}
this.tableState.currentPage = 1; // Reset to first page
this.render();
}
/**
* Handle filtering
*/
handleFilter(query) {
this.tableState.filterQuery = query;
this.tableState.currentPage = 1; // Reset to first page
this.render();
}
/**
* Clear filter
*/
clearFilter() {
this.tableState.filterQuery = '';
this.tableState.currentPage = 1;
this.render();
}
/**
* Go to specific page
*/
goToPage(page) {
const totalPages = Math.ceil(this.filteredData.length / this.tableConfig.pageSize);
if (page >= 1 && page <= totalPages) {
this.tableState.currentPage = page;
this.render();
}
}
/**
* Handle select all checkbox
*/
handleSelectAll(checked) {
const pageData = this.getCurrentPageData();
if (checked) {
pageData.forEach(row => {
const rowId = row.id || row.index;
this.tableState.selectedRows.add(rowId);
});
} else {
pageData.forEach(row => {
const rowId = row.id || row.index;
this.tableState.selectedRows.delete(rowId);
});
}
this.render();
}
/**
* Handle individual row selection
*/
handleSelectRow(rowId, checked) {
if (checked) {
this.tableState.selectedRows.add(rowId);
} else {
this.tableState.selectedRows.delete(rowId);
}
this.render();
}
/**
* Deselect all rows
*/
deselectAll() {
this.tableState.selectedRows.clear();
this.render();
}
/**
* Handle cell toggle action
*/
async handleCellToggle(rowId, column, value) {
try {
await this.handleAction('toggle-cell', { rowId, column, value });
} catch (error) {
this.handleError(error);
}
}
/**
* Handle row action
*/
async handleRowAction(rowId, actionType) {
try {
await this.handleAction('row-action', { rowId, actionType });
} catch (error) {
this.handleError(error);
}
}
/**
* Handle bulk action
*/
async handleBulkAction(actionType) {
const selectedIds = Array.from(this.tableState.selectedRows);
try {
await this.handleAction('bulk-action', { actionType, rowIds: selectedIds });
this.tableState.selectedRows.clear();
this.render();
} catch (error) {
this.handleError(error);
}
}
/**
* Export table data
*/
exportData() {
try {
const dataToExport = this.tableState.selectedRows.size > 0
? this.data.filter(row => this.tableState.selectedRows.has(row.id))
: this.filteredData;
const csv = this.convertToCSV(dataToExport);
this.downloadCSV(csv, 'table-data.csv');
this.log('INFO', 'Data exported successfully');
} catch (error) {
this.handleError(error);
}
}
/**
* Convert data to CSV format
*/
convertToCSV(data) {
if (data.length === 0) return '';
const headers = this.tableConfig.columns.map(col => col.label || col.key);
const csvRows = [headers.join(',')];
data.forEach(row => {
const values = this.tableConfig.columns.map(col => {
const value = this.getCellValue(row, col.key);
if (value === null || value === undefined) return '';
// Escape CSV special characters
const stringValue = String(value);
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
});
csvRows.push(values.join(','));
});
return csvRows.join('\n');
}
/**
* Download CSV file
*/
downloadCSV(csv, filename) {
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
/**
* Handle global action button clicks
*/
async handleGlobalAction(actionId) {
try {
const result = await this.handleAction(actionId, {});
if (result.showForm && result.form) {
// Use new ModalComponent for form display
await this.showFormModal(actionId, result.form);
} else if (result.success) {
this.log('INFO', result.message || 'Action completed successfully');
}
} catch (error) {
this.log('ERROR', `Global action failed: ${error.message}`);
}
}
/**
* Show form modal using new ModalComponent
*/
async showFormModal(actionId, formSchema) {
if (!window.MCPModal) {
this.log('ERROR', 'ModalComponent not available');
return;
}
try {
const formFields = formSchema.fields.map(field => ({
key: field.key,
label: field.label,
type: field.type,
required: field.required,
options: field.options
}));
const result = await window.MCPModal.form({
title: formSchema.title || 'Form',
fields: formFields,
onSubmit: async (formData) => {
try {
const actionResult = await this.handleAction(actionId, formData);
if (actionResult.success) {
this.log('INFO', actionResult.message || 'Form submitted successfully');
return { success: true };
} else {
throw new Error(actionResult.error || 'Form submission failed');
}
} catch (error) {
this.log('ERROR', `Form submission failed: ${error.message}`);
throw error;
}
}
});
if (result.action === 'submit') {
this.log('INFO', 'Form submitted successfully');
}
} catch (error) {
this.log('ERROR', `Failed to show form modal: ${error.message}`);
}
}
/**
* Calculate table statistics
*/
calculateStats() {
return {
total: this.data.length,
filtered: this.filteredData.length,
selected: this.tableState.selectedRows.size
};
}
/**
* Check if pagination should be shown
*/
shouldShowPagination() {
return this.filteredData.length > this.tableConfig.pageSize;
}
/**
* Check if all visible rows are selected
*/
isAllSelected() {
const pageData = this.getCurrentPageData();
if (pageData.length === 0) return false;
return pageData.every(row => {
const rowId = row.id || row.index;
return this.tableState.selectedRows.has(rowId);
});
}
/**
* Get page range text for pagination
*/
getPageRangeText() {
const start = (this.tableState.currentPage - 1) * this.tableConfig.pageSize + 1;
const end = Math.min(start + this.tableConfig.pageSize - 1, this.filteredData.length);
return `${start}-${end} of ${this.filteredData.length}`;
}
/**
* Enhanced cleanup for table-specific resources
*/
destroy() {
// Clear table state
this.tableState = null;
this.tableConfig = null;
this.processedData = null;
this.filteredData = null;
this.columnTypes = null;
// Call parent cleanup
super.destroy();
}
}
// Export for module systems
if (typeof module !== 'undefined' && module.exports) {
module.exports = TableComponent;
}
// Make available globally for vanilla JS usage
if (typeof window !== 'undefined') {
window.TableComponent = TableComponent;
}