187 lines
7.7 KiB
JavaScript
187 lines
7.7 KiB
JavaScript
|
|
window.TableEnhancer = {
|
||
|
|
init(selector, options = {}) {
|
||
|
|
const defaults = {
|
||
|
|
perPage: 10,
|
||
|
|
searchable: true,
|
||
|
|
sortable: true,
|
||
|
|
persistKey: null
|
||
|
|
};
|
||
|
|
const config = { ...defaults, ...options };
|
||
|
|
document.querySelectorAll(selector).forEach(table => this.enhance(table, config));
|
||
|
|
},
|
||
|
|
|
||
|
|
enhance(table, config) {
|
||
|
|
const container = table.closest('.table-container') || table.parentElement;
|
||
|
|
if (!container.querySelector('.table-search')) {
|
||
|
|
this.addControls(container, table, config);
|
||
|
|
}
|
||
|
|
this.setupSearch(container, table, config);
|
||
|
|
this.setupSort(container, table, config);
|
||
|
|
this.setupPagination(container, table, config);
|
||
|
|
},
|
||
|
|
|
||
|
|
addControls(container, table, config) {
|
||
|
|
const controls = document.createElement('div');
|
||
|
|
controls.className = 'table-controls flex justify-between items-center mb-4 p-4 bg-gray-50 rounded-t-lg';
|
||
|
|
controls.innerHTML = `
|
||
|
|
${config.searchable ? `
|
||
|
|
<div class="relative">
|
||
|
|
<input type="text" class="table-search pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 w-64" placeholder="Search...">
|
||
|
|
<svg class="absolute left-3 top-2.5 h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||
|
|
</svg>
|
||
|
|
</div>
|
||
|
|
` : ''}
|
||
|
|
<div class="table-info text-sm text-gray-500"></div>
|
||
|
|
`;
|
||
|
|
container.insertBefore(controls, table);
|
||
|
|
|
||
|
|
const pagination = document.createElement('div');
|
||
|
|
pagination.className = 'table-pagination flex justify-center items-center space-x-2 mt-4';
|
||
|
|
container.appendChild(pagination);
|
||
|
|
},
|
||
|
|
|
||
|
|
setupSearch(container, table, config) {
|
||
|
|
const searchInput = container.querySelector('.table-search');
|
||
|
|
if (!searchInput) return;
|
||
|
|
|
||
|
|
const key = config.persistKey ? `table_search_${config.persistKey}` : null;
|
||
|
|
if (key && localStorage.getItem(key)) {
|
||
|
|
searchInput.value = localStorage.getItem(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
searchInput.addEventListener('input', (e) => {
|
||
|
|
if (key) localStorage.setItem(key, e.target.value);
|
||
|
|
this.filterTable(table, e.target.value);
|
||
|
|
this.updatePagination(container, table, 1, config);
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
setupSort(container, table, config) {
|
||
|
|
if (!config.sortable) return;
|
||
|
|
|
||
|
|
const headers = table.querySelectorAll('th');
|
||
|
|
headers.forEach((th, index) => {
|
||
|
|
if (th.textContent.trim()) {
|
||
|
|
th.classList.add('cursor-pointer', 'hover:bg-gray-100');
|
||
|
|
th.dataset.sortCol = index;
|
||
|
|
th.innerHTML += `
|
||
|
|
<svg class="inline h-4 w-4 ml-1 text-gray-400 sort-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
|
||
|
|
</svg>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
container.querySelectorAll('th[data-sort-col]').forEach(th => {
|
||
|
|
th.addEventListener('click', () => {
|
||
|
|
const col = th.dataset.sortCol;
|
||
|
|
const currentSort = th.dataset.sort || 'none';
|
||
|
|
const newSort = currentSort === 'asc' ? 'desc' : 'asc';
|
||
|
|
this.sortTable(table, col, newSort);
|
||
|
|
th.dataset.sort = newSort;
|
||
|
|
th.querySelector('.sort-icon').innerHTML = newSort === 'asc'
|
||
|
|
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>'
|
||
|
|
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
},
|
||
|
|
|
||
|
|
setupPagination(container, table, config) {
|
||
|
|
this.updatePagination(container, table, 1, config);
|
||
|
|
},
|
||
|
|
|
||
|
|
filterTable(table, query) {
|
||
|
|
const rows = table.querySelectorAll('tbody tr');
|
||
|
|
const lowerQuery = query.toLowerCase();
|
||
|
|
let visibleCount = 0;
|
||
|
|
|
||
|
|
rows.forEach(row => {
|
||
|
|
const text = row.textContent.toLowerCase();
|
||
|
|
const match = text.includes(lowerQuery);
|
||
|
|
row.style.display = match ? '' : 'none';
|
||
|
|
if (match) visibleCount++;
|
||
|
|
});
|
||
|
|
|
||
|
|
const infoEl = container.querySelector('.table-info');
|
||
|
|
if (infoEl) {
|
||
|
|
infoEl.textContent = `${visibleCount} row${visibleCount !== 1 ? 's' : ''} found`;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
sortTable(table, col, direction) {
|
||
|
|
const tbody = table.querySelector('tbody');
|
||
|
|
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||
|
|
|
||
|
|
rows.sort((a, b) => {
|
||
|
|
const aVal = a.querySelectorAll('td')[col]?.textContent.trim() || '';
|
||
|
|
const bVal = b.querySelectorAll('td')[col]?.textContent.trim() || '';
|
||
|
|
const aNum = parseFloat(aVal);
|
||
|
|
const bNum = parseFloat(bVal);
|
||
|
|
|
||
|
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||
|
|
return direction === 'asc' ? aNum - bNum : bNum - aNum;
|
||
|
|
}
|
||
|
|
return direction === 'asc'
|
||
|
|
? aVal.localeCompare(bVal)
|
||
|
|
: bVal.localeCompare(aVal);
|
||
|
|
});
|
||
|
|
|
||
|
|
rows.forEach(row => tbody.appendChild(row));
|
||
|
|
},
|
||
|
|
|
||
|
|
updatePagination(container, table, page, config) {
|
||
|
|
const rows = Array.from(table.querySelectorAll('tbody tr:not([style*=\"display: none\"])'));
|
||
|
|
const totalPages = Math.ceil(rows.length / config.perPage);
|
||
|
|
const pagination = container.querySelector('.table-pagination');
|
||
|
|
if (!pagination) return;
|
||
|
|
|
||
|
|
const start = (page - 1) * config.perPage;
|
||
|
|
const end = start + config.perPage;
|
||
|
|
|
||
|
|
rows.forEach((row, index) => {
|
||
|
|
row.style.display = index >= start && index < end ? '' : 'none';
|
||
|
|
});
|
||
|
|
|
||
|
|
if (totalPages <= 1) {
|
||
|
|
pagination.innerHTML = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let html = `
|
||
|
|
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === 1 ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page - 1})">Prev</button>
|
||
|
|
`;
|
||
|
|
|
||
|
|
for (let i = 1; i <= totalPages; i++) {
|
||
|
|
if (i === 1 || i === totalPages || (i >= page - 1 && i <= page + 1)) {
|
||
|
|
html += `<button class="px-3 py-1 border rounded ${i === page ? 'bg-primary-600 text-white' : 'hover:bg-gray-100'}" onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${i})">${i}</button>`;
|
||
|
|
} else if (i === page - 2 || i === page + 2) {
|
||
|
|
html += `<span class="px-2">...</span>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
html += `
|
||
|
|
<button class="px-3 py-1 border rounded hover:bg-gray-100" ${page === totalPages ? 'disabled' : ''} onclick="TableEnhancer.goToPage('${container.querySelector('table').id || ''}', ${page + 1})">Next</button>
|
||
|
|
`;
|
||
|
|
|
||
|
|
pagination.innerHTML = html;
|
||
|
|
},
|
||
|
|
|
||
|
|
goToPage(tableId, page) {
|
||
|
|
const table = tableId ? document.getElementById(tableId) : document.querySelector('table');
|
||
|
|
if (!table) return;
|
||
|
|
const container = table.closest('.table-container') || table.parentElement;
|
||
|
|
const config = { perPage: 10 };
|
||
|
|
this.updatePagination(container, table, page, config);
|
||
|
|
},
|
||
|
|
|
||
|
|
goToPageBySelector(selector, page) {
|
||
|
|
const container = document.querySelector(selector);
|
||
|
|
if (!container) return;
|
||
|
|
const table = container.querySelector('table');
|
||
|
|
if (!table) return;
|
||
|
|
const config = { perPage: 10 };
|
||
|
|
this.updatePagination(container, table, page, config);
|
||
|
|
}
|
||
|
|
};
|