/**
* 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 = `
${this.options.loadingText} |
`;
}
}
/**
* Show no data message
*/
showNoData() {
const tbody = this.table.querySelector("tbody");
if (tbody) {
const colCount = this.table.querySelectorAll("th").length;
tbody.innerHTML = `
${this.options.noDataText} |
`;
}
}
/**
* 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 = '";
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 `
${truncatedTitle}
|
${paper.doi || "N/A"}
|
${paper.journal || "N/A"} |
${paper.issn || "N/A"} |
${statusBadge} |
${formatTimestamp(paper.created_at)} |
${formatTimestamp(paper.updated_at)} |
`;
}
/**
* 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());
}
}