import { ElementCreator } from 'tosijs'; import { SortCallback } from './make-sorter.js'; import { RowGroupIdFn, GroupCount } from './row-grouping.js'; import { ValueRendererType } from './value-renderer.js'; import type { JSONSchema } from './schema-form/json-schema.js'; export interface ColumnOptions { name?: string; prop: string; width: number; visible?: boolean; align?: string; type?: ValueRendererType; pinned?: 'left' | 'right'; sort?: false | 'ascending' | 'descending'; /** * What this column SORTS by, when that differs from what it stores. * * Defaults to `row[prop]`. A column with a custom `dataCell` renders whatever it likes * while the sort keys on `prop` — so when those differ, clicking "Sort Ascending" * reorders rows by a value the reader cannot see, which reads as "sorting is broken" * rather than "sorting a different field" (tosijs-ui#62). * * { name: 'Invoice #', prop: 'Customer invoice ID', dataCell: invoiceCell, * sortValue: (row) => row['Invoice number'] || row['Customer invoice ID'] } * * Every other escape hatch costs more: `table.sort` is table-wide (one derived column * means reimplementing sorting for all of them), replacing `headerCell` means * reimplementing the header menu, and changing `prop` breaks CSV export and anything * else keyed on it. */ sortValue?: (row: any) => unknown; /** * Whether this column's cells are editable. Defaults to the table's `editable`. * * Set `false` on a computed or identifying column of an otherwise-editable table, or * `true` to make one column editable in a read-only one. A column with its own * `dataCell` is never made editable — a custom cell builds and binds itself, and the * table has no business reaching into it. */ editable?: boolean; /** * Render this column as the row's disclosure toggle for `rowDetail`. The cell holds one * button; `prop` is unused (give it any unique string) and the header is left blank * unless you set `name`. */ expander?: boolean; headerCell?: (options: ColumnOptions) => HTMLElement; dataCell?: (options: ColumnOptions) => HTMLElement; } /** Builds the element shown under an expanded row. `null` means the row has no detail. */ export type RowDetailFn = (row: any) => HTMLElement | null; export interface TableData { columns?: ColumnOptions[] | null; array: any[]; filter?: ArrayFilter | null; } export type ArrayFilter = (array: any[]) => any[]; export type SelectCallback = (selected: any[]) => void; export declare function probeMaxElementHeight(): number; /** * Rows that fit inside the browser's layout ceiling. * * `fallback` covers the two cases where the ceiling tells us nothing: no DOM to probe, and * `rowHeight: 0`. The second is not a measurement failure but a different regime — with no * fixed row height there is no virtualisation, so every row becomes a real DOM node and the * cost genuinely is O(n). A cap earns its keep there; in virtual mode it does not. */ export declare function derivedMaxVisibleRows(maxElementHeightPx: number, rowHeight: number, fallback?: number): number; declare const TosiTable_base: import("tosijs").WithAttributes<{ rowHeight: number; charWidth: number; minColumnWidth: number; select: boolean; multiple: boolean; pinnedTop: number; pinnedBottom: number; nosort: boolean; nohide: boolean; noreorder: boolean; localized: boolean; nopreservescroll: boolean; editable: boolean; fullWidthHeader: boolean; }>; export declare class TosiTable extends TosiTable_base { static preferredTagName: string; static lightStyleSpec: { ':host': { '--tosi-table-row-height': string; '--tosi-table-touch-size': string; '--tosi-table-dragged-header-bg': string; '--tosi-table-dragged-header-color': string; '--tosi-table-drop-header-bg': string; display: string; overflow: string; background: string; }; ':host .scroll-area': { width: string; height: string; overflow: string; overscrollBehavior: string; }; ':host .thead, :host .tbody': { display: string; }; ':host .tr': { display: string; gridTemplateColumns: string; width: string; height: string; background: string; }; ':host .thead .tr': { position: string; top: string; zIndex: string; background: string; }; ':host .tbody-pinned-top .tr, :host .tbody-pinned-bottom .tr': { position: string; zIndex: string; }; ':host .th, :host .td': { overflow: string; whiteSpace: string; textOverflow: string; display: string; alignItems: string; height: string; lineHeight: string; }; ':host .col-pinned': { position: string; zIndex: string; background: string; }; ':host .th.col-pinned': { zIndex: string; background: string; }; ':host .tr:not(.table-cluster-first) .cluster-repeat': { color: string; userSelect: string; }; ':host .tr:not(.table-cluster-first) .cluster-repeat > *': { display: string; }; ':host .tr.table-cluster-odd': { _tosiTableBg: string; }; ':host .tr[aria-selected] .td': { background: string; }; ':host .td:focus, :host .th:focus': { outline: string; outlineOffset: string; zIndex: string; }; ':host .col-pinned:focus': { zIndex: string; }; ':host .col-edge-right': { boxShadow: string; }; ':host .col-edge-left': { boxShadow: string; }; ':host .row-edge-bottom': { boxShadow: string; }; ':host .row-edge-top': { boxShadow: string; }; ':host .scroll-area.has-row-detail': { containerType: string; }; ':host .tr.row-expanded': { height: string; }; ':host .tr-detail': { gridColumn: string; position: string; left: string; width: string; boxSizing: string; padding: string; background: string; boxShadow: string; }; ':host .td.expander-cell': { justifyContent: string; padding: number; }; ':host .row-expander': { color: string; background: string; boxShadow: string; padding: number; display: string; alignItems: string; justifyContent: string; width: string; height: string; }; ':host .row-expander[hidden]': { display: string; }; ':host .row-expander > *': { transition: string; }; ':host .row-expander[aria-expanded="true"] > *': { transform: string; }; ':host .th .menu-trigger': { color: string; background: string; padding: number; lineHeight: string; height: string; width: string; }; ':host [draggable="true"]': { cursor: string; }; ':host [draggable="true"]:active': { background: string; color: string; }; ':host .drag-over': { background: string; }; }; /** * Optional JSON Schema for the row shape. Drives editable cells and validates edits. * * The SAME model `` uses (`src/schema-form/fields.ts`), so a cell and a * field agree about what a property is — one description of the data, two surfaces. That * was the point of building the model DOM-free: #44 asked for an editable table, and the * alternative was a second, drifting answer to "what control does this property want". */ private _schema; get schema(): JSONSchema | null; set schema(schema: JSONSchema | null); selectionChanged: SelectCallback; private _rowDetail; private _rowId; /** Keys of the expanded rows: `rowId(row)` when there is a `rowId`, else the row itself. */ private _expanded; /** One detail container per expanded row, kept so its state survives scrolling. */ private _detailEls; /** The row each detail was built from: a detail is only as current as its row. */ private _detailRows; private _detailCount; /** * Builds the element shown under an expanded row, from that row's data. Return `null` * for a row with no detail. Called once per expansion: the element is kept while the row * stays expanded (scrolling it out of view and back does not rebuild it) and dropped when * the row collapses or the table's `array` is replaced. */ get rowDetail(): RowDetailFn | null; set rowDetail(fn: RowDetailFn | null); /** * Which rows have a detail, for the `expander` column: a row this returns `false` for * gets no toggle. Unset, every row gets one. */ rowHasDetail: ((row: any) => boolean) | null; /** * A stable id for a row. Without it expansion follows the row OBJECT, which is enough * for sorting and filtering; with it expansion also survives replacing `array` with new * objects, and `expandedRowIds` can be read and set. */ get rowId(): ((row: any) => string) | null; set rowId(fn: ((row: any) => string) | null); private _pendingExpandedIds; private _warnedPendingIds; /** * Ids of the expanded rows. Needs `rowId`; without one this reads empty. Assigning it * fires no `rowexpand` / `rowcollapse`: those report a toggle, not a restore. */ get expandedRowIds(): string[]; set expandedRowIds(ids: string[] | null); private forgetDetails; private detailKey; isRowExpanded(row: any): boolean; /** * Expand or collapse a row's detail (`open` omitted toggles). Fires `rowexpand` or * `rowcollapse` with `detail: { row, element }`. The toggled row keeps its place on * screen. */ toggleRowDetail(row: any, open?: boolean): void; /** The detail container for a row, built on first use. `null` when the row has none. */ private detailFor; private stampedRow; /** A row's cells: its children, less the detail that an expanded row also holds. */ private rowCells; private syncDetail; private syncStampedDetails; /** Have the virtual list re-measure its rows after one changed height. */ private remeasureRows; /** Is this event target inside one of THIS table's row details? */ private inDetail; rowRendered: ((item: any, cells: HTMLElement[]) => void) | null; private selectedKey; private selectBinding; /** * Most rows the table will lay out. Set it to override; leave it alone and it is * DERIVED from what the browser can actually lay out (see `derivedMaxVisibleRows`). * * **This is a LAYOUT limit, not a rendering-cost limit** — which is where the old flat * `10000` went wrong. A virtual `listBinding` renders only the visible window, so the UI * side is O(1) in row count and the size of the array is irrelevant to render cost. The * one thing that grows is the spacer element's height, and the one hard failure is the * browser refusing to lay out an element that tall. So the cap belongs at * `maxElementHeight / rowHeight`, not at a number someone picked. * * The old value was ~42x more conservative than that, and it silently `slice`d * everything past it — a 25,000-row table showed 10,000, said nothing, and every count, * filter and sort ran on the truncated set (tosijs-ui#82). Consumers routinely load * 300k+ rows with no UI cost at all, so the number was wrong in the direction that loses * data, and it was documented nowhere. */ get maxVisibleRows(): number; set maxVisibleRows(rows: number); private _maxVisibleRows; /** So the truncation notice is printed once per table, not once per render. */ private _warnedTruncation; private _head; private _scrollArea; private _tbodyTop; private _tbodyBottom; private _pinnedRowEdgeObserver; private _rowCellsCache; private itemFor; private cellsFor; get value(): TableData; set value(data: TableData); private rowData; private _array; private _columns; private _renderedCols; private _filter; private _sort?; private _rowGroupId; private _visibleGroupedRowIds; private _visibleMemo; private computeVisibleRows; private _nonRepeatingGroupedRowCells; private _pinnedTopRows?; private _pinnedBottomRows?; get pinnedTopRows(): any[] | null; set pinnedTopRows(rows: any[] | null | undefined); get pinnedBottomRows(): any[] | null; set pinnedBottomRows(rows: any[] | null | undefined); get effectivePinnedTopData(): any[]; get effectivePinnedBottomData(): any[]; private get effectiveBaseData(); constructor(); get array(): any[]; set array(newArray: any[]); get filter(): ArrayFilter; set filter(filterFunc: ArrayFilter); get rowGroupId(): RowGroupIdFn | null; set rowGroupId(fn: RowGroupIdFn | null); get visibleGroupedRowIds(): string[] | null; set visibleGroupedRowIds(ids: string[] | null); get nonRepeatingGroupedRowCells(): string[] | null; set nonRepeatingGroupedRowCells(props: string[] | null); /** The grouping function in force, or null when the table is ungrouped. */ private _groupIdMemo; private get groupIdFn(); /** * This row's group id, or null when the table is ungrouped. * * Public because the grouping may be INFERRED from `nonRepeatingGroupedRowCells`, in which * case the consumer never wrote the function and cannot reproduce its ids — which would * leave `rowGroupCounts` keyed by strings they have no way to construct. */ groupIdFor(row: any): string | null; /** * Per-group `{ visible, total }` counts — rendered rows against rows before filtering. * * For cells that report or control their own group: "showing 2 of 7", or a toggle that * adds the group to `visibleGroupedRowIds`. The table is the only thing that sees both * sides of the filter, so this is the piece a custom cell renderer cannot derive itself. * * Recomputed each render, and always a Map — empty when ungrouped — so callers can `.get()` * without a null check. Groups filtered away entirely are present with `visible: 0`; * pinned rows sit outside grouping and are not counted. */ get rowGroupCounts(): Map; private _grouping; private _rowGroupCounts; /** * Keep the reader in place across a re-render (default `true`). * * Set `false` when a render means "here is a different dataset" rather than "here is the * same data, re-viewed" — then starting at the top is the correct answer. * * Backed by the `nopreservescroll` ATTRIBUTE so it is settable from markup like every * other boolean here (`nosort`, `nohide`, `noreorder`). The inverted name is not * gratuitous: this defaults to `true`, and a presence-only attribute can only ever turn * something ON — so a `preservescroll` attribute could never express "off", which is the * only thing anyone needs to say. Getting this wrong is unfixable after release without a * rename, so it is settled here rather than later. */ get preserveScroll(): boolean; set preserveScroll(value: boolean); /** Horizontal scroll offset to put back after a re-render. */ private _scrollLeft; /** In-flight scroll restore, so a newer one supersedes it and gestures can cancel it. */ private _scrollRestore; private _scrollAnchor; get sort(): SortCallback | undefined; set sort(sortFunc: SortCallback | undefined); get columns(): ColumnOptions[]; set columns(newColumns: ColumnOptions[]); get visibleColumns(): ColumnOptions[]; /** @deprecated Set pinned: 'left' on individual columns instead */ get pinnedLeft(): number; /** @deprecated Set pinned: 'left' on individual columns instead */ set pinnedLeft(n: number); /** @deprecated Set pinned: 'right' on individual columns instead */ get pinnedRight(): number; /** @deprecated Set pinned: 'right' on individual columns instead */ set pinnedRight(n: number); content: null; private computeStickyInfo; private cellClasses; private rowClasses; private tagPinnedRows; private tagPinnedTbody; private cellStyle; private applyGridCellAttrs; /** Is this column editable? Table-level default, per-column override, `dataCell` wins. */ private columnEditable; private handleCellChange; private _fieldCache; private fieldFor; /** * Validate one edited cell against the schema. `undefined` when it conforms — or when * there is no schema, because a table with no description of its data cannot be wrong * about it. */ private validateCell; private buildEditableCell; private buildCell; private tagClusterParity; /** * Is this row the first of its group? Always true when the table is ungrouped. * * Exposed for custom `dataCell` columns, which render themselves and so have to decide * for themselves whether to show a value the rest of the group repeats. */ isFirstInGroup(row: any): boolean; private isRepeatedGroupRow; private buildRow; private buildHeaderCell; private buildHeader; private buildPinnedBody; getColumn(event: any): ColumnOptions | undefined; private setCursor; private resizeColumn; private stampSelection; private applySelection; selectRow(row: any, select?: boolean): void; selectRows(rows?: any[], select?: boolean): void; deSelect(rows?: any[]): void; private updateSelectionVisuals; private rangeStart?; private updateSelection; private findCell; private _pendingFocus; private captureScrollAnchor; private restoreScrollAnchor; private handleScrollEnd; private focusCell; private handleKeyNav; connectedCallback(): void; setColumnWidths(): void; sortByColumn: (columnOptions: ColumnOptions, direction?: "ascending" | "descending" | "auto") => void; popColumnMenu: (target: HTMLElement, options: ColumnOptions) => void; get captionSpan(): ElementCreator; labelAttrs(key: string, attr?: string): Record; get visibleRows(): any[]; get visibleSelectedRows(): any[]; get selectedRows(): any[]; getCells(itemOrCell: any): HTMLElement[] | undefined; getItem(cell: Element): any; private draggedColumn?; private dropColumn; render(): void; private observePinnedRowMutations; } /** @deprecated Use TosiTable instead */ export type DataTable = TosiTable; /** @deprecated Use TosiTable instead */ export declare const DataTable: typeof TosiTable; export declare const tosiTable: ElementCreator; /** @deprecated Use tosiTable instead */ export declare const dataTable: ElementCreator; /** @deprecated Use tosiTable instead */ export declare const xinTable: ElementCreator; export {};