Backup #2200

ID2200
Dateipfad/var/www/dev.campus.systemische-tools.de/public/js/components/data-table.js
Version1
Typ modified
Größe10.3 KB
Hashc56e2701ccc5542f0e1119585fdb76347b2af63b49d321661f1cf7b707a85ff6
Datum2026-01-01 18:09:16
Geändert vonclaude-code-hook
GrundClaude 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

Herunterladen

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