/** * 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()); } }