Backup #2200
| ID | 2200 |
| Dateipfad | /var/www/dev.campus.systemische-tools.de/public/js/components/data-table.js |
| Version | 1 |
| Typ |
modified |
| Größe | 10.3 KB |
| Hash | c56e2701ccc5542f0e1119585fdb76347b2af63b49d321661f1cf7b707a85ff6 |
| Datum | 2026-01-01 18:09:16 |
| Geändert von | claude-code-hook |
| Grund | Claude Code Pre-Hook Backup vor Edit-Operation |
| Datei existiert |
Ja
|
Dateiinhalt
/**
* 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,
`<td colspan="${this.headers.length}" style="text-align:center;">Keine Einträge gefunden</td>`
);
domAdapter.appendChild(this.tbody, emptyRow);
} else {
pageRows.forEach((row) => domAdapter.appendChild(this.tbody, row.element));
}
this.renderPagination(visibleRows.length, totalPages);
}
renderPagination(totalItems, totalPages) {
// Clear previous pagination
domAdapter.setInnerHTML(this.paginationContainer, "");
// Don't show pagination if only one page
if (totalPages <= 1) {
return;
}
// Info text
const startItem = (this.currentPage - 1) * this.pageSize + 1;
const endItem = Math.min(this.currentPage * this.pageSize, totalItems);
const info = domAdapter.createElement("span");
domAdapter.addClass(info, "pagination__info");
domAdapter.setTextContent(info, `${startItem}-${endItem} von ${totalItems}`);
domAdapter.appendChild(this.paginationContainer, info);
// Buttons container
const buttons = domAdapter.createElement("span");
domAdapter.addClass(buttons, "pagination__buttons");
// Previous button
const prevBtn = domAdapter.createElement("button");
domAdapter.addClass(prevBtn, "pagination__btn");
domAdapter.setTextContent(prevBtn, "←");
if (this.currentPage === 1) {
domAdapter.setAttribute(prevBtn, "disabled", "true");
}
const prevCleanup = eventAdapter.on(
prevBtn,
"click",
() => this.goToPage(this.currentPage - 1),
this.deps,
"DATATABLE_PAGE_PREV",
"datatable"
);
this.cleanupFunctions.push(prevCleanup);
domAdapter.appendChild(buttons, prevBtn);
// Page numbers
const maxButtons = 5;
let startPage = Math.max(1, this.currentPage - Math.floor(maxButtons / 2));
const endPage = Math.min(totalPages, startPage + maxButtons - 1);
startPage = Math.max(1, endPage - maxButtons + 1);
for (let i = startPage; i <= endPage; i++) {
const pageBtn = domAdapter.createElement("button");
domAdapter.addClass(pageBtn, "pagination__btn");
if (i === this.currentPage) {
domAdapter.addClass(pageBtn, "pagination__btn--active");
}
domAdapter.setTextContent(pageBtn, String(i));
const page = i;
const pageCleanup = eventAdapter.on(
pageBtn,
"click",
() => this.goToPage(page),
this.deps,
"DATATABLE_PAGE",
"datatable"
);
this.cleanupFunctions.push(pageCleanup);
domAdapter.appendChild(buttons, pageBtn);
}
// Next button
const nextBtn = domAdapter.createElement("button");
domAdapter.addClass(nextBtn, "pagination__btn");
domAdapter.setTextContent(nextBtn, "→");
if (this.currentPage === totalPages) {
domAdapter.setAttribute(nextBtn, "disabled", "true");
}
const nextCleanup = eventAdapter.on(
nextBtn,
"click",
() => this.goToPage(this.currentPage + 1),
this.deps,
"DATATABLE_PAGE_NEXT",
"datatable"
);
this.cleanupFunctions.push(nextCleanup);
domAdapter.appendChild(buttons, nextBtn);
domAdapter.appendChild(this.paginationContainer, buttons);
}
refresh() {
this.cacheRows();
this.applyFilters();
}
dispose() {
this.cleanupFunctions.forEach((cleanup) => cleanup());
this.cleanupFunctions = [];
if (this.paginationContainer !== null && this.paginationContainer.parentNode !== null) {
this.paginationContainer.parentNode.removeChild(this.paginationContainer);
}
}
}
/**
* Initialize DataTable module with dependencies.
* @param {Object} deps - Injected dependencies
* @param {Object} deps.clock - Clock interface { now_epoch_ms_utc: () => number }
* @param {Object} deps.logger - Logger interface
* @param {Object} deps.ui - UI interface { showMessage, disableFeature, offerRetry }
* @param {string} deps.runId - Unique run identifier
* @returns {Object} DataTable factory
*/
export function init(deps) {
return {
/**
* Creates a new DataTable instance.
* @param {string} tableId - ID of the table element
* @param {Object} options - Configuration options
* @param {string} [options.searchInput] - ID of search input element
* @param {Object} [options.filters] - Filter configuration { selectId: columnIndex }
* @param {number} [options.pageSize=20] - Number of rows per page
* @returns {DataTable} DataTable instance
*/
create: (tableId, options = {}) => new DataTable(tableId, options, deps)
};
}
Vollständig herunterladen
Aktionen
Andere Versionen dieser Datei
| ID |
Version |
Typ |
Größe |
Datum |
| 2201 |
2 |
modified |
10.5 KB |
2026-01-01 18:09 |
| 2200 |
1 |
modified |
10.3 KB |
2026-01-01 18:09 |
← Zurück zur Übersicht