Newer
Older
vmk-ui-kit / src / js / components / tables.js
/**
 * VMK UI Kit browser table helper.
 *
 * Mirrors the public surface of gnexus-ui-kit tables:
 *   init(root), selectRow(row), sortBy(column)
 */

const initializedRoots = new WeakSet();

function getRows(table) {
  return [...table.querySelectorAll("tbody tr")];
}

function getHeaders(table) {
  return [...table.querySelectorAll("thead th")];
}

function selectRow(row, options = {}) {
  const table = row?.closest("table");
  if (!table) return;

  if (table.classList.contains("table-selectable")) {
    getRows(table).forEach(r => r.classList.toggle("is-selected", r === row));
  }

  if (options.focus !== false) {
    row.focus?.();
  }
}

function parseValue(cell) {
  const text = cell.textContent.trim();
  const num = Number(text.replace(/[^0-9.\-]/g, ""));
  return Number.isNaN(num) ? text.toLowerCase() : num;
}

function sortBy(table, columnIndex, direction = "asc") {
  const rows = getRows(table);
  const sorted = rows.slice().sort((a, b) => {
    const aValue = parseValue(a.children[columnIndex] || a.children[0]);
    const bValue = parseValue(b.children[columnIndex] || b.children[0]);
    if (aValue < bValue) return direction === "asc" ? -1 : 1;
    if (aValue > bValue) return direction === "asc" ? 1 : -1;
    return 0;
  });

  const tbody = table.querySelector("tbody");
  sorted.forEach(row => tbody.appendChild(row));

  getHeaders(table).forEach((header, index) => {
    header.setAttribute("aria-sort", index === columnIndex ? direction : "none");
  });
}

function handleClick(event) {
  const header = event.target.closest("thead th[data-sortable]");
  if (header) {
    const table = header.closest("table");
    const index = getHeaders(table).indexOf(header);
    const current = header.getAttribute("aria-sort") || "none";
    const direction = current === "asc" ? "desc" : "asc";
    sortBy(table, index, direction);
    return;
  }

  const row = event.target.closest("tbody tr");
  if (row) {
    selectRow(row, { focus: false });
  }
}

function prepare(table) {
  if (table.classList.contains("table-sortable")) {
    getHeaders(table).forEach((header, index) => {
      if (header.hasAttribute("data-sortable")) return;
      header.setAttribute("data-sortable", "");
      header.setAttribute("aria-sort", "none");
      header.style.cursor = "pointer";
    });
  }
}

function init(root = document) {
  if (initializedRoots.has(root)) return;

  root.querySelectorAll("table.table").forEach(prepare);
  root.addEventListener("click", handleClick);
  initializedRoots.add(root);
}

export default {
  init,
  selectRow,
  sortBy
};