Some checks are pending
CI / check (push) Waiting to run
- 51 headless Web Components (45 core + 6 animation) - Shared helpers: emit(), part(), listen(), wireLabel(), initialValue() - Zero `new CustomEvent` or `static #counter` — all use shared utils - Zod schemas for all 44 core components - MCP package with discover, inspect, compose, validate tools - Showcase with Aperture Science theme, M3 Expressive motion - 81 tests passing, TypeScript strict mode clean - Signals (~500B), SPA router (~400B), zero dependencies
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
import { emit } from "../../shared/helpers";
|
|
|
|
/** PettyDataTable — sortable table with click-to-sort column headers. */
|
|
export class PettyDataTable extends HTMLElement {
|
|
connectedCallback(): void {
|
|
const headers = this.querySelectorAll<HTMLElement>("th[data-sort]");
|
|
for (const th of headers) {
|
|
th.setAttribute("role", "columnheader");
|
|
th.setAttribute("aria-sort", "none");
|
|
th.style.cursor = "pointer";
|
|
th.addEventListener("click", this.#onHeaderClick);
|
|
}
|
|
}
|
|
|
|
disconnectedCallback(): void {
|
|
const headers = this.querySelectorAll<HTMLElement>("th[data-sort]");
|
|
for (const th of headers) th.removeEventListener("click", this.#onHeaderClick);
|
|
}
|
|
|
|
#onHeaderClick = (e: Event): void => {
|
|
const th = e.currentTarget as HTMLElement;
|
|
const column = th.dataset.sort ?? "";
|
|
const current = th.getAttribute("aria-sort") ?? "none";
|
|
const next = current === "ascending" ? "descending" : "ascending";
|
|
this.#clearSorts();
|
|
th.setAttribute("aria-sort", next);
|
|
th.dataset.state = next;
|
|
this.#sortBy(th, next === "ascending" ? 1 : -1);
|
|
emit(this, "sort", { column, direction: next });
|
|
};
|
|
|
|
#clearSorts(): void {
|
|
const headers = this.querySelectorAll<HTMLElement>("th[data-sort]");
|
|
for (const th of headers) { th.setAttribute("aria-sort", "none"); th.dataset.state = "none"; }
|
|
}
|
|
|
|
#sortBy(th: HTMLElement, dir: number): void {
|
|
const tbody = this.querySelector("tbody[data-part=body]") ?? this.querySelector("tbody");
|
|
if (!tbody) return;
|
|
const headerRow = th.parentElement;
|
|
if (!headerRow) return;
|
|
const colIdx = Array.from(headerRow.children).indexOf(th);
|
|
const rows = Array.from(tbody.querySelectorAll("tr"));
|
|
rows.sort((a, b) => {
|
|
const aText = a.children[colIdx]?.textContent?.trim() ?? "";
|
|
const bText = b.children[colIdx]?.textContent?.trim() ?? "";
|
|
const aNum = Number(aText);
|
|
const bNum = Number(bText);
|
|
if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) return (aNum - bNum) * dir;
|
|
return aText.localeCompare(bText) * dir;
|
|
});
|
|
for (const row of rows) tbody.appendChild(row);
|
|
}
|
|
}
|