@zakkster/lite-table 1.0.0 → 1.1.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/Table.d.ts CHANGED
@@ -36,6 +36,17 @@ 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;
39
50
  /** Computed value override; if absent, `row[key]` is used. */
40
51
  accessor?: (row: Row) => Value;
41
52
  /** Sort comparator; default is null-safe number-or-string compare. */
@@ -53,6 +64,11 @@ export interface ColumnState<Row = any> {
53
64
  readonly pinnable: boolean;
54
65
  readonly hideable: boolean;
55
66
  readonly reorderable: boolean;
67
+ /** M2 */
68
+ readonly editable: boolean;
69
+ readonly filterable: boolean;
70
+ readonly filter: ((value: unknown, query: string, row: Row) => boolean) | null;
71
+ readonly filterPlaceholder: string | null;
56
72
  readonly minWidth: number;
57
73
  readonly maxWidth: number;
58
74
  readonly width: Signal<number>;
@@ -68,6 +84,34 @@ export interface FocusedCell {
68
84
  columnKey: string;
69
85
  }
70
86
 
87
+ /** M2: cell currently being edited. Keyed on row identity so the edit
88
+ * state survives slot recycling / scroll / sort / filter -- same model
89
+ * as `FocusedCell`. */
90
+ export interface EditingCell {
91
+ rowId: string | number;
92
+ columnKey: string;
93
+ }
94
+
95
+ /** M2: payload passed to `onCellEdit` on commit.
96
+ *
97
+ * `newValue` is `string` when the commit came from the DOM editing path
98
+ * (double-click / F2 → contenteditable → Enter / Tab / blur). The
99
+ * consumer is responsible for coercion when editing typed columns. When
100
+ * the consumer calls `commitEdit(explicitValue)` with a non-string,
101
+ * `newValue` carries that value through unchanged (typed as unknown).
102
+ * `oldValue` is whatever the cell holds pre-edit (post-accessor).
103
+ *
104
+ * The unchanged-guard compares `String(oldValue) !== newValue` for string
105
+ * `newValue`s -- so a no-op Enter on a numeric column doesn't fire this
106
+ * handler. For non-string explicit values it falls back to strict
107
+ * equality. */
108
+ export interface CellEditPayload<Row = any> {
109
+ row: Row;
110
+ columnKey: string;
111
+ oldValue: unknown;
112
+ newValue: string | unknown;
113
+ }
114
+
71
115
  export interface SortEntry {
72
116
  key: string;
73
117
  dir: SortDir;
@@ -102,6 +146,54 @@ export interface CreateTableConfig<Row = any> {
102
146
  overscan?: number;
103
147
  initialFocus?: FocusedCell | null;
104
148
  initialSort?: readonly SortEntry[];
149
+ /** M2: commit hook for cell edits. Receives the row + column key + old +
150
+ * new value. The table does NOT mutate rows itself; consumer is
151
+ * responsible for the data write (in-memory mutation, backend POST,
152
+ * optimistic update, etc.). Skipped when newValue === oldValue.
153
+ * Errors thrown by the handler are caught + logged. */
154
+ onCellEdit?: (payload: CellEditPayload<Row>) => void | Promise<unknown>;
155
+ }
156
+
157
+ // --- Export option types ----------------------------------------------------
158
+
159
+ /** Row source for export.
160
+ *
161
+ * - `"visible"` (default): the current post-sort view that the user sees.
162
+ * - `"all"`: the original master `rows` array, ignoring sort.
163
+ * - `"selected"`: the current selection materialized against the master
164
+ * array (so the export order matches insertion order, not the sorted
165
+ * order the user is looking at). Pass an explicit array if you want
166
+ * a different source.
167
+ * - Array: an explicit row array (e.g. a paginated page snapshot).
168
+ */
169
+ export type ExportRowsSource<Row = any> = "visible" | "all" | "selected" | readonly Row[];
170
+
171
+ /** Column projection for export.
172
+ *
173
+ * - `"visible"` (default): column order matches the current visible order
174
+ * and skips hidden columns.
175
+ * - `"all"`: all declared columns in declaration order, including hidden.
176
+ * - Array: explicit list of column keys (projection + ordering).
177
+ */
178
+ export type ExportColumnsSelector = "visible" | "all" | readonly string[];
179
+
180
+ export interface ExportCsvOptions<Row = any> {
181
+ rows?: ExportRowsSource<Row>;
182
+ columns?: ExportColumnsSelector;
183
+ delimiter?: string;
184
+ quote?: string;
185
+ headers?: boolean;
186
+ newline?: string;
187
+ bom?: boolean;
188
+ formatter?: (row: Row, col: ColumnState<Row>) => unknown;
189
+ }
190
+
191
+ export interface ExportJsonOptions<Row = any> {
192
+ rows?: ExportRowsSource<Row>;
193
+ columns?: ExportColumnsSelector;
194
+ indent?: number;
195
+ format?: "string" | "array";
196
+ formatter?: (row: Row, col: ColumnState<Row>) => unknown;
105
197
  }
106
198
 
107
199
  // --- Headless core ----------------------------------------------------------
@@ -116,6 +208,10 @@ export interface TableCore<Row = any> {
116
208
 
117
209
  // ---- Reactive: data ----
118
210
  rowsGetter(): readonly Row[];
211
+ /** M2: filtered rows BEFORE sort. `visibleRows` reads from this; consumers
212
+ * can read it directly when they want filter-aware row materializers
213
+ * (e.g. count of pre-sort matches, export of filtered-but-not-sorted). */
214
+ readonly filteredRows: Computed<readonly Row[]>;
119
215
  readonly visibleRows: Computed<readonly Row[]>;
120
216
  readonly rowCount: Computed<number>;
121
217
 
@@ -133,8 +229,12 @@ export interface TableCore<Row = any> {
133
229
  readonly leftOffsets: Computed<Map<string, number>>;
134
230
  readonly rightOffsets: Computed<Map<string, number>>;
135
231
 
136
- // ---- Reactive: sort / focus / selection ----
232
+ // ---- Reactive: sort / filter / focus / selection / editing ----
137
233
  readonly sortChain: Signal<readonly SortEntry[]>;
234
+ /** M2: per-column filter state. Map<columnKey, queryString>. Mutated
235
+ * via `setColumnFilter` -- direct .set on the signal works but always
236
+ * pass a fresh Map (Object.is-equal mutations would not notify). */
237
+ readonly columnFilters: Signal<ReadonlyMap<string, string>>;
138
238
  readonly focusedCell: Signal<FocusedCell | null>;
139
239
  /** Predicate-based selection state. Reading `.set.has(id)` directly is
140
240
  * wrong in all-mode -- use `isSelected(id)` for membership. */
@@ -143,6 +243,14 @@ export interface TableCore<Row = any> {
143
243
  /** Reactive O(1) selected count. In all-mode this is
144
244
  * `rowCount() - blacklist.size`; otherwise `whitelist.size`. */
145
245
  readonly selectedCount: Computed<number>;
246
+ /** M2: current cell being edited, or null. Keyed on row identity so it
247
+ * survives scroll / sort / filter / slot recycling. */
248
+ readonly editingCell: Signal<EditingCell | null>;
249
+ /** M2: in-progress edit value (the contenteditable's textContent).
250
+ * mountTable writes to this from `input` events; consumers can also
251
+ * set it programmatically to drive an external input. `commitEdit`
252
+ * without an explicit value reads this. */
253
+ readonly editingDraft: Signal<string>;
146
254
 
147
255
  // ---- Methods: sort ----
148
256
  setSort(key: string, dir: SortDir | null): void;
@@ -178,6 +286,31 @@ export interface TableCore<Row = any> {
178
286
  setColumnOrder(keys: readonly string[]): void;
179
287
  moveColumn(fromKey: string, toKey: string, opts?: { before?: boolean }): void;
180
288
 
289
+ // ---- Methods: filters (M2) ----
290
+ /** Set or clear a column's filter. Pass `null`/`undefined`/`""` to clear.
291
+ * No-op on non-filterable or unknown columns. */
292
+ setColumnFilter(key: string, value: string | null | undefined): void;
293
+ /** Clear all filters. No-op + no notify if the filter map is already empty. */
294
+ clearColumnFilters(): void;
295
+
296
+ // ---- Methods: editing (M2) ----
297
+ /** Start editing the (rowId, columnKey) cell. No-op on non-editable
298
+ * columns. Auto-commits any in-flight edit of a different cell. Idempotent
299
+ * on the same cell (does not re-seed the draft). */
300
+ startEdit(rowId: string | number, columnKey: string): void;
301
+ /** Commit the in-flight edit. With no argument, reads from `editingDraft`.
302
+ * Fires `onCellEdit` only when `newValue !== oldValue`. Clears edit state. */
303
+ commitEdit(value?: string): void;
304
+ /** Discard the in-flight edit without firing `onCellEdit`. */
305
+ cancelEdit(): void;
306
+ /** O(1) predicate. */
307
+ isEditing(rowId: string | number, columnKey: string): boolean;
308
+
309
+ // ---- Methods: export ----
310
+ exportCsv(opts?: ExportCsvOptions<Row>): string;
311
+ exportJson(opts?: ExportJsonOptions<Row> & { format?: "string" }): string;
312
+ exportJson<R = Row>(opts: ExportJsonOptions<Row> & { format: "array" }): object[];
313
+
181
314
  // ---- Methods: focus ----
182
315
  moveFocus(direction: FocusDirection, opts?: { pageSize?: number }): void;
183
316