reuse form
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
export class AjaxTable {
|
||||
constructor(options = {}) {
|
||||
this.formId = options.formId || 'filterForm';
|
||||
this.tableContainerSelector = options.tableContainerSelector || '#userTableContainer';
|
||||
this.spinnerId = options.spinnerId || 'loadingSpinner';
|
||||
this.searchInputId = options.searchInputId || 'searchInput';
|
||||
this.btnClearSearchId = options.btnClearSearchId || 'btnClearSearch';
|
||||
this.btnClearFiltersId = options.btnClearFiltersId || 'btnClearFilters';
|
||||
this.flagSendFilterId = options.flagSendFilterId || 'flagSendFilter';
|
||||
this.monthFilterId = options.monthFilterId || 'monthFilter';
|
||||
this.skeletonColumns = options.skeletonColumns || 7;
|
||||
|
||||
this.isLoading = false;
|
||||
this.searchDebounceTimer = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.form = document.getElementById(this.formId);
|
||||
this.spinner = document.getElementById(this.spinnerId);
|
||||
this.searchInput = document.getElementById(this.searchInputId);
|
||||
this.btnClearSearch = document.getElementById(this.btnClearSearchId);
|
||||
this.btnClearFilters = document.getElementById(this.btnClearFiltersId);
|
||||
this.flagSendFilter = document.getElementById(this.flagSendFilterId);
|
||||
this.monthFilter = document.getElementById(this.monthFilterId);
|
||||
|
||||
if (this.form) {
|
||||
this.form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.submitForm();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.searchInput) {
|
||||
this.searchInput.addEventListener('input', () => {
|
||||
this.toggleClearSearchBtn();
|
||||
clearTimeout(this.searchDebounceTimer);
|
||||
this.searchDebounceTimer = setTimeout(() => {
|
||||
this.submitForm();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.btnClearSearch) {
|
||||
this.btnClearSearch.addEventListener('click', () => {
|
||||
if (this.searchInput) {
|
||||
this.searchInput.value = '';
|
||||
this.toggleClearSearchBtn();
|
||||
this.submitForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.flagSendFilter) {
|
||||
this.flagSendFilter.addEventListener('change', () => {
|
||||
this.submitForm();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.btnClearFilters) {
|
||||
this.btnClearFilters.addEventListener('click', () => {
|
||||
if (this.searchInput) this.searchInput.value = '';
|
||||
if (this.flagSendFilter) this.flagSendFilter.value = '';
|
||||
if (this.monthFilter) {
|
||||
this.monthFilter.value = new Date().toISOString().slice(0, 7);
|
||||
}
|
||||
this.toggleClearSearchBtn();
|
||||
this.submitForm();
|
||||
});
|
||||
}
|
||||
|
||||
// Intercept pagination links
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.isLoading) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
const link = e.target.closest(`${this.tableContainerSelector} nav a`);
|
||||
if (link) {
|
||||
e.preventDefault();
|
||||
this.loadUrl(link.href);
|
||||
}
|
||||
});
|
||||
|
||||
// Browser history handling
|
||||
window.addEventListener('popstate', () => {
|
||||
this.loadUrl(window.location.href, false);
|
||||
});
|
||||
|
||||
this.toggleClearSearchBtn();
|
||||
}
|
||||
|
||||
toggleClearSearchBtn() {
|
||||
if (this.searchInput && this.btnClearSearch) {
|
||||
if (this.searchInput.value.trim() !== '') {
|
||||
this.btnClearSearch.classList.remove('hidden');
|
||||
} else {
|
||||
this.btnClearSearch.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submitForm() {
|
||||
if (!this.form) return;
|
||||
const url = this.form.action + '?' + new URLSearchParams(new FormData(this.form)).toString();
|
||||
this.loadUrl(url);
|
||||
}
|
||||
|
||||
getSkeletonHtml() {
|
||||
let html = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
html += `
|
||||
<tr class="animate-pulse h-[69px]">
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-48"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-12"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-4 bg-gray-200 rounded w-16"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-6 bg-gray-200 rounded-full w-20"></div></td>
|
||||
<td class="px-6 py-4"><div class="h-8 bg-gray-200 rounded w-20 ml-auto"></div></td>
|
||||
</tr>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
async loadUrl(url, updateHistory = true) {
|
||||
if (this.isLoading) return;
|
||||
this.isLoading = true;
|
||||
|
||||
if (this.spinner) {
|
||||
this.spinner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
let formElements = [];
|
||||
if (this.form) {
|
||||
formElements = this.form.querySelectorAll('input, select, button');
|
||||
formElements.forEach(el => el.disabled = true);
|
||||
}
|
||||
|
||||
const tableContainer = document.querySelector(this.tableContainerSelector);
|
||||
const tableBody = tableContainer ? tableContainer.querySelector('tbody') : null;
|
||||
if (tableBody) {
|
||||
tableBody.innerHTML = this.getSkeletonHtml();
|
||||
}
|
||||
|
||||
try {
|
||||
const [res] = await Promise.all([
|
||||
fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }),
|
||||
new Promise(resolve => setTimeout(resolve, 200)) // smooth transition
|
||||
]);
|
||||
|
||||
if (!res.ok) throw new Error('Network response was not ok');
|
||||
|
||||
const contentType = res.headers.get('content-type');
|
||||
const updateDOM = async () => {
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await res.json();
|
||||
|
||||
const titleEl = document.getElementById('pageTitle');
|
||||
if (titleEl && data.title !== undefined) {
|
||||
titleEl.innerHTML = data.title;
|
||||
}
|
||||
|
||||
const statsEl = document.getElementById('statsGrid');
|
||||
if (statsEl && data.stats !== undefined) {
|
||||
statsEl.innerHTML = data.stats;
|
||||
}
|
||||
|
||||
if (tableContainer && data.table !== undefined) {
|
||||
tableContainer.innerHTML = data.table;
|
||||
}
|
||||
} else {
|
||||
const html = await res.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
const currentTitle = document.getElementById('pageTitle');
|
||||
const newTitle = doc.getElementById('pageTitle');
|
||||
if (currentTitle && newTitle) currentTitle.innerHTML = newTitle.innerHTML;
|
||||
|
||||
const currentGrid = document.getElementById('statsGrid');
|
||||
const newGrid = doc.getElementById('statsGrid');
|
||||
if (currentGrid && newGrid) currentGrid.innerHTML = newGrid.innerHTML;
|
||||
|
||||
const newTable = doc.querySelector(this.tableContainerSelector);
|
||||
if (tableContainer && newTable) tableContainer.innerHTML = newTable.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(updateDOM);
|
||||
} else {
|
||||
await updateDOM();
|
||||
}
|
||||
|
||||
if (updateHistory) {
|
||||
window.history.pushState({}, '', url);
|
||||
}
|
||||
|
||||
const newTableContainer = document.querySelector(this.tableContainerSelector);
|
||||
if (newTableContainer) {
|
||||
newTableContainer.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error fetching data:', err);
|
||||
window.location.reload();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
if (this.spinner) {
|
||||
this.spinner.classList.add('hidden');
|
||||
}
|
||||
if (this.form) {
|
||||
formElements.forEach(el => el.disabled = false);
|
||||
}
|
||||
this.toggleClearSearchBtn();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,27 @@
|
||||
import './bootstrap';
|
||||
import { AjaxTable } from './AjaxTable';
|
||||
|
||||
window.AjaxTable = AjaxTable;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('filterForm') && document.querySelector('.bg-white.shadow-sm')) {
|
||||
new AjaxTable();
|
||||
}
|
||||
});
|
||||
|
||||
window.openDeleteModal = function(msnv, url) {
|
||||
const messageEl = document.getElementById('deleteModal-message');
|
||||
|
||||
if (messageEl) {
|
||||
messageEl.innerHTML = `Bạn có chắc chắn muốn đánh dấu nhân viên <span class="font-extrabold text-red-600">${msnv}</span> đã nghỉ việc?<br><br>Thao tác này sẽ ngăn user đăng nhập vào hệ thống, nhưng dữ liệu lịch sử vẫn được giữ lại.`;
|
||||
}
|
||||
|
||||
const formEl = document.getElementById('deleteModal-form');
|
||||
|
||||
if (formEl) {
|
||||
formEl.action = url;
|
||||
}
|
||||
if (typeof window.openModal === 'function') {
|
||||
window.openModal('deleteModal');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user