class DataTableViewer {
constructor(config) {
this.config = {
dataUrl: config.dataUrl,
editEndpoint: config.editEndpoint || '',
extraEditData: config.extraEditData || {},
onEditSuccess: config.onEditSuccess || ((response) => console.log('Edit success:', response)),
columns: config.columns || [],
rowsPerPage: config.rowsPerPage || 10,
container: config.container || document.getElementById('tableContainer')
};
this.data = [];
this.filteredData = [];
this.currentPage = 1;
this.sortColumn = null;
this.sortDirection = 'asc';
this.columnSearchValues = {};
this.globalSearchValue = '';
this.hiddenColumns = new Set();
this.init();
}
async init() {
await this.loadData();
this.setupControls();
this.render();
}
async loadData() {
this.config.container.innerHTML = '
Loading data...
';
try {
if (this.config.dataUrl) {
const response = await fetch(this.config.dataUrl);
this.data = await response.json();
} else {
// Use sample data for preview
this.data = sampleData;
}
this.filteredData = [...this.data];
} catch (error) {
console.error('Error loading data:', error);
this.config.container.innerHTML = 'Error loading data
';
}
}
setupControls() {
// Global search
document.getElementById('globalSearch').addEventListener('input', (e) => {
this.globalSearchValue = e.target.value.toLowerCase();
this.filterData();
this.currentPage = 1;
this.render();
});
// Rows per page
document.getElementById('rowsPerPage').addEventListener('change', (e) => {
this.config.rowsPerPage = parseInt(e.target.value);
this.currentPage = 1;
this.render();
});
// Column toggle menu
const toggleBtn = document.getElementById('toggleColumnsBtn');
const menu = document.getElementById('columnToggleMenu');
toggleBtn.addEventListener('click', () => {
menu.classList.toggle('hidden');
menu.style.top = `${toggleBtn.offsetTop + toggleBtn.offsetHeight}px`;
menu.style.left = `${toggleBtn.offsetLeft}px`;
menu.innerHTML = this.config.columns.map(col => `
`).join('');
});
menu.addEventListener('change', (e) => {
if (e.target.matches('input[type="checkbox"]')) {
const column = e.target.dataset.column;
if (e.target.checked) {
this.hiddenColumns.delete(column);
} else {
this.hiddenColumns.add(column);
}
this.render();
}
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!menu.contains(e.target) && e.target !== toggleBtn) {
menu.classList.add('hidden');
}
});
}
filterData() {
this.filteredData = this.data.filter(row => {
// Global search
if (this.globalSearchValue) {
const rowString = Object.values(row).join(' ').toLowerCase();
if (!rowString.includes(this.globalSearchValue)) return false;
}
// Column-specific search
return Object.entries(this.columnSearchValues).every(([field, searchValue]) => {
if (!searchValue) return true;
const value = String(row[field]).toLowerCase();
// For select type columns, do exact match
const col = this.config.columns.find(c => c.field === field);
if (col?.searchType === 'select') {
return value === searchValue.toLowerCase();
}
// For text type columns, do contains match
return value.includes(searchValue.toLowerCase());
});
});
}
sortData(column) {
if (this.sortColumn === column) {
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.sortColumn = column;
this.sortDirection = 'asc';
}
const col = this.config.columns.find(c => c.field === column);
this.filteredData.sort((a, b) => {
let valA = a[column];
let valB = b[column];
if (col.type === 'date') {
valA = new Date(valA);
valB = new Date(valB);
}
if (valA < valB) return this.sortDirection === 'desc' ? 1 : -1;
if (valA > valB) return this.sortDirection === 'desc' ? -1 : 1;
return 0;
});
this.render();
}
async handleEdit(rowIndex, row) {
try {
const response = await fetch(this.config.editEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...row,
...this.config.extraEditData
})
});
if (!response.ok) throw new Error('Edit failed');
const result = await response.json();
this.config.onEditSuccess(result);
// Update local data
Object.assign(this.data[rowIndex], row);
this.filterData();
this.render();
} catch (error) {
console.error('Error saving edit:', error);
alert('Failed to save changes');
}
}
renderHeader() {
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
return `
Edit |
${visibleColumns.map(col => `
${col.title}
|
`).join('')}
`;
}
renderBody(ret = 'html') {
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
const endIndex = startIndex + this.config.rowsPerPage;
const pageData = this.filteredData.slice(startIndex, endIndex);
const tbody = `
${pageData.map((row, idx) => `
|
${visibleColumns.map(col => `
${row[col.field]} |
`).join('')}
`).join('')}
`;
if(ret === 'html'){
return tbody;
} else if(ret === 'update'){
this.config.container.querySelector('.container-tbody').innerHTML = tbody;
this.renderEdit();
}
}
renderFooter() {
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
return `
`;
}
renderPagination() {
return `
`;
}
render() {
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
const endIndex = startIndex + this.config.rowsPerPage;
const pageData = this.filteredData.slice(startIndex, endIndex);
this.config.container.innerHTML = `
${this.renderHeader()}
${this.renderBody()}
${this.renderFooter()}
${this.renderPagination()}
`;
this.renderColumnSearch();
this.renderColumnSort();
this.renderEdit();
}
renderColumnSearch() {
// Setup column search
this.config.container.querySelectorAll('.column-search').forEach(input => {
input.addEventListener('keyup', (e) => {
const column = e.target.dataset.column;
const value = e.target.value;
if (value === '') {
delete this.columnSearchValues[column];
} else {
this.columnSearchValues[column] = value;
}
this.filterData();
this.currentPage = 1;
this.render();
//this.renderColumnSearch();
// Focus the input and move the cursor to the end
const targetInput = document.querySelector(`tfoot input[data-column="${column}"]`);
if (targetInput) {
targetInput.focus();
const length = targetInput.value.length;
targetInput.setSelectionRange(length, length);
}
});
});
}
renderColumnSort() {
// Setup sorting
this.config.container.querySelectorAll('.header-cell').forEach(header => {
header.addEventListener('click', () => {
const column = header.dataset.column;
this.sortData(column);
});
});
}
renderEdit() {
// Setup edit buttons
const visibleColumns = this.config.columns.filter(col => !this.hiddenColumns.has(col.field));
const startIndex = (this.currentPage - 1) * this.config.rowsPerPage;
const endIndex = startIndex + this.config.rowsPerPage;
const pageData = this.filteredData.slice(startIndex, endIndex);
this.config.container.querySelectorAll('.edit-btn').forEach(btn => {
btn.addEventListener('click', () => {
const rowIndex = parseInt(btn.dataset.row);
const row = pageData[rowIndex];
const tr = document.getElementById(`row-${rowIndex}`);
const originalContent = tr.innerHTML;
tr.innerHTML = `
|
${visibleColumns.map(col => `
|
`).join('')}
`;
tr.querySelector('.save-btn').addEventListener('click', () => {
const updatedRow = { ...row };
tr.querySelectorAll('input[data-field]').forEach(input => {
updatedRow[input.dataset.field] = input.value;
});
this.handleEdit(rowIndex, updatedRow);
});
tr.querySelector('.cancel-btn').addEventListener('click', () => {
//this.renderBody();
tr.innerHTML = originalContent;
});
});
});
}
}