/** * DataTable Component with Pagination * Contract: js-browser-architecture-contract_v2.yaml, html-tables-contract_v1.0.yaml * Principles: DRY, KISS, OOP, SRP, YAGNI */ import { domAdapter } from "../adapters/domAdapter.js"; import { eventAdapter } from "../adapters/eventAdapter.js"; class DataTable { constructor(tableId, options, deps) { this.deps = deps; this.cleanupFunctions = []; this.table = domAdapter.getElementById(tableId); if (this.table === null) { return; } this.tbody = domAdapter.querySelector(this.table, "tbody"); this.headers = domAdapter.querySelectorAll(this.table, "th[data-sort]"); this.rows = []; this.sortColumn = null; this.sortDirection = "asc"; this.searchInput = options.searchInput !== undefined ? domAdapter.getElementById(options.searchInput) : null; this.filters = options.filters !== undefined ? options.filters : {}; // Pagination this.pageSize = options.pageSize !== undefined ? options.pageSize : 20; this.currentPage = 1; this.paginationContainer = null; this.setup(); } setup() { this.cacheRows(); this.bindSorting(); this.bindSearch(); this.bindFilters(); this.createPaginationUI(); this.render(); } cacheRows() { const rows = domAdapter.querySelectorAll(this.tbody, "tr"); this.rows = Array.from(rows).map((row) => { const cells = domAdapter.querySelectorAll(row, "td"); return { element: row, data: Array.from(cells).map((td) => domAdapter.getTextContent(td).trim().toLowerCase() ), raw: Array.from(cells).map((td) => domAdapter.getTextContent(td).trim() ) }; }); } bindSorting() { this.headers.forEach((header, index) => { domAdapter.setStyle(header, "cursor", "pointer"); const cleanup = eventAdapter.on( header, "click", () => this.sort(index, domAdapter.getAttribute(header, "data-sort")), this.deps, "DATATABLE_SORT", "datatable" ); this.cleanupFunctions.push(cleanup); }); } sort(columnIndex, columnName) { const direction = this.sortColumn === columnName && this.sortDirection === "asc" ? "desc" : "asc"; this.sortColumn = columnName; this.sortDirection = direction; this.headers.forEach((h) => { domAdapter.removeClass(h, "sort-asc"); domAdapter.removeClass(h, "sort-desc"); }); const activeHeader = domAdapter.querySelector( this.table, `th[data-sort="${columnName}"]` ); if (activeHeader !== null) { domAdapter.addClass(activeHeader, `sort-${direction}`); } this.rows.sort((a, b) => { const valA = a.raw[columnIndex] !== undefined ? a.raw[columnIndex] : ""; const valB = b.raw[columnIndex] !== undefined ? b.raw[columnIndex] : ""; const numA = parseFloat(valA); const numB = parseFloat(valB); if (!isNaN(numA) && !isNaN(numB)) { return direction === "asc" ? numA - numB : numB - numA; } return direction === "asc" ? valA.localeCompare(valB, "de") : valB.localeCompare(valA, "de"); }); this.currentPage = 1; this.render(); } bindSearch() { if (this.searchInput === null) { return; } const cleanup = eventAdapter.on( this.searchInput, "input", () => { this.currentPage = 1; this.applyFilters(); }, this.deps, "DATATABLE_SEARCH", "datatable" ); this.cleanupFunctions.push(cleanup); } bindFilters() { Object.entries(this.filters).forEach(([id, _columnIndex]) => { const select = domAdapter.getElementById(id); if (select !== null) { const cleanup = eventAdapter.on( select, "change", () => { this.currentPage = 1; this.applyFilters(); }, this.deps, "DATATABLE_FILTER", "datatable" ); this.cleanupFunctions.push(cleanup); } }); } applyFilters() { const searchTerm = this.searchInput !== null ? this.searchInput.value.toLowerCase() : ""; this.rows.forEach((row) => { let visible = true; if (searchTerm !== "") { visible = row.data.some((cell) => cell.includes(searchTerm)); } Object.entries(this.filters).forEach(([id, columnIndex]) => { const select = domAdapter.getElementById(id); if (select !== null && select.value !== "") { visible = visible && row.data[columnIndex] === select.value.toLowerCase(); } }); row.visible = visible; }); this.render(); } createPaginationUI() { this.paginationContainer = domAdapter.createElement("div"); domAdapter.addClass(this.paginationContainer, "pagination"); this.table.parentNode.insertBefore( this.paginationContainer, this.table.nextSibling ); } getVisibleRows() { return this.rows.filter((row) => row.visible !== false); } getTotalPages() { const visibleRows = this.getVisibleRows(); return Math.ceil(visibleRows.length / this.pageSize); } goToPage(page) { const totalPages = this.getTotalPages(); if (page < 1) { this.currentPage = 1; } else if (page > totalPages) { this.currentPage = totalPages; } else { this.currentPage = page; } this.render(); } render() { domAdapter.setInnerHTML(this.tbody, ""); const visibleRows = this.getVisibleRows(); const totalPages = this.getTotalPages(); if (this.currentPage > totalPages && totalPages > 0) { this.currentPage = totalPages; } const startIndex = (this.currentPage - 1) * this.pageSize; const endIndex = startIndex + this.pageSize; const pageRows = visibleRows.slice(startIndex, endIndex); if (pageRows.length === 0) { const emptyRow = domAdapter.createElement("tr"); domAdapter.setInnerHTML( emptyRow, `