import { TemplateResult } from 'lit'; import { BahtElement } from '../base.js'; export type CellRenderer = (value: unknown, row: Record) => unknown; /** Group key for `groupBy`: a (dotted) row key, or a function of the row. */ export type GroupKey = string | ((row: Record) => unknown); /** Group header content: the group's value plus the rows in that group. */ export type GroupLabelRenderer = (value: unknown, rows: Record[]) => unknown; /** `baht-group-toggle` detail: which group and whether it is now collapsed. */ export interface GroupToggleDetail { value: unknown; collapsed: boolean; } export type ColumnAlign = 'start' | 'center' | 'end'; /** * One column: a plain label, or a spec. A spec may carry `fields` (a * reference column the picker can dot-walk into, `requester.department`; * fields nest), `sortable` (header becomes a sort button), `numeric` * (right-aligned tabular figures) and `align` (explicit alignment). */ export interface ColumnSpec { label: string; fields?: ColumnMap; sortable?: boolean; numeric?: boolean; align?: ColumnAlign; } export type ColumnMap = Record; export type SortDirection = 'asc' | 'desc'; export interface SortState { key: string; direction: SortDirection; } /** What the picker committed: dotted keys plus the labels the header shows. */ export interface ColumnsChangeDetail { visibleColumns: string[]; columns: { key: string; label: string; }[]; } /** * Server-side data table. `items` is ONE page of rows — the component never * slices or fetches; on `baht-page-change` the host fetches the next page and * re-feeds `items`/`page`. While that fetch runs, set `loading` to show * skeleton rows. * * const t = document.querySelector('baht-table'); * t.columns = { name: 'Name', created_at: 'Created' }; * t.items = pageRows; // current page only * t.loading = false; t.page = 1; t.totalPages = 12; * t.addEventListener('baht-page-change', (e) => fetchPage(e.detail.page)); * t.addEventListener('baht-row-click', (e) => open(e.detail.id)); * * Set `cursor-mode` for cursor-based APIs (no total count) — the footer * becomes a Prev/Next pair gated by `has-next`/`has-prev`, and the host * listens for `baht-cursor-change` instead of `baht-page-change`: * * t.cursorMode = true; t.hasNext = true; t.hasPrev = false; * t.addEventListener('baht-cursor-change', (e) => fetchPage(e.detail.direction)); * * A built-in "Columns" button is ON by default — it opens a ServiceNow-style * Available/Selected dual-list modal (add/remove, reorder, Select all/Clear * all) derived from `columns` (or the first row's keys), no host wiring * required. Set `t.columnPicker = false` (JS property — a boolean attribute * can't express "off") to hide it. OK emits `baht-columns-change` with the * chosen keys and their labels; persist that and feed it back as * `visibleColumns` to remember a person's layout: * * t.addEventListener('baht-columns-change', (e) => save(e.detail.visibleColumns)); * * Dot-walking (spec: reference columns). A column may declare `fields`, in * which case the picker lets the person expand it and pick a sub-field — * the key becomes `requester.department`, the header reads * "Requested by › Department", and the cell resolves the path on the row. * Rows whose values are plain objects get the same treatment automatically * when no `columns` map is given. * * t.columns = { * number: 'Number', * requester: { label: 'Requested by', fields: { name: 'Name', department: 'Department' } }, * }; * t.visibleColumns = ['number', 'requester.name']; * * `visibleColumns` still works standalone for a host-controlled subset (no * refetch) — pass it explicitly and the built-in picker (if also enabled) * starts from that selection and keeps emitting updates for it. * * Toolbar, selection, sorting, row actions (all server-side, all off by default): * * t.total = 143; // "143 results" in the toolbar (search/filter UI lives outside) * t.selectable = true; t.selected = ['g1']; // checkbox column, controlled by row key * t.addEventListener('baht-selection-change', (e) => (t.selected = e.detail.selected)); * t.columns = { amount: { label: 'Amount', numeric: true, sortable: true } }; * t.sort = { key: 'amount', direction: 'desc' }; * t.addEventListener('baht-sort-change', (e) => refetch({ sort: e.detail })); * t.rowActions = true; // trailing "..." button -> baht-row-actions { row, index, id } * * Grouping (ServiceNow "Group by" on a list): `group-by` names a (dotted) * row key, or pass a function as the property. Every row on the CURRENT page * is bucketed by that value in first-appearance order; each bucket is its own * `` headed by a `th[scope=rowgroup]` disclosure reading * "Column: value (n)". Click the header to collapse/expand the group * (`baht-group-toggle`); `group-collapsed` starts them all closed, and the * open/closed state resets when `items`/`page` change. Grouping is per page * only — it rides on the existing pagination and never fetches or sorts, so * a value split across pages shows up on each. `groupLabel` replaces the * default header text (it still sits inside the toggle). Row numbers and * event indexes stay page-global. `hide-header` drops the column header row * for list-style layouts. * * t.groupBy = 'state'; // "State: Open (12)" * t.groupBy = (row) => row.occurred_at.slice(0, 10); * t.groupLabel = (day, rows) => `${formatDay(day)} · ${rows.length}`; * t.groupCollapsed = true; * t.addEventListener('baht-group-toggle', (e) => console.log(e.detail)); // { value, collapsed } */ export declare class BahtTable extends BahtElement { /** Rows of the CURRENT page. */ items: Record[]; /** column key -> display name, or a reference column with `fields` (property); empty = derive keys from the first row (`snake_case` -> `Title Case`). */ columns: ColumnMap; loading: boolean; error: boolean; errorMessage: string; emptyMessage: string; /** Row-number column (No.). Offset lets numbering continue across pages. */ showIndex: boolean; indexOffset: number; skeletonRows: number; /** Which row key identifies a row in baht-row-click detail.id (e.g. 'group_id'). */ rowKey: string; /** Rows emit baht-row-click + get hover/keyboard affordances. */ clickable: boolean; /** Custom cell renderers by column key, dotted keys included (functions — a property, never JSON). */ renderers: Record; /** Subset + order of columns to render (dotted keys allowed); empty/omitted = show all top-level columns. */ visibleColumns: string[]; /** Group the page's rows by this (dotted) column key — or a function of the row (property). Unset = no grouping. */ groupBy?: GroupKey; /** Group header content (property); default is "Column: value (n)". */ groupLabel?: GroupLabelRenderer; /** Group headers are disclosures (click to collapse). Set the JS property to `false` for static headers. */ groupCollapsible: boolean; /** Start every group collapsed (per page — resets when `items`/`page` change). */ groupCollapsed: boolean; /** Drop the column header row (list-style tables, e.g. grouped ledgers). */ hideHeader: boolean; /** Built-in "Columns" toggle — derives its options from `columns`/rows. Set `column-picker="false"` to hide it. */ columnPicker: boolean; /** Total matching rows across all pages — renders "{n} results" in the toolbar. Unset = no count. */ total?: number | undefined; selectable: boolean; selected: unknown[]; /** Make every column sortable (or set `sortable` per column in `columns`). */ sortable: boolean; /** Current sort, mirrored in the header (`aria-sort`); the host keeps it in sync with the fetch. */ sort: SortState | null; /** Trailing "..." button per row; emits `baht-row-actions { row, index, id }`. */ rowActions: boolean; page: number; totalPages: number; /** Switch the footer to Prev/Next (cursor-based API, no total count). */ cursorMode: boolean; hasNext: boolean; hasPrev: boolean; private pickerOpen; private get pickerTitleId(); /** User's picks once they've touched the picker; null = follow `visibleColumns`/all. */ private pickedColumns; /** Working copy edited inside the modal; committed to `pickedColumns` on OK. */ private draftColumns; /** Highlighted (not yet moved) options in each list. */ private pickAvail; private pickSel; /** Reference columns unfolded in the Available tree (dot-walk). */ private expanded; /** Group keys the person toggled away from `groupCollapsed`; reset on a new page. */ private toggledGroups; /** Element focused before the picker opened — focus returns there on close. */ private pickerPreviousFocus; private readonly onPickerKeydown; private pickerFocusables; /** Keep Tab cycling inside the column-picker dialog (no escape to the page behind). */ private trapPickerFocus; disconnectedCallback(): void; protected willUpdate(changed: Map): void; /** The declared map, or one derived from the first row (nested objects become reference columns). */ private get spec(); /** Walk a dotted key through the spec; undefined when no such column exists. */ private specAt; private isReference; private childrenOf; /** Every pickable key, reference fields included (`requester`, `requester.name`, ...). */ private allPaths; private get allColumnKeys(); private get columnKeys(); /** "Requested by › Department" for a dotted key; the plain label otherwise. */ private labelFor; /** The row's value at a dotted key (`requester.department`). */ private static valueAt; private openPicker; private closePicker; private static selectedValues; private toggleAvail; private toggleExpanded; private addToSelected; private removeFromSelected; private moveSelected; private confirmPicker; private idOf; private emitRow; /** Selection key of a row: its `rowKey` value, else its index on this page. */ private keyOf; private isSelected; private emitSelection; private toggleRow; private get pageKeys(); private get allOnPageSelected(); private get someOnPageSelected(); private toggleAll; private isSortable; private requestSort; /** Alignment class for a column (explicit `align` beats `numeric`). */ private alignClass; private cell; private span; private groupValue; /** Stable identity of a group value for the collapsed set (objects by JSON, else by type+string). */ private static groupId; /** * The page's rows bucketed by group value, in first-appearance order. Each * row keeps its page index so numbering and event indexes stay host-order. */ private get groups(); private get isGrouped(); private isCollapsed; private toggleGroup; /** Default header: "Column: value (n)" — no column prefix for a function key. */ private defaultGroupLabel; private renderGroups; private renderBody; private renderRow; private renderHeaderCell; private get hasToolbar(); private renderToolbar; private renderColumnToggle; /** * One row of the Available tree. A reference column gets an expander that * unfolds its fields (the dot-walk); every row is a toggle button so the * list works from the keyboard without the native multi-select. */ private renderAvailRow; private renderColumnModal; protected render(): TemplateResult; private renderHead; private renderFooter; }