@zakkster/lite-table 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +344 -0
- package/README.md +564 -19
- package/Table.d.ts +258 -1
- package/Table.js +1644 -116
- package/llms.txt +291 -6
- package/package.json +15 -9
package/Table.d.ts
CHANGED
|
@@ -36,12 +36,38 @@ export interface ColumnDef<Row = any, Value = unknown> {
|
|
|
36
36
|
pinnable?: boolean;
|
|
37
37
|
hideable?: boolean;
|
|
38
38
|
reorderable?: boolean;
|
|
39
|
+
/** M2: enable double-click / F2 cell editing for this column. Default false. */
|
|
40
|
+
editable?: boolean;
|
|
41
|
+
/** M2: enable the per-column filter input above this column. Default false. */
|
|
42
|
+
filterable?: boolean;
|
|
43
|
+
/** M2: custom filter predicate. Receives the column's value (post-accessor),
|
|
44
|
+
* the current (trimmed) filter query string, and the full row. Return
|
|
45
|
+
* true to keep the row in the result. Defaults to case-insensitive
|
|
46
|
+
* substring match on the stringified value. */
|
|
47
|
+
filter?: (value: Value, query: string, row: Row) => boolean;
|
|
48
|
+
/** M2: placeholder text for the filter input. Default "Filter…". */
|
|
49
|
+
filterPlaceholder?: string;
|
|
50
|
+
/** M3: aggregate reducer for group-header + grand-total cells. One of the
|
|
51
|
+
* built-in strings, or a custom `(rows, col) => value` function. `null`
|
|
52
|
+
* or omitted means "no aggregate" -- the column shows blank in header
|
|
53
|
+
* rows. Aggregates always fold over LEAF rows at every depth (safe
|
|
54
|
+
* for non-associative reducers like median or last-value-wins). */
|
|
55
|
+
aggregate?: AggregateSpec<Row> | null;
|
|
56
|
+
/** M3: format the aggregate for display. Only affects rendered text --
|
|
57
|
+
* `entry.aggregates.get(key)` still returns the raw value so consumers
|
|
58
|
+
* can format independently for export etc. Errors are caught + logged. */
|
|
59
|
+
aggregateFormat?: (value: unknown, col: ColumnState<Row>, count: number) => string;
|
|
39
60
|
/** Computed value override; if absent, `row[key]` is used. */
|
|
40
61
|
accessor?: (row: Row) => Value;
|
|
41
62
|
/** Sort comparator; default is null-safe number-or-string compare. */
|
|
42
63
|
compare?: (a: Value, b: Value) => number;
|
|
43
64
|
}
|
|
44
65
|
|
|
66
|
+
/** M3: aggregate spec on a column. */
|
|
67
|
+
export type AggregateSpec<Row = any> =
|
|
68
|
+
| "sum" | "avg" | "min" | "max" | "count"
|
|
69
|
+
| ((rows: readonly Row[], col: ColumnState<Row>) => unknown);
|
|
70
|
+
|
|
45
71
|
/** Live reactive state for a column, exposed on `table.columns[i]`. */
|
|
46
72
|
export interface ColumnState<Row = any> {
|
|
47
73
|
readonly key: string;
|
|
@@ -53,6 +79,11 @@ export interface ColumnState<Row = any> {
|
|
|
53
79
|
readonly pinnable: boolean;
|
|
54
80
|
readonly hideable: boolean;
|
|
55
81
|
readonly reorderable: boolean;
|
|
82
|
+
/** M2 */
|
|
83
|
+
readonly editable: boolean;
|
|
84
|
+
readonly filterable: boolean;
|
|
85
|
+
readonly filter: ((value: unknown, query: string, row: Row) => boolean) | null;
|
|
86
|
+
readonly filterPlaceholder: string | null;
|
|
56
87
|
readonly minWidth: number;
|
|
57
88
|
readonly maxWidth: number;
|
|
58
89
|
readonly width: Signal<number>;
|
|
@@ -68,6 +99,34 @@ export interface FocusedCell {
|
|
|
68
99
|
columnKey: string;
|
|
69
100
|
}
|
|
70
101
|
|
|
102
|
+
/** M2: cell currently being edited. Keyed on row identity so the edit
|
|
103
|
+
* state survives slot recycling / scroll / sort / filter -- same model
|
|
104
|
+
* as `FocusedCell`. */
|
|
105
|
+
export interface EditingCell {
|
|
106
|
+
rowId: string | number;
|
|
107
|
+
columnKey: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** M2: payload passed to `onCellEdit` on commit.
|
|
111
|
+
*
|
|
112
|
+
* `newValue` is `string` when the commit came from the DOM editing path
|
|
113
|
+
* (double-click / F2 → contenteditable → Enter / Tab / blur). The
|
|
114
|
+
* consumer is responsible for coercion when editing typed columns. When
|
|
115
|
+
* the consumer calls `commitEdit(explicitValue)` with a non-string,
|
|
116
|
+
* `newValue` carries that value through unchanged (typed as unknown).
|
|
117
|
+
* `oldValue` is whatever the cell holds pre-edit (post-accessor).
|
|
118
|
+
*
|
|
119
|
+
* The unchanged-guard compares `String(oldValue) !== newValue` for string
|
|
120
|
+
* `newValue`s -- so a no-op Enter on a numeric column doesn't fire this
|
|
121
|
+
* handler. For non-string explicit values it falls back to strict
|
|
122
|
+
* equality. */
|
|
123
|
+
export interface CellEditPayload<Row = any> {
|
|
124
|
+
row: Row;
|
|
125
|
+
columnKey: string;
|
|
126
|
+
oldValue: unknown;
|
|
127
|
+
newValue: string | unknown;
|
|
128
|
+
}
|
|
129
|
+
|
|
71
130
|
export interface SortEntry {
|
|
72
131
|
key: string;
|
|
73
132
|
dir: SortDir;
|
|
@@ -102,6 +161,117 @@ export interface CreateTableConfig<Row = any> {
|
|
|
102
161
|
overscan?: number;
|
|
103
162
|
initialFocus?: FocusedCell | null;
|
|
104
163
|
initialSort?: readonly SortEntry[];
|
|
164
|
+
/** M2: commit hook for cell edits. Receives the row + column key + old +
|
|
165
|
+
* new value. The table does NOT mutate rows itself; consumer is
|
|
166
|
+
* responsible for the data write (in-memory mutation, backend POST,
|
|
167
|
+
* optimistic update, etc.). Skipped when newValue === oldValue.
|
|
168
|
+
* Errors thrown by the handler are caught + logged. */
|
|
169
|
+
onCellEdit?: (payload: CellEditPayload<Row>) => void | Promise<unknown>;
|
|
170
|
+
|
|
171
|
+
/** M3: group rows by one or more column keys. `null` (default) or an
|
|
172
|
+
* empty array = no grouping. A bare string is normalized to `[string]`.
|
|
173
|
+
* Unknown column keys are silently dropped so persisted state that
|
|
174
|
+
* outlives a column config survives without crashes. */
|
|
175
|
+
groupBy?: string | readonly string[] | null;
|
|
176
|
+
/** M3: pre-seed the collapsed set. Each entry is a group path -- e.g.
|
|
177
|
+
* `[["Europe"], ["Asia", "Books"]]`. Unknown paths become no-ops when
|
|
178
|
+
* their groups don't exist. */
|
|
179
|
+
initialCollapsedGroups?: readonly (readonly string[])[];
|
|
180
|
+
/** M3: append a grand-total entry at the tail of `visibleEntries` when
|
|
181
|
+
* any column has an `aggregate` spec. Grand-total aggregates fold
|
|
182
|
+
* over `filteredRows()` and are stable across collapse operations. */
|
|
183
|
+
showGrandTotal?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// --- M3: grouping + aggregation types ---------------------------------------
|
|
187
|
+
|
|
188
|
+
/** One node in the grouped-rows tree. Leaf nodes have `rows` populated and
|
|
189
|
+
* `subGroups: null`; internal nodes have `subGroups` populated and
|
|
190
|
+
* `rows: null`. Aggregates always fold over leaf rows recursively. */
|
|
191
|
+
export interface GroupNode<Row = any> {
|
|
192
|
+
readonly depth: number;
|
|
193
|
+
/** The column key this depth groups on. */
|
|
194
|
+
readonly key: string;
|
|
195
|
+
/** The group's identifying value (post-`accessor`). `null` for the
|
|
196
|
+
* null / undefined bucket. */
|
|
197
|
+
readonly value: unknown;
|
|
198
|
+
/** Path of ancestor values including this node. e.g. `["Europe", "Books"]`. */
|
|
199
|
+
readonly path: readonly string[];
|
|
200
|
+
/** Stringified path (U+001F separator) -- the storage key for the
|
|
201
|
+
* collapsed-groups Set. */
|
|
202
|
+
readonly pathStr: string;
|
|
203
|
+
/** Recursive leaf-row count. */
|
|
204
|
+
readonly count: number;
|
|
205
|
+
readonly aggregates: ReadonlyMap<string, unknown>;
|
|
206
|
+
readonly subGroups: readonly GroupNode<Row>[] | null;
|
|
207
|
+
readonly rows: readonly Row[] | null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** A row in `visibleEntries`. The mount layer dispatches per-slot rendering
|
|
211
|
+
* on `type`. */
|
|
212
|
+
export type Entry<Row = any> = DataEntry<Row> | GroupHeaderEntry | GrandTotalEntry;
|
|
213
|
+
|
|
214
|
+
export interface DataEntry<Row = any> {
|
|
215
|
+
readonly type: "data";
|
|
216
|
+
readonly row: Row;
|
|
217
|
+
}
|
|
218
|
+
export interface GroupHeaderEntry {
|
|
219
|
+
readonly type: "group-header";
|
|
220
|
+
readonly depth: number;
|
|
221
|
+
readonly key: string;
|
|
222
|
+
readonly value: unknown;
|
|
223
|
+
readonly path: readonly string[];
|
|
224
|
+
readonly pathStr: string;
|
|
225
|
+
readonly count: number;
|
|
226
|
+
readonly aggregates: ReadonlyMap<string, unknown>;
|
|
227
|
+
readonly isCollapsed: boolean;
|
|
228
|
+
}
|
|
229
|
+
export interface GrandTotalEntry {
|
|
230
|
+
readonly type: "grand-total";
|
|
231
|
+
readonly aggregates: ReadonlyMap<string, unknown>;
|
|
232
|
+
readonly count: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Export option types ----------------------------------------------------
|
|
236
|
+
|
|
237
|
+
/** Row source for export.
|
|
238
|
+
*
|
|
239
|
+
* - `"visible"` (default): the current post-sort view that the user sees.
|
|
240
|
+
* - `"all"`: the original master `rows` array, ignoring sort.
|
|
241
|
+
* - `"selected"`: the current selection materialized against the master
|
|
242
|
+
* array (so the export order matches insertion order, not the sorted
|
|
243
|
+
* order the user is looking at). Pass an explicit array if you want
|
|
244
|
+
* a different source.
|
|
245
|
+
* - Array: an explicit row array (e.g. a paginated page snapshot).
|
|
246
|
+
*/
|
|
247
|
+
export type ExportRowsSource<Row = any> = "visible" | "all" | "selected" | readonly Row[];
|
|
248
|
+
|
|
249
|
+
/** Column projection for export.
|
|
250
|
+
*
|
|
251
|
+
* - `"visible"` (default): column order matches the current visible order
|
|
252
|
+
* and skips hidden columns.
|
|
253
|
+
* - `"all"`: all declared columns in declaration order, including hidden.
|
|
254
|
+
* - Array: explicit list of column keys (projection + ordering).
|
|
255
|
+
*/
|
|
256
|
+
export type ExportColumnsSelector = "visible" | "all" | readonly string[];
|
|
257
|
+
|
|
258
|
+
export interface ExportCsvOptions<Row = any> {
|
|
259
|
+
rows?: ExportRowsSource<Row>;
|
|
260
|
+
columns?: ExportColumnsSelector;
|
|
261
|
+
delimiter?: string;
|
|
262
|
+
quote?: string;
|
|
263
|
+
headers?: boolean;
|
|
264
|
+
newline?: string;
|
|
265
|
+
bom?: boolean;
|
|
266
|
+
formatter?: (row: Row, col: ColumnState<Row>) => unknown;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface ExportJsonOptions<Row = any> {
|
|
270
|
+
rows?: ExportRowsSource<Row>;
|
|
271
|
+
columns?: ExportColumnsSelector;
|
|
272
|
+
indent?: number;
|
|
273
|
+
format?: "string" | "array";
|
|
274
|
+
formatter?: (row: Row, col: ColumnState<Row>) => unknown;
|
|
105
275
|
}
|
|
106
276
|
|
|
107
277
|
// --- Headless core ----------------------------------------------------------
|
|
@@ -116,9 +286,35 @@ export interface TableCore<Row = any> {
|
|
|
116
286
|
|
|
117
287
|
// ---- Reactive: data ----
|
|
118
288
|
rowsGetter(): readonly Row[];
|
|
289
|
+
/** M2: filtered rows BEFORE sort. `visibleRows` reads from this; consumers
|
|
290
|
+
* can read it directly when they want filter-aware row materializers
|
|
291
|
+
* (e.g. count of pre-sort matches, export of filtered-but-not-sorted). */
|
|
292
|
+
readonly filteredRows: Computed<readonly Row[]>;
|
|
119
293
|
readonly visibleRows: Computed<readonly Row[]>;
|
|
120
294
|
readonly rowCount: Computed<number>;
|
|
121
295
|
|
|
296
|
+
// ---- Reactive: grouping + aggregation (M3) ----
|
|
297
|
+
/** Current grouping keys. Empty array = no grouping (fast path).
|
|
298
|
+
* Assign via `setGroupBy`; the raw signal is exposed for read-only
|
|
299
|
+
* access + persistence with `@zakkster/lite-persist`. */
|
|
300
|
+
readonly groupBy: Signal<readonly string[]>;
|
|
301
|
+
/** Set of collapsed group path-strings (U+001F separator). Mutated via
|
|
302
|
+
* `toggleGroup`, `collapseGroup`, `expandGroup`, `collapseAllGroups`,
|
|
303
|
+
* `expandAllGroups`. `isGroupCollapsed(path)` is the O(1) predicate. */
|
|
304
|
+
readonly collapsedGroups: Signal<ReadonlySet<string>>;
|
|
305
|
+
/** Grouped tree of `filteredRows`, sorted within leaves per `sortChain`.
|
|
306
|
+
* `null` when `groupBy` is empty -- consumers can branch on this to
|
|
307
|
+
* render a flat list without walking the tree. */
|
|
308
|
+
readonly groupedRows: Computed<readonly GroupNode<Row>[] | null>;
|
|
309
|
+
/** Flat interleaved list of `{type: "data" | "group-header" | "grand-total"}`
|
|
310
|
+
* entries. Drives the mount layer's slot rendering. Length matches
|
|
311
|
+
* `entryCount()`. */
|
|
312
|
+
readonly visibleEntries: Computed<readonly Entry<Row>[]>;
|
|
313
|
+
/** Total entry count including group headers + grand total. Drives the
|
|
314
|
+
* virtual axis in the mount layer -- `rowCount` (data-only) is kept
|
|
315
|
+
* as a separate consumer stat. */
|
|
316
|
+
readonly entryCount: Computed<number>;
|
|
317
|
+
|
|
122
318
|
// ---- Reactive: columns ----
|
|
123
319
|
readonly columnOrder: Signal<readonly string[]>;
|
|
124
320
|
readonly visibleColumns: Computed<readonly ColumnState<Row>[]>;
|
|
@@ -133,8 +329,12 @@ export interface TableCore<Row = any> {
|
|
|
133
329
|
readonly leftOffsets: Computed<Map<string, number>>;
|
|
134
330
|
readonly rightOffsets: Computed<Map<string, number>>;
|
|
135
331
|
|
|
136
|
-
// ---- Reactive: sort / focus / selection ----
|
|
332
|
+
// ---- Reactive: sort / filter / focus / selection / editing ----
|
|
137
333
|
readonly sortChain: Signal<readonly SortEntry[]>;
|
|
334
|
+
/** M2: per-column filter state. Map<columnKey, queryString>. Mutated
|
|
335
|
+
* via `setColumnFilter` -- direct .set on the signal works but always
|
|
336
|
+
* pass a fresh Map (Object.is-equal mutations would not notify). */
|
|
337
|
+
readonly columnFilters: Signal<ReadonlyMap<string, string>>;
|
|
138
338
|
readonly focusedCell: Signal<FocusedCell | null>;
|
|
139
339
|
/** Predicate-based selection state. Reading `.set.has(id)` directly is
|
|
140
340
|
* wrong in all-mode -- use `isSelected(id)` for membership. */
|
|
@@ -143,6 +343,14 @@ export interface TableCore<Row = any> {
|
|
|
143
343
|
/** Reactive O(1) selected count. In all-mode this is
|
|
144
344
|
* `rowCount() - blacklist.size`; otherwise `whitelist.size`. */
|
|
145
345
|
readonly selectedCount: Computed<number>;
|
|
346
|
+
/** M2: current cell being edited, or null. Keyed on row identity so it
|
|
347
|
+
* survives scroll / sort / filter / slot recycling. */
|
|
348
|
+
readonly editingCell: Signal<EditingCell | null>;
|
|
349
|
+
/** M2: in-progress edit value (the contenteditable's textContent).
|
|
350
|
+
* mountTable writes to this from `input` events; consumers can also
|
|
351
|
+
* set it programmatically to drive an external input. `commitEdit`
|
|
352
|
+
* without an explicit value reads this. */
|
|
353
|
+
readonly editingDraft: Signal<string>;
|
|
146
354
|
|
|
147
355
|
// ---- Methods: sort ----
|
|
148
356
|
setSort(key: string, dir: SortDir | null): void;
|
|
@@ -178,6 +386,55 @@ export interface TableCore<Row = any> {
|
|
|
178
386
|
setColumnOrder(keys: readonly string[]): void;
|
|
179
387
|
moveColumn(fromKey: string, toKey: string, opts?: { before?: boolean }): void;
|
|
180
388
|
|
|
389
|
+
// ---- Methods: filters (M2) ----
|
|
390
|
+
/** Set or clear a column's filter. Pass `null`/`undefined`/`""` to clear.
|
|
391
|
+
* No-op on non-filterable or unknown columns. */
|
|
392
|
+
setColumnFilter(key: string, value: string | null | undefined): void;
|
|
393
|
+
/** Clear all filters. No-op + no notify if the filter map is already empty. */
|
|
394
|
+
clearColumnFilters(): void;
|
|
395
|
+
|
|
396
|
+
// ---- Methods: grouping (M3) ----
|
|
397
|
+
/** Set the grouping keys. Accepts a bare string, a string[], or
|
|
398
|
+
* null/empty (no grouping). Unknown column keys are silently dropped.
|
|
399
|
+
* Idempotent -- setting the same effective value is a no-op that
|
|
400
|
+
* doesn't churn the signal. */
|
|
401
|
+
setGroupBy(v: string | readonly string[] | null): void;
|
|
402
|
+
/** Flip the collapsed state for the given group path. */
|
|
403
|
+
toggleGroup(path: readonly string[]): void;
|
|
404
|
+
/** Expand a specific group (no-op if already expanded). */
|
|
405
|
+
expandGroup(path: readonly string[]): void;
|
|
406
|
+
/** Collapse a specific group (no-op if already collapsed). */
|
|
407
|
+
collapseGroup(path: readonly string[]): void;
|
|
408
|
+
/** Expand every group -- clears the collapsed set. */
|
|
409
|
+
expandAllGroups(): void;
|
|
410
|
+
/** Collapse every group in the current tree. Walks `groupedRows()` once. */
|
|
411
|
+
collapseAllGroups(): void;
|
|
412
|
+
/** O(1) predicate: is this exact group path currently collapsed? */
|
|
413
|
+
isGroupCollapsed(path: readonly string[]): boolean;
|
|
414
|
+
/** Given an entry index (typically `axis.start()`), return the group
|
|
415
|
+
* headers that CONTAIN it -- one per depth level, deepest last.
|
|
416
|
+
* Used by consumers implementing sticky group headers. Returns [] for
|
|
417
|
+
* the ungrouped fast path or when the target is above the first header. */
|
|
418
|
+
groupAncestryAt(entryIndex: number): readonly GroupHeaderEntry[];
|
|
419
|
+
|
|
420
|
+
// ---- Methods: editing (M2) ----
|
|
421
|
+
/** Start editing the (rowId, columnKey) cell. No-op on non-editable
|
|
422
|
+
* columns. Auto-commits any in-flight edit of a different cell. Idempotent
|
|
423
|
+
* on the same cell (does not re-seed the draft). */
|
|
424
|
+
startEdit(rowId: string | number, columnKey: string): void;
|
|
425
|
+
/** Commit the in-flight edit. With no argument, reads from `editingDraft`.
|
|
426
|
+
* Fires `onCellEdit` only when `newValue !== oldValue`. Clears edit state. */
|
|
427
|
+
commitEdit(value?: string): void;
|
|
428
|
+
/** Discard the in-flight edit without firing `onCellEdit`. */
|
|
429
|
+
cancelEdit(): void;
|
|
430
|
+
/** O(1) predicate. */
|
|
431
|
+
isEditing(rowId: string | number, columnKey: string): boolean;
|
|
432
|
+
|
|
433
|
+
// ---- Methods: export ----
|
|
434
|
+
exportCsv(opts?: ExportCsvOptions<Row>): string;
|
|
435
|
+
exportJson(opts?: ExportJsonOptions<Row> & { format?: "string" }): string;
|
|
436
|
+
exportJson<R = Row>(opts: ExportJsonOptions<Row> & { format: "array" }): object[];
|
|
437
|
+
|
|
181
438
|
// ---- Methods: focus ----
|
|
182
439
|
moveFocus(direction: FocusDirection, opts?: { pageSize?: number }): void;
|
|
183
440
|
|