338 lines
8.7 KiB
JavaScript
338 lines
8.7 KiB
JavaScript
/**
|
|
* Table utilities for handling data tables with pagination, sorting, and filtering
|
|
*/
|
|
|
|
class TableHandler {
|
|
constructor(tableId, options = {}) {
|
|
this.table = document.getElementById(tableId);
|
|
this.options = {
|
|
enableSorting: true,
|
|
enableFiltering: true,
|
|
enablePagination: true,
|
|
loadingText: "Loading...",
|
|
noDataText: "No data available",
|
|
...options,
|
|
};
|
|
|
|
this.currentPage = 1;
|
|
this.itemsPerPage = options.itemsPerPage || 20;
|
|
this.sortColumn = null;
|
|
this.sortDirection = "asc";
|
|
this.filters = {};
|
|
|
|
this.initializeTable();
|
|
}
|
|
|
|
/**
|
|
* Initialize table features
|
|
*/
|
|
initializeTable() {
|
|
if (!this.table) return;
|
|
|
|
if (this.options.enableSorting) {
|
|
this.setupSortingHandlers();
|
|
}
|
|
|
|
if (this.options.enableFiltering) {
|
|
this.setupFilteringHandlers();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up sorting handlers for table headers
|
|
*/
|
|
setupSortingHandlers() {
|
|
const headers = this.table.querySelectorAll("th[data-sortable]");
|
|
|
|
headers.forEach((header) => {
|
|
header.style.cursor = "pointer";
|
|
header.addEventListener("click", () => {
|
|
const column = header.dataset.sortable;
|
|
this.sortByColumn(column);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sort table by column
|
|
* @param {string} column - Column to sort by
|
|
*/
|
|
sortByColumn(column) {
|
|
if (this.sortColumn === column) {
|
|
this.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
|
|
} else {
|
|
this.sortColumn = column;
|
|
this.sortDirection = "asc";
|
|
}
|
|
|
|
this.updateSortIndicators();
|
|
this.refreshData();
|
|
}
|
|
|
|
/**
|
|
* Update sort direction indicators in table headers
|
|
*/
|
|
updateSortIndicators() {
|
|
// Remove existing sort indicators
|
|
this.table.querySelectorAll("th .sort-indicator").forEach((indicator) => {
|
|
indicator.remove();
|
|
});
|
|
|
|
// Add indicator to current sort column
|
|
if (this.sortColumn) {
|
|
const header = this.table.querySelector(
|
|
`th[data-sortable="${this.sortColumn}"]`
|
|
);
|
|
if (header) {
|
|
const indicator = document.createElement("span");
|
|
indicator.className = "sort-indicator";
|
|
indicator.innerHTML = this.sortDirection === "asc" ? " ↑" : " ↓";
|
|
header.appendChild(indicator);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set up filtering handlers
|
|
*/
|
|
setupFilteringHandlers() {
|
|
const filterInputs = document.querySelectorAll("[data-table-filter]");
|
|
|
|
filterInputs.forEach((input) => {
|
|
input.addEventListener("input", (e) => {
|
|
const filterKey = e.target.dataset.tableFilter;
|
|
this.setFilter(filterKey, e.target.value);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Set a filter value
|
|
* @param {string} key - Filter key
|
|
* @param {string} value - Filter value
|
|
*/
|
|
setFilter(key, value) {
|
|
if (value && value.trim() !== "") {
|
|
this.filters[key] = value.trim();
|
|
} else {
|
|
delete this.filters[key];
|
|
}
|
|
|
|
this.currentPage = 1; // Reset to first page when filtering
|
|
this.refreshData();
|
|
}
|
|
|
|
/**
|
|
* Show loading state
|
|
*/
|
|
showLoading() {
|
|
const tbody = this.table.querySelector("tbody");
|
|
if (tbody) {
|
|
const colCount = this.table.querySelectorAll("th").length;
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="${colCount}" class="text-center">${this.options.loadingText}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show no data message
|
|
*/
|
|
showNoData() {
|
|
const tbody = this.table.querySelector("tbody");
|
|
if (tbody) {
|
|
const colCount = this.table.querySelectorAll("th").length;
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="${colCount}" class="text-center">${this.options.noDataText}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render table data
|
|
* @param {Array} data - Array of data objects
|
|
* @param {Function} rowRenderer - Function to render each row
|
|
*/
|
|
renderData(data, rowRenderer) {
|
|
const tbody = this.table.querySelector("tbody");
|
|
if (!tbody) return;
|
|
|
|
if (!data || data.length === 0) {
|
|
this.showNoData();
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = data.map(rowRenderer).join("");
|
|
}
|
|
|
|
/**
|
|
* Build query parameters for API requests
|
|
* @returns {object} Query parameters object
|
|
*/
|
|
buildQueryParams() {
|
|
const params = {
|
|
page: this.currentPage,
|
|
per_page: this.itemsPerPage,
|
|
...this.filters,
|
|
};
|
|
|
|
if (this.sortColumn) {
|
|
params.sort_by = this.sortColumn;
|
|
params.sort_dir = this.sortDirection;
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Refresh table data (to be implemented by subclasses or passed as callback)
|
|
*/
|
|
refreshData() {
|
|
if (this.options.onRefresh) {
|
|
this.options.onRefresh(this.buildQueryParams());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update pagination controls
|
|
* @param {object} paginationInfo - Pagination information
|
|
*/
|
|
updatePagination(paginationInfo) {
|
|
const paginationContainer = document.querySelector(".pagination-container");
|
|
if (!paginationContainer || !paginationInfo) return;
|
|
|
|
// This is a basic implementation - you might want to enhance this
|
|
const { current_page, total_pages, has_prev, has_next } = paginationInfo;
|
|
|
|
let paginationHTML = '<nav><ul class="pagination justify-content-center">';
|
|
|
|
// Previous button
|
|
if (has_prev) {
|
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" data-page="${
|
|
current_page - 1
|
|
}">Previous</a></li>`;
|
|
} else {
|
|
paginationHTML +=
|
|
'<li class="page-item disabled"><span class="page-link">Previous</span></li>';
|
|
}
|
|
|
|
// Page numbers (simplified - show current and adjacent pages)
|
|
const startPage = Math.max(1, current_page - 2);
|
|
const endPage = Math.min(total_pages, current_page + 2);
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
if (i === current_page) {
|
|
paginationHTML += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
|
|
} else {
|
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" data-page="${i}">${i}</a></li>`;
|
|
}
|
|
}
|
|
|
|
// Next button
|
|
if (has_next) {
|
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" data-page="${
|
|
current_page + 1
|
|
}">Next</a></li>`;
|
|
} else {
|
|
paginationHTML +=
|
|
'<li class="page-item disabled"><span class="page-link">Next</span></li>';
|
|
}
|
|
|
|
paginationHTML += "</ul></nav>";
|
|
paginationContainer.innerHTML = paginationHTML;
|
|
|
|
// Add click handlers for pagination links
|
|
paginationContainer.querySelectorAll("a[data-page]").forEach((link) => {
|
|
link.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
this.currentPage = parseInt(e.target.dataset.page);
|
|
this.refreshData();
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Specialized table handler for papers
|
|
*/
|
|
class PapersTableHandler extends TableHandler {
|
|
constructor(tableId, options = {}) {
|
|
super(tableId, {
|
|
apiEndpoint: "/api/papers",
|
|
...options,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render a paper row
|
|
* @param {object} paper - Paper data object
|
|
* @returns {string} HTML string for table row
|
|
*/
|
|
renderPaperRow(paper) {
|
|
const statusBadge = createStatusBadge(paper.status);
|
|
const truncatedTitle = truncateText(paper.title, 70);
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<a href="#" class="paper-link" data-url="/papers/${
|
|
paper.id
|
|
}/detail">
|
|
${truncatedTitle}
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<a href="https://doi.org/${paper.doi}" target="_blank">
|
|
${paper.doi || "N/A"}
|
|
</a>
|
|
</td>
|
|
<td>${paper.journal || "N/A"}</td>
|
|
<td>${paper.issn || "N/A"}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${formatTimestamp(paper.created_at)}</td>
|
|
<td>${formatTimestamp(paper.updated_at)}</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Load and display papers data
|
|
* @param {object} params - Query parameters
|
|
*/
|
|
async loadPapers(params = {}) {
|
|
this.showLoading();
|
|
|
|
try {
|
|
const queryString = new URLSearchParams(params).toString();
|
|
const url = `${this.options.apiEndpoint}?${queryString}`;
|
|
|
|
const response = await fetch(url);
|
|
const data = await response.json();
|
|
|
|
if (data.papers) {
|
|
this.renderData(data.papers, (paper) => this.renderPaperRow(paper));
|
|
|
|
if (data.pagination) {
|
|
this.updatePagination(data.pagination);
|
|
}
|
|
} else {
|
|
this.showNoData();
|
|
}
|
|
} catch (error) {
|
|
console.error("Error loading papers:", error);
|
|
this.showNoData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refresh data implementation
|
|
*/
|
|
refreshData() {
|
|
this.loadPapers(this.buildQueryParams());
|
|
}
|
|
}
|