@gridkitjs/core 0.5.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -97,6 +97,8 @@ interface ColumnDefinition<Row, Node = string> {
97
97
  * grid-level default. A column that cannot move can still be moved past.
98
98
  */
99
99
  reorderable?: boolean;
100
+ /** Whether this column can be sorted, overriding the grid-level default. */
101
+ sortable?: boolean;
100
102
  /**
101
103
  * Lets this column's header and/or cell text wrap onto multiple lines
102
104
  * instead of the grid's default single line with an ellipsis. Off by
@@ -219,6 +221,82 @@ interface ColumnOrderEvent {
219
221
  readonly columnId: string;
220
222
  readonly order: ColumnOrderState;
221
223
  }
224
+ /** Which way a column sorts. */
225
+ type SortDirection = "asc" | "desc";
226
+ /** One column's place in a stacked sort, and which way it sorts. */
227
+ interface ColumnSortEntry {
228
+ readonly columnId: string;
229
+ readonly direction: SortDirection;
230
+ }
231
+ /**
232
+ * The active sort, in priority order: index 0 sorts first, ties broken by
233
+ * index 1, and so on down the stack. Empty for an unsorted grid, mirroring
234
+ * `SelectionState`'s empty array for "nothing selected".
235
+ */
236
+ type ColumnSortState = readonly ColumnSortEntry[];
237
+ /**
238
+ * Reports a user sort change whole — the stack to persist from. No `phase`:
239
+ * unlike a resize, a sort has no in-progress state, only before and after.
240
+ */
241
+ interface ColumnSortEvent {
242
+ readonly columnId: string;
243
+ readonly sort: ColumnSortState;
244
+ }
245
+ interface FilterEntryBase<Row> {
246
+ readonly columnId?: FieldPath<Row> | (string & {});
247
+ }
248
+ /** Matches a column's stringified value against an SQL LIKE-style query. */
249
+ interface TextFilterEntry<Row> extends FilterEntryBase<Row> {
250
+ /**
251
+ * A bare query is an exact match; `%text%` is contains; `text%` is
252
+ * starts-with; `%text` is ends-with. Always case-insensitive. Only the
253
+ * first and last characters are read as anchors — a `%` anywhere else is
254
+ * a literal character, not a wildcard.
255
+ */
256
+ readonly query: string;
257
+ }
258
+ /**
259
+ * Matches a column's actual (non-stringified) value by strict equality —
260
+ * only when the column's resolved `type` agrees: `number`/`decimal`/
261
+ * `currency`/`percent` for a `number` value, `boolean` for a `boolean`
262
+ * value, `date`/`dateTime`/`time` for a `Date` value (compared via
263
+ * `getTime()`). A `value` against a column of the wrong type never matches,
264
+ * rather than silently stringifying and falling through to a text
265
+ * comparison — the whole point of this variant existing separately from
266
+ * `TextFilterEntry` is that it doesn't do that.
267
+ */
268
+ interface ValueFilterEntry<Row> extends FilterEntryBase<Row> {
269
+ readonly value: number | boolean | Date;
270
+ }
271
+ type FilterPredicate<Row> = (value: unknown, row: Row) => boolean;
272
+ /**
273
+ * Matches via caller-supplied logic — a numeric range, a cross-field
274
+ * combination, anything `query`/`value` can't express. Called once per row,
275
+ * not per column: when `columnId` is given, `value` is that column's value,
276
+ * resolved for convenience; when it's omitted, `value` is `undefined` and
277
+ * the predicate is expected to read whatever it needs off `row` itself.
278
+ */
279
+ interface PredicateFilterEntry<Row> extends FilterEntryBase<Row> {
280
+ readonly predicate: FilterPredicate<Row>;
281
+ }
282
+ /**
283
+ * A nested group of entries, combined with `combinator` instead of the
284
+ * implicit AND every top-level `FilterState` uses. `entries` is itself a
285
+ * `FilterState<Row>`, so groups nest to any depth — the only entry variant
286
+ * with no `columnId` of its own, since it isn't scoped to one column, only
287
+ * to the entries it composes.
288
+ */
289
+ interface GroupFilterEntry<Row> {
290
+ readonly combinator: "and" | "or";
291
+ readonly entries: FilterState<Row>;
292
+ }
293
+ type FilterEntry<Row> = TextFilterEntry<Row> | ValueFilterEntry<Row> | PredicateFilterEntry<Row> | GroupFilterEntry<Row>;
294
+ /**
295
+ * The active filter, every entry ANDed together at the top level — nest a
296
+ * `GroupFilterEntry` for anything that needs OR. Empty for an unfiltered
297
+ * grid, mirroring `ColumnSortState`'s empty array for "no sort".
298
+ */
299
+ type FilterState<Row> = readonly FilterEntry<Row>[];
222
300
  /**
223
301
  * Ids selected, in the order they were selected — so the most recent is last,
224
302
  * and a consumer wanting only that reads `at(-1)` rather than tracking it
@@ -252,6 +330,11 @@ interface SelectableConfig {
252
330
  columns?: SelectionMode | undefined;
253
331
  cells?: CellSelectionMode | undefined;
254
332
  }
333
+ /**
334
+ * What an interaction means for the selection, read off its modifiers once at
335
+ * the boundary so that nothing downstream handles a raw event.
336
+ */
337
+ type SelectIntent = "replace" | "toggle" | "range";
255
338
  /** The address of one cell: which row, and which column within it. */
256
339
  interface SelectedCellRef {
257
340
  readonly rowId: string;
@@ -399,6 +482,18 @@ declare function resolveRowId<Row>(row: Row, index: number, getRowId?: (row: Row
399
482
  * reading as `"Application Id"`.
400
483
  */
401
484
  declare function resolveColumnLabel<Row, Node>(column: ColumnDefinition<Row, Node>): Node | string;
485
+ /** Which of a column header's keyboard interactions to announce. */
486
+ interface KeyShortcutCapabilities {
487
+ reorderable: boolean;
488
+ resizable: boolean;
489
+ sortable: boolean;
490
+ }
491
+ /**
492
+ * The `aria-keyshortcuts` string for a column header, naming only the keys
493
+ * that column actually responds to — so the keys are discoverable, since
494
+ * none of them is a convention a user would try unprompted.
495
+ */
496
+ declare function buildKeyShortcuts(capabilities: KeyShortcutCapabilities): string;
402
497
  /**
403
498
  * Accesses a value at a dotted path, e.g. `foo.bar.baz`.
404
499
  *
@@ -408,6 +503,40 @@ declare function resolveColumnLabel<Row, Node>(column: ColumnDefinition<Row, Nod
408
503
  */
409
504
  declare function accessDotted(obj: unknown, path: string): unknown;
410
505
 
506
+ /**
507
+ * The header's row index. One coordinate space covers the header and the body
508
+ * so that arrowing up out of the first row reaches the header without a case
509
+ * of its own, and every other move is plain arithmetic.
510
+ */
511
+ declare const HEADER_ROW = -1;
512
+ /** The cell the grid's single tab stop sits on. */
513
+ interface GridFocus {
514
+ readonly rowIndex: number;
515
+ readonly columnIndex: number;
516
+ }
517
+ /** The modifier keys held with a navigation key press. */
518
+ interface NavigationModifiers {
519
+ readonly ctrlKey: boolean;
520
+ readonly metaKey: boolean;
521
+ readonly altKey: boolean;
522
+ readonly shiftKey: boolean;
523
+ }
524
+ /**
525
+ * Holds a coordinate inside the grid, so that a focus surviving a column
526
+ * removal or a shorter page never leaves the grid with no tab stop at all —
527
+ * which is a grid a keyboard cannot reach.
528
+ */
529
+ declare function clampFocus(focus: GridFocus, rowCount: number, columnCount: number): GridFocus;
530
+ /**
531
+ * The focus a navigation key press moves to, or `null` when the key isn't
532
+ * this grid's to handle — left for the browser or another handler.
533
+ *
534
+ * `pageSize` is a pre-measured row count rather than a callback: this
535
+ * function takes plain data only, so a caller measures the viewport once and
536
+ * hands over the number.
537
+ */
538
+ declare function nextFocusForKey(key: string, modifiers: NavigationModifiers, focus: GridFocus, rowCount: number, columnCount: number, pageSize: number): GridFocus | null;
539
+
411
540
  /**
412
541
  * Puts `columns` in the order `order` gives. Ids it lists come first, in that
413
542
  * order; columns it omits keep their position among the definitions and
@@ -441,6 +570,16 @@ declare function movesColumn(orderedIds: ColumnOrderState, movedId: string, befo
441
570
  * keeps a drop target a single nullable string.
442
571
  */
443
572
  declare function resolveDropBefore(orderedIds: ColumnOrderState, targetId: string, side: DropSide): string | null;
573
+ /**
574
+ * The `beforeId` a keyboard nudge on `columnId` produces, for `onDrop` to
575
+ * apply the same way a pointer drop does — or `undefined` when the nudge is a
576
+ * no-op: `columnId` is not in `orderedIds`, or the nudge would move it left
577
+ * of the start.
578
+ *
579
+ * `null` is a real answer here, not "no-op": it means append to the end, the
580
+ * same convention `moveColumnBefore` uses.
581
+ */
582
+ declare function resolveKeyboardDropTarget(orderedIds: ColumnOrderState, columnId: string, direction: -1 | 1): string | null | undefined;
444
583
 
445
584
  /**
446
585
  * The selection with `id` added if absent and removed if present — a
@@ -496,6 +635,18 @@ declare function selectCell(selection: CellSelectionState, cell: SelectedCellRef
496
635
  * Ctrl-click, matching `toggleSelection`.
497
636
  */
498
637
  declare function toggleCellSelection(selection: CellSelectionState, cell: SelectedCellRef, mode: CellSelectionMode): CellSelectionState;
638
+ /** The intent a click or key press carries, by the modifiers held with it. */
639
+ declare function intentOf(event: {
640
+ ctrlKey: boolean;
641
+ metaKey: boolean;
642
+ shiftKey: boolean;
643
+ }): SelectIntent;
644
+ /**
645
+ * The transform an intent asks for. A function rather than a branch inside
646
+ * each caller, so a row and a column cannot come to mean different things by
647
+ * the same click.
648
+ */
649
+ declare function applySelectionIntent(selection: SelectionState, orderedIds: readonly string[], anchorId: string | null, id: string, intent: SelectIntent, mode: SelectionMode): SelectionState;
499
650
 
500
651
  /**
501
652
  * Applied to any column that does not size itself. The minimum is a width a
@@ -556,5 +707,149 @@ declare function fitColumnsToWidth<Row, Node>(resolved: readonly ResolvedColumn<
556
707
  * exactly its content can still round down into an ellipsis.
557
708
  */
558
709
  declare function sizeColumnToContent(measuredWidth: number, constraints: ColumnConstraints, padding?: number): number;
710
+ /**
711
+ * `current` with `columnId` reverted to its width in `base` — or omitted
712
+ * entirely if `base` had none — for Escape to restore a resize in progress.
713
+ *
714
+ * A column with no stored width in `base` is correctly omitted by deleting
715
+ * the key rather than merging its `base` width back in, which would
716
+ * otherwise leave it pinned and hidden from auto-fit.
717
+ */
718
+ declare function revertColumnSize(current: ColumnSizingState, base: ColumnSizingState, columnId: string): ColumnSizingState;
719
+
720
+ /**
721
+ * Compares two cell values the way `type` implies — numerically for the
722
+ * numeric types, chronologically for the date/time types, lexically
723
+ * otherwise. Defaults to string comparison when `type` is absent, mirroring
724
+ * `ColumnDefinition.type`'s own default.
725
+ *
726
+ * `null`/`undefined` sort after every value on both sides. Used directly,
727
+ * that ordering is fixed; `sortRows` applies it directionally without
728
+ * disturbing it — see its own doc comment.
729
+ */
730
+ declare function compareValues(a: unknown, b: unknown, type?: ColumnType): number;
731
+ /** This column's direction in `sort`, or `null` outside it. */
732
+ declare function sortDirectionFor(sort: ColumnSortState, columnId: string): SortDirection | null;
733
+ /** This column's 1-based place in `sort`, or `null` outside it. */
734
+ declare function sortPriorityFor(sort: ColumnSortState, columnId: string): number | null;
735
+ /**
736
+ * The sort with `columnId` cycled to its next direction — none → asc → desc →
737
+ * none.
738
+ *
739
+ * `stack: false` (a plain click) means "sort by only this column": unless
740
+ * `columnId` is already the sole entry, in which case its own cycle just
741
+ * continues, it collapses `sort` down to `columnId` alone at `"asc"`,
742
+ * regardless of any direction it held as part of a larger stack.
743
+ *
744
+ * `stack: true` (a Shift-click) cycles or appends `columnId` in place without
745
+ * disturbing the rest of the stack; cycling it back to `"none"` removes it
746
+ * and closes the gap, leaving the others in their existing order.
747
+ */
748
+ declare function toggleColumnSort(sort: ColumnSortState, columnId: string, options: {
749
+ readonly stack: boolean;
750
+ }): ColumnSortState;
751
+ /**
752
+ * `rows` ordered by `sort` — each entry read against `columns` for its
753
+ * `field` and `type` — with `rowIndex` renumbered to match the new
754
+ * positions, the invariant the rest of the grid relies on for keyboard
755
+ * navigation and `aria-rowindex`.
756
+ *
757
+ * Stable (`Array.prototype.sort` has been stable since ES2019), so rows
758
+ * equal under every active key keep their relative order. A `null`/
759
+ * `undefined` value sorts last under either direction — resolved before
760
+ * `compareValues` is asked, and left out of the direction flip below, so
761
+ * that toggling a column reorders the values it holds rather than moving its
762
+ * gaps to the front.
763
+ *
764
+ * Returns `rows` itself, untouched, when `sort` is empty or names only
765
+ * columns absent from `columns` — a stored sort tolerates a column removed
766
+ * from the definition the same way a stored `ColumnOrderState` does.
767
+ */
768
+ declare function sortRows<Row>(rows: readonly ResolvedRow<Row>[], sort: ColumnSortState, columns: readonly ResolvedColumn<Row, unknown>[]): readonly ResolvedRow<Row>[];
769
+
770
+ /**
771
+ * Whether `value`, stringified, matches `query`'s SQL LIKE-style pattern —
772
+ * case-insensitive always. A bare query is an exact match; `%text%` is
773
+ * contains; `text%` is starts-with; `%text` is ends-with. `null`/`undefined`
774
+ * stringify to `""`.
775
+ *
776
+ * A one-shot convenience: parses `query` fresh on every call. `filterRows`
777
+ * does not call this per cell — it compiles each `TextFilterEntry` once,
778
+ * outside its row loop.
779
+ */
780
+ declare function matchesQuery(value: unknown, query: string): boolean;
781
+ /**
782
+ * This column's current text query, or the global entry's when `columnId`
783
+ * is omitted; `null` if no `TextFilterEntry` names it — including when the
784
+ * entry that exists for that key is a `ValueFilterEntry`/
785
+ * `PredicateFilterEntry`/`GroupFilterEntry` instead, since none of those
786
+ * hold a query string to report.
787
+ */
788
+ declare function filterQueryFor<Row>(filter: FilterState<Row>, columnId?: FieldPath<Row> | (string & {})): string | null;
789
+ /**
790
+ * The filter with the `TextFilterEntry` for `columnId` (or the global
791
+ * entry, when `columnId` is omitted) set to `query` — replacing whatever
792
+ * entry already holds that key, of any variant, if present; appending a new
793
+ * `TextFilterEntry` if not. An empty `query` removes the entry instead of
794
+ * storing a filter that can never usefully match.
795
+ *
796
+ * Returns `filter` itself, unchanged, when the result would be a no-op: an
797
+ * empty `query` naming a key with no existing entry, or a `query` equal to
798
+ * an existing `TextFilterEntry`'s.
799
+ */
800
+ declare function setColumnFilter<Row>(filter: FilterState<Row>, columnId: (FieldPath<Row> | (string & {})) | undefined, query: string): FilterState<Row>;
801
+ /** `filter` reduced to empty. Returns `filter` itself when already empty. */
802
+ declare function clearAllFilters<Row>(filter: FilterState<Row>): FilterState<Row>;
803
+ /**
804
+ * `rows` reduced to those matching every entry in `filter` (AND at the top
805
+ * level — nest a `GroupFilterEntry` for OR). `rowIndex` is renumbered to
806
+ * match the retained positions — the same invariant `sortRows` keeps,
807
+ * needed here independently since `filterRows` runs before `sortRows` in
808
+ * the composed pipeline (see `resolveShownRows`).
809
+ *
810
+ * Order-preserving: surviving rows keep their relative order, which is what
811
+ * makes running this before a sort produce the identical final order a
812
+ * sort-then-filter would — only the cost differs.
813
+ *
814
+ * Every entry is compiled once, before the row loop.
815
+ *
816
+ * Returns `rows` itself, untouched, when `filter` is empty or names only
817
+ * columns absent from `columns` — a stored filter tolerates a column
818
+ * removed from the definition the same way a stored `ColumnSortState` does.
819
+ */
820
+ declare function filterRows<Row>(rows: readonly ResolvedRow<Row>[], filter: FilterState<Row>, columns: readonly ResolvedColumn<Row, unknown>[]): readonly ResolvedRow<Row>[];
821
+
822
+ /**
823
+ * `rows`, filtered then sorted — the one function `DataGrid.tsx` (and any
824
+ * future framework binding) calls instead of composing `filterRows`/
825
+ * `sortRows` itself.
826
+ *
827
+ * Filter always runs first: an O(n) pass that shrinks what the O(n log n)
828
+ * sort has to touch whenever a filter is active, free when it isn't. This
829
+ * is a performance choice, not a correctness one — filtering is an
830
+ * order-preserving predicate and sort is a stable total order over whatever
831
+ * it's handed, so the final order is identical either way.
832
+ */
833
+ declare function resolveShownRows<Row>(rows: readonly ResolvedRow<Row>[], filter: FilterState<Row>, sort: ColumnSortState, columns: readonly ResolvedColumn<Row, unknown>[]): readonly ResolvedRow<Row>[];
834
+ /**
835
+ * `ids` resolved back to the rows behind them, keyed by `rowsById`.
836
+ *
837
+ * An id with no row behind it is dropped rather than left `undefined`: a
838
+ * selection outlives a change to the data, so that filtering a row out and
839
+ * back leaves it selected, and only what still exists can be handed to a
840
+ * callback.
841
+ */
842
+ declare function resolveRows<Row>(rowsById: ReadonlyMap<string, ResolvedRow<Row>>, ids: readonly string[]): readonly ResolvedRow<Row>[];
843
+ /**
844
+ * `ids` resolved back to the columns behind them, paired with the position
845
+ * each holds in `columns` — a stale id is dropped, matching `resolveRows`.
846
+ */
847
+ declare function resolveColumns<Row, Node>(columns: readonly ResolvedColumn<Row, Node>[], ids: readonly string[]): readonly SelectedColumn<Row, Node>[];
848
+ /**
849
+ * `cell` resolved down to the row, column and value it addresses — `null`
850
+ * when there is no cell, or when either half of it names a row or column
851
+ * that no longer exists.
852
+ */
853
+ declare function resolveCell<Row, Node>(rowsById: ReadonlyMap<string, ResolvedRow<Row>>, columns: readonly ResolvedColumn<Row, Node>[], cell: SelectedCellRef | null): SelectedCell<Row, Node> | null;
559
854
 
560
- export { type CellSelectEvent, type CellSelectionChangeEvent, type CellSelectionMode, type CellSelectionState, type CellTemplateContext, type ColumnAlignment, type ColumnConstraints, type ColumnDefinition, type ColumnOrderEvent, type ColumnOrderState, type ColumnResizeEvent, type ColumnResizeSession, type ColumnResolveOptions, type ColumnSelectEvent, type ColumnSelectionChangeEvent, type ColumnSizeDefaults, type ColumnSizingState, type ColumnType, type ColumnWrapConfig, type ColumnsSelectEvent, DEFAULT_COLUMN_SIZES, type DropSide, type FieldPath, KEYBOARD_STEP, type ResolvedColumn, type ResolvedRow, type RowSelectEvent, type RowSelectionChangeEvent, type RowsSelectEvent, type SelectableConfig, type SelectedCell, type SelectedCellRef, type SelectedColumn, type SelectionDiff, type SelectionMode, type SelectionState, accessDotted, alignmentForType, applyColumnOrder, applyColumnResize, beginColumnResize, clampColumnWidth, clearSelection, defineColumnsFromRows, diffSelection, fitColumnsToWidth, getColumnId, isSameCell, moveColumnBefore, movesColumn, resolveColumnConstraints, resolveColumnLabel, resolveColumnWidths, resolveDropBefore, resolveRowId, selectAll, selectCell, selectOnly, selectRange, sizeColumnToContent, toggleCellSelection, toggleSelection, totalColumnWidth };
855
+ export { type CellSelectEvent, type CellSelectionChangeEvent, type CellSelectionMode, type CellSelectionState, type CellTemplateContext, type ColumnAlignment, type ColumnConstraints, type ColumnDefinition, type ColumnOrderEvent, type ColumnOrderState, type ColumnResizeEvent, type ColumnResizeSession, type ColumnResolveOptions, type ColumnSelectEvent, type ColumnSelectionChangeEvent, type ColumnSizeDefaults, type ColumnSizingState, type ColumnSortEntry, type ColumnSortEvent, type ColumnSortState, type ColumnType, type ColumnWrapConfig, type ColumnsSelectEvent, DEFAULT_COLUMN_SIZES, type DropSide, type FieldPath, type FilterEntry, type FilterPredicate, type FilterState, type GridFocus, type GroupFilterEntry, HEADER_ROW, KEYBOARD_STEP, type KeyShortcutCapabilities, type NavigationModifiers, type PredicateFilterEntry, type ResolvedColumn, type ResolvedRow, type RowSelectEvent, type RowSelectionChangeEvent, type RowsSelectEvent, type SelectIntent, type SelectableConfig, type SelectedCell, type SelectedCellRef, type SelectedColumn, type SelectionDiff, type SelectionMode, type SelectionState, type SortDirection, type TextFilterEntry, type ValueFilterEntry, accessDotted, alignmentForType, applyColumnOrder, applyColumnResize, applySelectionIntent, beginColumnResize, buildKeyShortcuts, clampColumnWidth, clampFocus, clearAllFilters, clearSelection, compareValues, defineColumnsFromRows, diffSelection, filterQueryFor, filterRows, fitColumnsToWidth, getColumnId, intentOf, isSameCell, matchesQuery, moveColumnBefore, movesColumn, nextFocusForKey, resolveCell, resolveColumnConstraints, resolveColumnLabel, resolveColumnWidths, resolveColumns, resolveDropBefore, resolveKeyboardDropTarget, resolveRowId, resolveRows, resolveShownRows, revertColumnSize, selectAll, selectCell, selectOnly, selectRange, setColumnFilter, sizeColumnToContent, sortDirectionFor, sortPriorityFor, sortRows, toggleCellSelection, toggleColumnSort, toggleSelection, totalColumnWidth };
package/dist/index.js CHANGED
@@ -70,10 +70,62 @@ function resolveColumnLabel(column) {
70
70
  }
71
71
  return typeof headerTemplate === "function" ? headerTemplate() : headerTemplate;
72
72
  }
73
+ function buildKeyShortcuts(capabilities) {
74
+ return [
75
+ capabilities.reorderable ? "Control+ArrowLeft Control+ArrowRight" : "",
76
+ // Alt+Enter is the keyboard equivalent of double-clicking the resize
77
+ // handle to size the column to its content — same gate as the nudge
78
+ // shortcuts, since both are resize actions.
79
+ capabilities.resizable ? "Alt+ArrowLeft Alt+ArrowRight Alt+Enter" : "",
80
+ capabilities.sortable ? "Alt+ArrowUp" : ""
81
+ ].filter(Boolean).join(" ");
82
+ }
73
83
  function accessDotted(obj, path) {
74
84
  return path.split(".").reduce((value, key) => isNested(value) ? value[key] : void 0, obj);
75
85
  }
76
86
 
87
+ // src/util/navigation.ts
88
+ var HEADER_ROW = -1;
89
+ function clampFocus(focus, rowCount, columnCount) {
90
+ return {
91
+ rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
92
+ columnIndex: Math.min(
93
+ Math.max(focus.columnIndex, 0),
94
+ Math.max(columnCount - 1, 0)
95
+ )
96
+ };
97
+ }
98
+ function nextFocusForKey(key, modifiers, focus, rowCount, columnCount, pageSize) {
99
+ if (modifiers.altKey) {
100
+ return null;
101
+ }
102
+ const { rowIndex, columnIndex } = focus;
103
+ switch (key) {
104
+ case "ArrowLeft":
105
+ if (modifiers.ctrlKey) return null;
106
+ return { rowIndex, columnIndex: columnIndex - 1 };
107
+ case "ArrowRight":
108
+ if (modifiers.ctrlKey) return null;
109
+ return { rowIndex, columnIndex: columnIndex + 1 };
110
+ case "ArrowUp":
111
+ return { rowIndex: rowIndex - 1, columnIndex };
112
+ case "ArrowDown":
113
+ return { rowIndex: rowIndex + 1, columnIndex };
114
+ // Ctrl takes Home and End to the grid's ends rather than the row's, which
115
+ // is the one place Ctrl is navigation rather than reorder.
116
+ case "Home":
117
+ return modifiers.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
118
+ case "End":
119
+ return modifiers.ctrlKey ? { rowIndex: rowCount - 1, columnIndex: columnCount - 1 } : { rowIndex, columnIndex: columnCount - 1 };
120
+ case "PageUp":
121
+ return { rowIndex: rowIndex - pageSize, columnIndex };
122
+ case "PageDown":
123
+ return { rowIndex: rowIndex + pageSize, columnIndex };
124
+ default:
125
+ return null;
126
+ }
127
+ }
128
+
77
129
  // src/util/ordering.ts
78
130
  function applyColumnOrder(columns, order) {
79
131
  if (order.length === 0) {
@@ -110,6 +162,17 @@ function resolveDropBefore(orderedIds, targetId, side) {
110
162
  const index = orderedIds.indexOf(targetId);
111
163
  return index === -1 ? null : orderedIds[index + 1] ?? null;
112
164
  }
165
+ function resolveKeyboardDropTarget(orderedIds, columnId, direction) {
166
+ const index = orderedIds.indexOf(columnId);
167
+ if (index === -1) {
168
+ return void 0;
169
+ }
170
+ const targetIndex = direction === -1 ? index - 1 : index + 2;
171
+ if (targetIndex < 0) {
172
+ return void 0;
173
+ }
174
+ return orderedIds[targetIndex] ?? null;
175
+ }
113
176
 
114
177
  // src/util/selection.ts
115
178
  var NO_CHANGE = { added: [], removed: [] };
@@ -184,6 +247,24 @@ function toggleCellSelection(selection, cell, mode) {
184
247
  }
185
248
  return isSameCell(selection, cell) ? null : cell;
186
249
  }
250
+ function intentOf(event) {
251
+ if (event.shiftKey) {
252
+ return "range";
253
+ }
254
+ return event.ctrlKey || event.metaKey ? "toggle" : "replace";
255
+ }
256
+ function applySelectionIntent(selection, orderedIds, anchorId, id, intent, mode) {
257
+ switch (intent) {
258
+ case "toggle":
259
+ return toggleSelection(selection, id, mode);
260
+ // A range with no anchor yet spans from the id to itself, which is the
261
+ // plain click a first Shift-click may as well be.
262
+ case "range":
263
+ return selectRange(selection, orderedIds, anchorId ?? id, id, mode);
264
+ default:
265
+ return selectOnly(selection, id, mode);
266
+ }
267
+ }
187
268
 
188
269
  // src/util/sizing.ts
189
270
  var DEFAULT_COLUMN_SIZES = {
@@ -314,34 +395,352 @@ function fitColumnsToWidth(resolved, availableWidth, defaults) {
314
395
  function sizeColumnToContent(measuredWidth, constraints, padding = 2) {
315
396
  return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);
316
397
  }
398
+ function revertColumnSize(current, base, columnId) {
399
+ const next = Object.fromEntries(
400
+ Object.entries(current).filter(([id]) => id !== columnId)
401
+ );
402
+ if (columnId in base) {
403
+ next[columnId] = base[columnId];
404
+ }
405
+ return next;
406
+ }
407
+
408
+ // src/util/sorting.ts
409
+ function isEmpty(value) {
410
+ return value === null || value === void 0;
411
+ }
412
+ function toTime(value) {
413
+ return value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
414
+ }
415
+ function compareValues(a, b, type) {
416
+ if (isEmpty(a)) {
417
+ return isEmpty(b) ? 0 : 1;
418
+ }
419
+ if (isEmpty(b)) {
420
+ return -1;
421
+ }
422
+ switch (type) {
423
+ case "number":
424
+ case "decimal":
425
+ case "currency":
426
+ case "percent":
427
+ return Number(a) - Number(b);
428
+ case "boolean":
429
+ return Number(Boolean(a)) - Number(Boolean(b));
430
+ case "date":
431
+ case "dateTime":
432
+ case "time":
433
+ return toTime(a) - toTime(b);
434
+ default:
435
+ return String(a).localeCompare(String(b));
436
+ }
437
+ }
438
+ function sortDirectionFor(sort, columnId) {
439
+ return sort.find((entry) => entry.columnId === columnId)?.direction ?? null;
440
+ }
441
+ function sortPriorityFor(sort, columnId) {
442
+ const index = sort.findIndex((entry) => entry.columnId === columnId);
443
+ return index === -1 ? null : index + 1;
444
+ }
445
+ function toggleColumnSort(sort, columnId, options) {
446
+ const index = sort.findIndex((entry) => entry.columnId === columnId);
447
+ const isSoleEntry = sort.length === 1 && index === 0;
448
+ if (!options.stack && !isSoleEntry) {
449
+ return [{ columnId, direction: "asc" }];
450
+ }
451
+ const current = index === -1 ? void 0 : sort[index]?.direction;
452
+ const next = current === void 0 ? "asc" : current === "asc" ? "desc" : null;
453
+ if (next === null) {
454
+ return sort.filter((entry) => entry.columnId !== columnId);
455
+ }
456
+ if (index === -1) {
457
+ return [...sort, { columnId, direction: next }];
458
+ }
459
+ return sort.map(
460
+ (entry, i) => i === index ? { columnId, direction: next } : entry
461
+ );
462
+ }
463
+ function sortRows(rows, sort, columns) {
464
+ if (sort.length === 0) {
465
+ return rows;
466
+ }
467
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
468
+ const active = sort.filter((entry) => byId.has(entry.columnId));
469
+ if (active.length === 0) {
470
+ return rows;
471
+ }
472
+ const sorted = [...rows].sort((a, b) => {
473
+ for (const { columnId, direction } of active) {
474
+ const column = byId.get(columnId);
475
+ if (column === void 0) {
476
+ continue;
477
+ }
478
+ const left = accessDotted(a.row, column.field);
479
+ const right = accessDotted(b.row, column.field);
480
+ const leftEmpty = isEmpty(left);
481
+ const rightEmpty = isEmpty(right);
482
+ if (leftEmpty || rightEmpty) {
483
+ if (leftEmpty && rightEmpty) {
484
+ continue;
485
+ }
486
+ return leftEmpty ? 1 : -1;
487
+ }
488
+ const result = compareValues(left, right, column.type);
489
+ if (result !== 0) {
490
+ return direction === "asc" ? result : -result;
491
+ }
492
+ }
493
+ return 0;
494
+ });
495
+ return sorted.map(
496
+ (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
497
+ );
498
+ }
499
+
500
+ // src/util/filtering.ts
501
+ function parseFilterPattern(query) {
502
+ const start = query.startsWith("%");
503
+ const end = query.endsWith("%");
504
+ let text = query;
505
+ if (start) {
506
+ text = text.slice(1);
507
+ }
508
+ if (end) {
509
+ text = text.slice(0, -1);
510
+ }
511
+ text = text.toLowerCase();
512
+ if (start && end) {
513
+ return { mode: "contains", text };
514
+ }
515
+ if (start) {
516
+ return { mode: "endsWith", text };
517
+ }
518
+ if (end) {
519
+ return { mode: "startsWith", text };
520
+ }
521
+ return { mode: "exact", text };
522
+ }
523
+ function isEmptyValue(value) {
524
+ return value === null || value === void 0;
525
+ }
526
+ function compileFilterQuery(query) {
527
+ const { mode, text } = parseFilterPattern(query);
528
+ return (value) => {
529
+ const candidate = (isEmptyValue(value) ? "" : String(value)).toLowerCase();
530
+ switch (mode) {
531
+ case "exact":
532
+ return candidate === text;
533
+ case "contains":
534
+ return candidate.includes(text);
535
+ case "startsWith":
536
+ return candidate.startsWith(text);
537
+ case "endsWith":
538
+ return candidate.endsWith(text);
539
+ }
540
+ };
541
+ }
542
+ function matchesTypedValue(value, target, type) {
543
+ if (typeof target === "number") {
544
+ if (type !== "number" && type !== "decimal" && type !== "currency" && type !== "percent") {
545
+ return false;
546
+ }
547
+ return typeof value === "number" && value === target;
548
+ }
549
+ if (typeof target === "boolean") {
550
+ if (type !== "boolean") {
551
+ return false;
552
+ }
553
+ return typeof value === "boolean" && value === target;
554
+ }
555
+ if (type !== "date" && type !== "dateTime" && type !== "time") {
556
+ return false;
557
+ }
558
+ return value instanceof Date && value.getTime() === target.getTime();
559
+ }
560
+ function isEntryUsable(entry, byId) {
561
+ if ("combinator" in entry) {
562
+ return true;
563
+ }
564
+ return entry.columnId === void 0 || byId.has(entry.columnId);
565
+ }
566
+ function compileFilterEntry(entry, byId, columns) {
567
+ if ("combinator" in entry) {
568
+ const tests = entry.entries.filter((nested) => isEntryUsable(nested, byId)).map((nested) => compileFilterEntry(nested, byId, columns));
569
+ return entry.combinator === "and" ? (row) => tests.every((test2) => test2(row)) : (row) => tests.some((test2) => test2(row));
570
+ }
571
+ if ("predicate" in entry) {
572
+ const { columnId: columnId2, predicate } = entry;
573
+ const column = columnId2 === void 0 ? void 0 : byId.get(columnId2);
574
+ return (row) => predicate(
575
+ column === void 0 ? void 0 : accessDotted(row.row, column.field),
576
+ row.row
577
+ );
578
+ }
579
+ if ("value" in entry) {
580
+ const { columnId: columnId2, value } = entry;
581
+ if (columnId2 !== void 0) {
582
+ const column = byId.get(columnId2);
583
+ if (column === void 0) {
584
+ return () => false;
585
+ }
586
+ return (row) => matchesTypedValue(
587
+ accessDotted(row.row, column.field),
588
+ value,
589
+ column.type
590
+ );
591
+ }
592
+ return (row) => columns.some(
593
+ (entry2) => matchesTypedValue(
594
+ accessDotted(row.row, entry2.column.field),
595
+ value,
596
+ entry2.column.type
597
+ )
598
+ );
599
+ }
600
+ const { columnId, query } = entry;
601
+ const test = compileFilterQuery(query);
602
+ if (columnId !== void 0) {
603
+ const column = byId.get(columnId);
604
+ if (column === void 0) {
605
+ return () => false;
606
+ }
607
+ return (row) => test(accessDotted(row.row, column.field));
608
+ }
609
+ return (row) => columns.some((entry2) => test(accessDotted(row.row, entry2.column.field)));
610
+ }
611
+ function matchesQuery(value, query) {
612
+ return compileFilterQuery(query)(value);
613
+ }
614
+ function filterQueryFor(filter, columnId) {
615
+ const entry = filter.find(
616
+ (candidate) => "query" in candidate && candidate.columnId === columnId
617
+ );
618
+ return entry?.query ?? null;
619
+ }
620
+ function setColumnFilter(filter, columnId, query) {
621
+ const index = filter.findIndex(
622
+ (entry) => !("combinator" in entry) && entry.columnId === columnId
623
+ );
624
+ if (query === "") {
625
+ return index === -1 ? filter : filter.filter((_entry, i) => i !== index);
626
+ }
627
+ const next = columnId === void 0 ? { query } : { columnId, query };
628
+ if (index === -1) {
629
+ return [...filter, next];
630
+ }
631
+ const existing = filter[index];
632
+ if (existing !== void 0 && "query" in existing && existing.query === query) {
633
+ return filter;
634
+ }
635
+ return filter.map((entry, i) => i === index ? next : entry);
636
+ }
637
+ function clearAllFilters(filter) {
638
+ return filter.length === 0 ? filter : [];
639
+ }
640
+ function filterRows(rows, filter, columns) {
641
+ if (filter.length === 0) {
642
+ return rows;
643
+ }
644
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
645
+ const active = filter.filter((entry) => isEntryUsable(entry, byId));
646
+ if (active.length === 0) {
647
+ return rows;
648
+ }
649
+ const compiled = active.map(
650
+ (entry) => compileFilterEntry(entry, byId, columns)
651
+ );
652
+ const filtered = rows.filter((row) => compiled.every((test) => test(row)));
653
+ return filtered.map(
654
+ (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
655
+ );
656
+ }
657
+
658
+ // src/util/rows.ts
659
+ function resolveShownRows(rows, filter, sort, columns) {
660
+ return sortRows(filterRows(rows, filter, columns), sort, columns);
661
+ }
662
+ function resolveRows(rowsById, ids) {
663
+ return ids.map((id) => rowsById.get(id)).filter((row) => row !== void 0);
664
+ }
665
+ function resolveColumns(columns, ids) {
666
+ return ids.map((id) => {
667
+ const columnIndex = columns.findIndex((entry) => entry.id === id);
668
+ const column = columns[columnIndex];
669
+ return column === void 0 ? void 0 : { columnId: id, column, columnIndex };
670
+ }).filter((column) => column !== void 0);
671
+ }
672
+ function resolveCell(rowsById, columns, cell) {
673
+ if (cell === null) {
674
+ return null;
675
+ }
676
+ const entry = rowsById.get(cell.rowId);
677
+ const columnIndex = columns.findIndex(
678
+ (candidate) => candidate.id === cell.columnId
679
+ );
680
+ const column = columns[columnIndex];
681
+ if (entry === void 0 || column === void 0) {
682
+ return null;
683
+ }
684
+ return {
685
+ rowId: cell.rowId,
686
+ columnId: cell.columnId,
687
+ row: entry.row,
688
+ column,
689
+ rowIndex: entry.rowIndex,
690
+ columnIndex,
691
+ value: accessDotted(entry.row, column.column.field)
692
+ };
693
+ }
317
694
  export {
318
695
  DEFAULT_COLUMN_SIZES,
696
+ HEADER_ROW,
319
697
  KEYBOARD_STEP,
320
698
  accessDotted,
321
699
  alignmentForType,
322
700
  applyColumnOrder,
323
701
  applyColumnResize,
702
+ applySelectionIntent,
324
703
  beginColumnResize,
704
+ buildKeyShortcuts,
325
705
  clampColumnWidth,
706
+ clampFocus,
707
+ clearAllFilters,
326
708
  clearSelection,
709
+ compareValues,
327
710
  defineColumnsFromRows,
328
711
  diffSelection,
712
+ filterQueryFor,
713
+ filterRows,
329
714
  fitColumnsToWidth,
330
715
  getColumnId,
716
+ intentOf,
331
717
  isSameCell,
718
+ matchesQuery,
332
719
  moveColumnBefore,
333
720
  movesColumn,
721
+ nextFocusForKey,
722
+ resolveCell,
334
723
  resolveColumnConstraints,
335
724
  resolveColumnLabel,
336
725
  resolveColumnWidths,
726
+ resolveColumns,
337
727
  resolveDropBefore,
728
+ resolveKeyboardDropTarget,
338
729
  resolveRowId,
730
+ resolveRows,
731
+ resolveShownRows,
732
+ revertColumnSize,
339
733
  selectAll,
340
734
  selectCell,
341
735
  selectOnly,
342
736
  selectRange,
737
+ setColumnFilter,
343
738
  sizeColumnToContent,
739
+ sortDirectionFor,
740
+ sortPriorityFor,
741
+ sortRows,
344
742
  toggleCellSelection,
743
+ toggleColumnSort,
345
744
  toggleSelection,
346
745
  totalColumnWidth
347
746
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/util/grid.ts","../src/util/ordering.ts","../src/util/selection.ts","../src/util/sizing.ts"],"sourcesContent":["import type { ColumnAlignment, ColumnDefinition, ColumnType } from \"../types\";\n\n/**\n * Narrows to a value worth drilling into. Mirrors the `LeafValue` union in\n * `../types`: arrays and `Date`s are cell values rather than groups of\n * columns, and `null` cannot be introspected. Keep the two in sync.\n */\nfunction isNested(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\nfunction getType(value: unknown): ColumnType {\n if (value instanceof Date) return \"dateTime\";\n if (typeof value === \"string\") return \"string\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"bigint\") return \"number\";\n if (typeof value === \"symbol\") return \"string\";\n return \"string\";\n}\n\n/**\n * How a column of `type` aligns by default. Numeric types align right so that\n * their digits line up by place value; everything else reads from the left.\n */\nexport function alignmentForType(type: ColumnType): ColumnAlignment {\n switch (type) {\n case \"number\":\n case \"decimal\":\n case \"currency\":\n case \"percent\":\n return \"right\";\n default:\n return \"left\";\n }\n}\n\nfunction getAlignment(value: unknown): ColumnAlignment {\n return alignmentForType(getType(value));\n}\n\n/**\n * Derives one column per field across `rows`, in the order each field is first\n * seen — so a key that only appears in a later row still gets a column,\n * appended at the end. Nested objects contribute their non-object properties\n * as `\"Parent.Child\"`; anything deeper is skipped, as is the nested object\n * itself.\n *\n * Order follows `Object.entries`, which lists integer-like keys first\n * regardless of the order they were written in.\n */\nexport function defineColumnsFromRows<Row>(\n rows: readonly Row[],\n): readonly ColumnDefinition<Row>[] {\n const columns: ColumnDefinition<Row>[] = [];\n const seen = new Set<string>();\n\n function add(field: string, value: unknown): void {\n if (seen.has(field)) {\n return;\n }\n seen.add(field);\n columns.push({\n field,\n type: getType(value),\n alignment: getAlignment(value),\n });\n }\n\n for (const row of rows) {\n if (!isNested(row)) {\n continue;\n }\n for (const [key, value] of Object.entries(row)) {\n if (!isNested(value)) {\n add(key, value);\n continue;\n }\n for (const [nestedKey, nestedValue] of Object.entries(value)) {\n if (!isNested(nestedValue)) {\n add(`${key}.${nestedKey}`, nestedValue);\n }\n }\n }\n }\n\n return columns;\n}\n\n/**\n * The key state about a column is stored under, and the key it renders with.\n * Every such lookup goes through here so the two can never disagree.\n */\nexport function getColumnId<Row>(\n column: ColumnDefinition<Row, unknown>,\n): string {\n return column.id ?? column.field;\n}\n\n/**\n * The key state about a row is stored under, and the key it renders with — the\n * row's counterpart to `getColumnId`, and for the same reason: every such\n * lookup goes through here so no two can disagree.\n *\n * Falls back to the position so a grid given no `getRowId` still works. That\n * fallback ties a selection to where a row sits rather than to the row, so\n * anything that sorts, filters or pages its data has to supply one — otherwise\n * a re-sort leaves the same positions selected under different rows.\n */\nexport function resolveRowId<Row>(\n row: Row,\n index: number,\n getRowId?: (row: Row, index: number) => string,\n): string {\n return getRowId?.(row, index) ?? String(index);\n}\n\n/**\n * What a column's header shows: its own `headerTemplate`, called first if it is\n * a function, or else a label read off the field path — `\"Application.Id\"`\n * reading as `\"Application Id\"`.\n */\nexport function resolveColumnLabel<Row, Node>(\n column: ColumnDefinition<Row, Node>,\n): Node | string {\n const { headerTemplate } = column;\n if (headerTemplate === undefined) {\n return column.field.split(\".\").join(\" \");\n }\n // `Node` is open, so it could itself be a function type and the `typeof`\n // check cannot narrow on its own. A header that is callable is the lazy\n // form by definition — no adapter binds `Node` to a function.\n return typeof headerTemplate === \"function\"\n ? (headerTemplate as () => Node)()\n : headerTemplate;\n}\n\n/**\n * Accesses a value at a dotted path, e.g. `foo.bar.baz`.\n *\n * @param obj The object to access\n * @param path Path to the value, e.g. `\"foo.bar\"`\n * @returns The value at `path`, or `undefined` if it doesn't exist\n */\nexport function accessDotted(obj: unknown, path: string): unknown {\n return path\n .split(\".\")\n .reduce((value, key) => (isNested(value) ? value[key] : undefined), obj);\n}\n","import type { ColumnDefinition, ColumnOrderState, DropSide } from \"../types\";\nimport { getColumnId } from \"./grid\";\n\n/**\n * Puts `columns` in the order `order` gives. Ids it lists come first, in that\n * order; columns it omits keep their position among the definitions and\n * follow. Ids naming no column are ignored.\n *\n * Tolerating both gaps is what lets a stored order outlive the column list it\n * was made against.\n */\nexport function applyColumnOrder<Row, Node>(\n columns: readonly ColumnDefinition<Row, Node>[],\n order: ColumnOrderState,\n): readonly ColumnDefinition<Row, Node>[] {\n if (order.length === 0) {\n return columns;\n }\n\n const byId = new Map(columns.map((column) => [getColumnId(column), column]));\n const listed = order\n .map((id) => byId.get(id))\n .filter((column) => column !== undefined);\n const taken = new Set(listed.map((column) => getColumnId(column)));\n\n return [\n ...listed,\n ...columns.filter((column) => !taken.has(getColumnId(column))),\n ];\n}\n\n/**\n * The order with `movedId` lifted out and put back in front of `beforeId`, or\n * at the end when that is `null`.\n *\n * Returns `orderedIds` itself when the move changes nothing, so callers can\n * treat an unchanged reference as \"no move happened\".\n */\nexport function moveColumnBefore(\n orderedIds: ColumnOrderState,\n movedId: string,\n beforeId: string | null,\n): ColumnOrderState {\n if (movedId === beforeId || !orderedIds.includes(movedId)) {\n return orderedIds;\n }\n\n const without = orderedIds.filter((id) => id !== movedId);\n // Taken after the removal: an index read before it would be one too far\n // right for every move that travels rightwards.\n const target = beforeId === null ? without.length : without.indexOf(beforeId);\n if (target === -1) {\n return orderedIds;\n }\n\n const next = [...without];\n next.splice(target, 0, movedId);\n return next.every((id, index) => id === orderedIds[index])\n ? orderedIds\n : next;\n}\n\n/**\n * Whether moving `movedId` in front of `beforeId` would rearrange anything, so\n * that a drop indicator promises exactly the moves that happen.\n *\n * Defined against `moveColumnBefore` rather than restating its conditions, so\n * the two cannot disagree about what a no-op is.\n */\nexport function movesColumn(\n orderedIds: ColumnOrderState,\n movedId: string,\n beforeId: string | null,\n): boolean {\n return moveColumnBefore(orderedIds, movedId, beforeId) !== orderedIds;\n}\n\n/**\n * The id a drop on `side` of `targetId` lands in front of, which is `null`\n * past the last column.\n *\n * \"After C\" and \"before D\" name the same gap, so collapsing both to one id\n * keeps a drop target a single nullable string.\n */\nexport function resolveDropBefore(\n orderedIds: ColumnOrderState,\n targetId: string,\n side: DropSide,\n): string | null {\n if (side === \"before\") {\n return targetId;\n }\n const index = orderedIds.indexOf(targetId);\n return index === -1 ? null : (orderedIds[index + 1] ?? null);\n}\n","import type {\n CellSelectionMode,\n CellSelectionState,\n SelectedCellRef,\n SelectionDiff,\n SelectionMode,\n SelectionState,\n} from \"../types\";\n\n/** Nothing changed, so nothing to report — shared to save an allocation. */\nconst NO_CHANGE: SelectionDiff = { added: [], removed: [] };\n\n/**\n * Whether two selections hold the same ids in the same order.\n *\n * Order counts because the selection's own order is what makes `at(-1)` the\n * most recently selected; a comparison that ignored it would have a transform\n * return the old reference after a reordering that a consumer can observe.\n */\nfunction sameSelection(a: SelectionState, b: SelectionState): boolean {\n return a.length === b.length && a.every((id, index) => id === b[index]);\n}\n\n/**\n * The selection with `id` added if absent and removed if present — a\n * Ctrl-click, or Space on the focused member.\n *\n * The mode is enforced here rather than by each caller, so `\"single\"` cannot\n * accumulate no matter which interaction reaches it. Returns `selection`\n * itself when `mode` is `false`, keeping the unchanged-reference contract\n * `moveColumnBefore` set: a caller treats an unchanged reference as \"nothing\n * happened\" and neither renders nor reports.\n */\nexport function toggleSelection(\n selection: SelectionState,\n id: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode === false) {\n return selection;\n }\n if (selection.includes(id)) {\n return selection.filter((selectedId) => selectedId !== id);\n }\n return mode === \"single\" ? [id] : [...selection, id];\n}\n\n/**\n * The selection reduced to `id` alone — a plain click, which discards what\n * came before rather than adding to it.\n *\n * Never deselects, so clicking the one selected row leaves it selected.\n * Letting go of it is Ctrl-click's job, and `toggleSelection`'s.\n */\nexport function selectOnly(\n selection: SelectionState,\n id: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode === false) {\n return selection;\n }\n return selection.length === 1 && selection[0] === id ? selection : [id];\n}\n\n/**\n * The selection replaced by every id from `anchorId` to `focusId` inclusive — a\n * Shift-click. The two may be given in either order.\n *\n * Spans `orderedIds`, the order as displayed, rather than the selection's own:\n * the range a user drew across the screen is the range they mean, whatever\n * order they clicked its ends in. Degrades to `selectOnly` under `\"single\"`,\n * where a range has nowhere to go.\n */\nexport function selectRange(\n selection: SelectionState,\n orderedIds: readonly string[],\n anchorId: string,\n focusId: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode !== \"multiple\") {\n return selectOnly(selection, focusId, mode);\n }\n\n const anchor = orderedIds.indexOf(anchorId);\n const focus = orderedIds.indexOf(focusId);\n if (anchor === -1 || focus === -1) {\n return selection;\n }\n\n const next = orderedIds.slice(\n Math.min(anchor, focus),\n Math.max(anchor, focus) + 1,\n );\n return sameSelection(selection, next) ? selection : next;\n}\n\n/**\n * Every id in `orderedIds`, or `selection` untouched under any mode but\n * `\"multiple\"` — there is no selecting all of one thing.\n */\nexport function selectAll(\n selection: SelectionState,\n orderedIds: readonly string[],\n mode: SelectionMode,\n): SelectionState {\n if (mode !== \"multiple\") {\n return selection;\n }\n return sameSelection(selection, orderedIds) ? selection : orderedIds;\n}\n\nexport function clearSelection(selection: SelectionState): SelectionState {\n return selection.length === 0 ? selection : [];\n}\n\n/**\n * What one transition added and what it removed.\n *\n * Defined here rather than in an adapter so that every callback reporting a\n * selection change reads one answer, and a select and a deselect fired for the\n * same interaction cannot disagree about which it was.\n */\nexport function diffSelection(\n previous: SelectionState,\n next: SelectionState,\n): SelectionDiff {\n if (previous === next) {\n return NO_CHANGE;\n }\n\n const before = new Set(previous);\n const after = new Set(next);\n return {\n added: next.filter((id) => !before.has(id)),\n removed: previous.filter((id) => !after.has(id)),\n };\n}\n\nexport function isSameCell(\n a: CellSelectionState,\n b: CellSelectionState,\n): boolean {\n if (a === null || b === null) {\n return a === b;\n }\n return a.rowId === b.rowId && a.columnId === b.columnId;\n}\n\n/**\n * The cell selection moved to `cell` — a plain click. Idempotent: clicking the\n * selected cell again leaves it selected, matching `selectOnly`.\n */\nexport function selectCell(\n selection: CellSelectionState,\n cell: SelectedCellRef,\n mode: CellSelectionMode,\n): CellSelectionState {\n if (mode === false || isSameCell(selection, cell)) {\n return selection;\n }\n return cell;\n}\n\n/**\n * The cell selection moved to `cell`, or cleared when it is already there — a\n * Ctrl-click, matching `toggleSelection`.\n */\nexport function toggleCellSelection(\n selection: CellSelectionState,\n cell: SelectedCellRef,\n mode: CellSelectionMode,\n): CellSelectionState {\n if (mode === false) {\n return selection;\n }\n return isSameCell(selection, cell) ? null : cell;\n}\n","import type {\n ColumnConstraints,\n ColumnDefinition,\n ColumnResizeSession,\n ColumnResolveOptions,\n ColumnSizeDefaults,\n ColumnSizingState,\n ResolvedColumn,\n} from \"../types\";\nimport { alignmentForType, getColumnId, resolveColumnLabel } from \"./grid\";\n\n/**\n * Applied to any column that does not size itself. The minimum is a width a\n * header stays legible at rather than a measured value — a column dragged\n * narrower than this reads as a rendering fault.\n */\nexport const DEFAULT_COLUMN_SIZES: ColumnSizeDefaults = {\n width: 150,\n minWidth: 40,\n maxWidth: Number.POSITIVE_INFINITY,\n};\n\n/** How far one arrow key press moves a column edge. */\nexport const KEYBOARD_STEP = 10;\n\nfunction withDefaults(\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnSizeDefaults {\n return { ...DEFAULT_COLUMN_SIZES, ...defaults };\n}\n\n/**\n * The bounds `column` may be sized between. Resolved in one place so that a\n * drag, a clamp and an auto-fit cannot disagree about a column's limits.\n */\nexport function resolveColumnConstraints<Row>(\n column: ColumnDefinition<Row, unknown>,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnConstraints {\n const resolved = withDefaults(defaults);\n return {\n minWidth: column.minWidth ?? resolved.minWidth,\n maxWidth: column.maxWidth ?? resolved.maxWidth,\n };\n}\n\n/** Holds `width` within `constraints`. */\nexport function clampColumnWidth(\n width: number,\n constraints: ColumnConstraints,\n): number {\n return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);\n}\n\n/**\n * Pairs each column with everything needed to render it — the width it sits\n * at, the label its header shows, whether it may be resized and how its cells\n * align. Widths take the first of the sizing state, the column's own `width`,\n * and the default, then clamp.\n *\n * The sizing state winning means a user's drag outlives a re-render; a column\n * absent from it still tracks a `width` edited in the definition.\n */\nexport function resolveColumnWidths<Row, Node>(\n columns: readonly ColumnDefinition<Row, Node>[],\n sizing: ColumnSizingState,\n options?: ColumnResolveOptions,\n): readonly ResolvedColumn<Row, Node>[] {\n const resolved = withDefaults(options?.sizes);\n\n return columns.map((column) => {\n const id = getColumnId(column);\n const stored = sizing[id];\n\n return {\n column,\n id,\n sized: stored !== undefined,\n width: clampColumnWidth(\n stored ?? column.width ?? resolved.width,\n resolveColumnConstraints(column, resolved),\n ),\n label: resolveColumnLabel(column),\n resizable: column.resizable ?? options?.resizable ?? false,\n reorderable: column.reorderable ?? options?.reorderable ?? false,\n alignment: column.alignment ?? alignmentForType(column.type ?? \"string\"),\n };\n });\n}\n\nexport function totalColumnWidth<Row, Node>(\n resolved: readonly ResolvedColumn<Row, Node>[],\n): number {\n return resolved.reduce((total, entry) => total + entry.width, 0);\n}\n\n/**\n * Opens a resize. The session captures its constraints and starting point up\n * front, so applying a pointer position later is arithmetic on numbers alone —\n * which is what keeps the drag testable without a DOM.\n *\n * @param startPosition Where the pointer went down, on the axis being dragged.\n */\nexport function beginColumnResize<Row>(\n column: ColumnDefinition<Row, unknown>,\n startWidth: number,\n startPosition: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnResizeSession {\n return {\n columnId: getColumnId(column),\n startWidth,\n startPosition,\n constraints: resolveColumnConstraints(column, defaults),\n };\n}\n\n/**\n * The width the dragged column takes with the pointer at `position`. The delta\n * is signed, so a right-to-left adapter negates it and nothing else changes.\n */\nexport function applyColumnResize(\n session: ColumnResizeSession,\n position: number,\n): number {\n return clampColumnWidth(\n session.startWidth + (position - session.startPosition),\n session.constraints,\n );\n}\n\ninterface SizingEntry<Row, Node> {\n entry: ResolvedColumn<Row, Node>;\n width: number;\n bound: number;\n adjustable: boolean;\n}\n\n/**\n * Whether `item` has reached `bound` in the direction it moves toward — above\n * it for a column growing to a `maxWidth`, below it for one shrinking to a\n * `minWidth`.\n */\nfunction reachedBound<Row, Node>(\n width: number,\n item: SizingEntry<Row, Node>,\n): boolean {\n return item.bound > item.width ? width >= item.bound : width <= item.bound;\n}\n\n/**\n * Moves every adjustable entry's `width` toward `bound` by its proportional\n * share of `remaining`'s current surplus or deficit, looping so a column\n * clamped at its bound in one pass frees the remainder for the others to\n * absorb in the next.\n */\nfunction redistribute<Row, Node>(\n entries: readonly SizingEntry<Row, Node>[],\n remaining: (sum: number) => number,\n): void {\n for (;;) {\n const movable = entries.filter(\n (item) => item.adjustable && !reachedBound(item.width, item),\n );\n const left = remaining(entries.reduce((t, i) => t + i.width, 0));\n if (left <= 0 || movable.length === 0) {\n break;\n }\n\n const base = movable.reduce((total, item) => total + item.width, 0);\n let clamped = false;\n\n for (const item of movable) {\n const share =\n base === 0 ? left / movable.length : (left * item.width) / base;\n const next = item.width + (item.bound > item.width ? share : -share);\n\n if (reachedBound(next, item)) {\n item.width = item.bound;\n clamped = true;\n } else {\n item.width = next;\n }\n }\n\n if (!clamped) {\n break;\n }\n }\n}\n\n/**\n * Fits columns to `availableWidth`: grows them to fill leftover space, or\n * shrinks them to relieve an overflow, in both cases proportionally to each\n * column's current width. Columns the user has sized are left alone in\n * either direction, so dragging one column neither rubber-bands it back nor\n * has it absorb a squeeze meant for the rest.\n *\n * Growing stops at each column's `maxWidth`; shrinking stops at its\n * `minWidth`. If shrinking every adjustable column to its floor still does\n * not reach `availableWidth`, the columns are returned untouched and the\n * container scrolls rather than crushing them further.\n */\nexport function fitColumnsToWidth<Row, Node>(\n resolved: readonly ResolvedColumn<Row, Node>[],\n availableWidth: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): readonly ResolvedColumn<Row, Node>[] {\n const target = Math.floor(availableWidth);\n const total = totalColumnWidth(resolved);\n if (resolved.length === 0 || total === target) {\n return resolved;\n }\n\n const growing = total < target;\n const entries: SizingEntry<Row, Node>[] = resolved.map((entry) => {\n const constraints = resolveColumnConstraints(entry.column, defaults);\n return {\n entry,\n width: entry.width,\n bound: growing ? constraints.maxWidth : constraints.minWidth,\n adjustable: !entry.sized,\n };\n });\n\n if (!growing) {\n // Only unsized columns can be shrunk, so if they alone cannot reach the\n // target the grid genuinely cannot fit and should scroll, unchanged.\n const floor = entries.reduce(\n (sum, item) => sum + (item.adjustable ? item.bound : item.width),\n 0,\n );\n if (floor > target) {\n return resolved;\n }\n }\n\n redistribute(entries, (sum) => (growing ? target - sum : sum - target));\n\n // Fractional widths would leave the table a sub-pixel off its container and\n // show a scrollbar that never goes away, so round every column toward the\n // target and hand the remaining pixels back out (growing) or claw them\n // back (shrinking) one at a time.\n const rounded = entries.map((item) =>\n growing ? Math.floor(item.width) : Math.ceil(item.width),\n );\n let remainder = target - rounded.reduce((total, width) => total + width, 0);\n\n return entries.map((item, index) => {\n let width = rounded[index] ?? item.width;\n // Rounding toward the target can only ever leave it short when growing\n // (floor) or over when shrinking (ceil), so only one direction is ever\n // live here — but each still needs room left before its own bound.\n const hasRoom = growing ? width < item.bound : width > item.bound;\n\n if (remainder !== 0 && item.adjustable && hasRoom) {\n width += growing ? 1 : -1;\n remainder += growing ? -1 : 1;\n }\n\n return { ...item.entry, width };\n });\n}\n\n/**\n * The width a column needs to show `measuredWidth` of content.\n *\n * `padding` is an allowance on top of the measurement, since a column sized to\n * exactly its content can still round down into an ellipsis.\n */\nexport function sizeColumnToContent(\n measuredWidth: number,\n constraints: ColumnConstraints,\n padding = 2,\n): number {\n return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);\n}\n"],"mappings":"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","names":["total"]}
1
+ {"version":3,"sources":["../src/util/grid.ts","../src/util/navigation.ts","../src/util/ordering.ts","../src/util/selection.ts","../src/util/sizing.ts","../src/util/sorting.ts","../src/util/filtering.ts","../src/util/rows.ts"],"sourcesContent":["import type { ColumnAlignment, ColumnDefinition, ColumnType } from \"../types\";\n\n/**\n * Narrows to a value worth drilling into. Mirrors the `LeafValue` union in\n * `../types`: arrays and `Date`s are cell values rather than groups of\n * columns, and `null` cannot be introspected. Keep the two in sync.\n */\nfunction isNested(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\nfunction getType(value: unknown): ColumnType {\n if (value instanceof Date) return \"dateTime\";\n if (typeof value === \"string\") return \"string\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"bigint\") return \"number\";\n if (typeof value === \"symbol\") return \"string\";\n return \"string\";\n}\n\n/**\n * How a column of `type` aligns by default. Numeric types align right so that\n * their digits line up by place value; everything else reads from the left.\n */\nexport function alignmentForType(type: ColumnType): ColumnAlignment {\n switch (type) {\n case \"number\":\n case \"decimal\":\n case \"currency\":\n case \"percent\":\n return \"right\";\n default:\n return \"left\";\n }\n}\n\nfunction getAlignment(value: unknown): ColumnAlignment {\n return alignmentForType(getType(value));\n}\n\n/**\n * Derives one column per field across `rows`, in the order each field is first\n * seen — so a key that only appears in a later row still gets a column,\n * appended at the end. Nested objects contribute their non-object properties\n * as `\"Parent.Child\"`; anything deeper is skipped, as is the nested object\n * itself.\n *\n * Order follows `Object.entries`, which lists integer-like keys first\n * regardless of the order they were written in.\n */\nexport function defineColumnsFromRows<Row>(\n rows: readonly Row[],\n): readonly ColumnDefinition<Row>[] {\n const columns: ColumnDefinition<Row>[] = [];\n const seen = new Set<string>();\n\n function add(field: string, value: unknown): void {\n if (seen.has(field)) {\n return;\n }\n seen.add(field);\n columns.push({\n field,\n type: getType(value),\n alignment: getAlignment(value),\n });\n }\n\n for (const row of rows) {\n if (!isNested(row)) {\n continue;\n }\n for (const [key, value] of Object.entries(row)) {\n if (!isNested(value)) {\n add(key, value);\n continue;\n }\n for (const [nestedKey, nestedValue] of Object.entries(value)) {\n if (!isNested(nestedValue)) {\n add(`${key}.${nestedKey}`, nestedValue);\n }\n }\n }\n }\n\n return columns;\n}\n\n/**\n * The key state about a column is stored under, and the key it renders with.\n * Every such lookup goes through here so the two can never disagree.\n */\nexport function getColumnId<Row>(\n column: ColumnDefinition<Row, unknown>,\n): string {\n return column.id ?? column.field;\n}\n\n/**\n * The key state about a row is stored under, and the key it renders with — the\n * row's counterpart to `getColumnId`, and for the same reason: every such\n * lookup goes through here so no two can disagree.\n *\n * Falls back to the position so a grid given no `getRowId` still works. That\n * fallback ties a selection to where a row sits rather than to the row, so\n * anything that sorts, filters or pages its data has to supply one — otherwise\n * a re-sort leaves the same positions selected under different rows.\n */\nexport function resolveRowId<Row>(\n row: Row,\n index: number,\n getRowId?: (row: Row, index: number) => string,\n): string {\n return getRowId?.(row, index) ?? String(index);\n}\n\n/**\n * What a column's header shows: its own `headerTemplate`, called first if it is\n * a function, or else a label read off the field path — `\"Application.Id\"`\n * reading as `\"Application Id\"`.\n */\nexport function resolveColumnLabel<Row, Node>(\n column: ColumnDefinition<Row, Node>,\n): Node | string {\n const { headerTemplate } = column;\n if (headerTemplate === undefined) {\n return column.field.split(\".\").join(\" \");\n }\n // `Node` is open, so it could itself be a function type and the `typeof`\n // check cannot narrow on its own. A header that is callable is the lazy\n // form by definition — no adapter binds `Node` to a function.\n return typeof headerTemplate === \"function\"\n ? (headerTemplate as () => Node)()\n : headerTemplate;\n}\n\n/** Which of a column header's keyboard interactions to announce. */\nexport interface KeyShortcutCapabilities {\n reorderable: boolean;\n resizable: boolean;\n sortable: boolean;\n}\n\n/**\n * The `aria-keyshortcuts` string for a column header, naming only the keys\n * that column actually responds to — so the keys are discoverable, since\n * none of them is a convention a user would try unprompted.\n */\nexport function buildKeyShortcuts(\n capabilities: KeyShortcutCapabilities,\n): string {\n return [\n capabilities.reorderable ? \"Control+ArrowLeft Control+ArrowRight\" : \"\",\n // Alt+Enter is the keyboard equivalent of double-clicking the resize\n // handle to size the column to its content — same gate as the nudge\n // shortcuts, since both are resize actions.\n capabilities.resizable ? \"Alt+ArrowLeft Alt+ArrowRight Alt+Enter\" : \"\",\n capabilities.sortable ? \"Alt+ArrowUp\" : \"\",\n ]\n .filter(Boolean)\n .join(\" \");\n}\n\n/**\n * Accesses a value at a dotted path, e.g. `foo.bar.baz`.\n *\n * @param obj The object to access\n * @param path Path to the value, e.g. `\"foo.bar\"`\n * @returns The value at `path`, or `undefined` if it doesn't exist\n */\nexport function accessDotted(obj: unknown, path: string): unknown {\n return path\n .split(\".\")\n .reduce((value, key) => (isNested(value) ? value[key] : undefined), obj);\n}\n","/**\n * The header's row index. One coordinate space covers the header and the body\n * so that arrowing up out of the first row reaches the header without a case\n * of its own, and every other move is plain arithmetic.\n */\nexport const HEADER_ROW = -1;\n\n/** The cell the grid's single tab stop sits on. */\nexport interface GridFocus {\n readonly rowIndex: number;\n readonly columnIndex: number;\n}\n\n/** The modifier keys held with a navigation key press. */\nexport interface NavigationModifiers {\n readonly ctrlKey: boolean;\n readonly metaKey: boolean;\n readonly altKey: boolean;\n readonly shiftKey: boolean;\n}\n\n/**\n * Holds a coordinate inside the grid, so that a focus surviving a column\n * removal or a shorter page never leaves the grid with no tab stop at all —\n * which is a grid a keyboard cannot reach.\n */\nexport function clampFocus(\n focus: GridFocus,\n rowCount: number,\n columnCount: number,\n): GridFocus {\n return {\n rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),\n columnIndex: Math.min(\n Math.max(focus.columnIndex, 0),\n Math.max(columnCount - 1, 0),\n ),\n };\n}\n\n/**\n * The focus a navigation key press moves to, or `null` when the key isn't\n * this grid's to handle — left for the browser or another handler.\n *\n * `pageSize` is a pre-measured row count rather than a callback: this\n * function takes plain data only, so a caller measures the viewport once and\n * hands over the number.\n */\nexport function nextFocusForKey(\n key: string,\n modifiers: NavigationModifiers,\n focus: GridFocus,\n rowCount: number,\n columnCount: number,\n pageSize: number,\n): GridFocus | null {\n // Alt on an arrow resizes and Ctrl on one reorders; the header claims both\n // before this runs, and elsewhere they belong to the browser.\n if (modifiers.altKey) {\n return null;\n }\n\n const { rowIndex, columnIndex } = focus;\n\n switch (key) {\n case \"ArrowLeft\":\n if (modifiers.ctrlKey) return null;\n return { rowIndex, columnIndex: columnIndex - 1 };\n case \"ArrowRight\":\n if (modifiers.ctrlKey) return null;\n return { rowIndex, columnIndex: columnIndex + 1 };\n case \"ArrowUp\":\n return { rowIndex: rowIndex - 1, columnIndex };\n case \"ArrowDown\":\n return { rowIndex: rowIndex + 1, columnIndex };\n // Ctrl takes Home and End to the grid's ends rather than the row's, which\n // is the one place Ctrl is navigation rather than reorder.\n case \"Home\":\n return modifiers.ctrlKey\n ? { rowIndex: HEADER_ROW, columnIndex: 0 }\n : { rowIndex, columnIndex: 0 };\n case \"End\":\n return modifiers.ctrlKey\n ? { rowIndex: rowCount - 1, columnIndex: columnCount - 1 }\n : { rowIndex, columnIndex: columnCount - 1 };\n case \"PageUp\":\n return { rowIndex: rowIndex - pageSize, columnIndex };\n case \"PageDown\":\n return { rowIndex: rowIndex + pageSize, columnIndex };\n default:\n return null;\n }\n}\n","import type { ColumnDefinition, ColumnOrderState, DropSide } from \"../types\";\nimport { getColumnId } from \"./grid\";\n\n/**\n * Puts `columns` in the order `order` gives. Ids it lists come first, in that\n * order; columns it omits keep their position among the definitions and\n * follow. Ids naming no column are ignored.\n *\n * Tolerating both gaps is what lets a stored order outlive the column list it\n * was made against.\n */\nexport function applyColumnOrder<Row, Node>(\n columns: readonly ColumnDefinition<Row, Node>[],\n order: ColumnOrderState,\n): readonly ColumnDefinition<Row, Node>[] {\n if (order.length === 0) {\n return columns;\n }\n\n const byId = new Map(columns.map((column) => [getColumnId(column), column]));\n const listed = order\n .map((id) => byId.get(id))\n .filter((column) => column !== undefined);\n const taken = new Set(listed.map((column) => getColumnId(column)));\n\n return [\n ...listed,\n ...columns.filter((column) => !taken.has(getColumnId(column))),\n ];\n}\n\n/**\n * The order with `movedId` lifted out and put back in front of `beforeId`, or\n * at the end when that is `null`.\n *\n * Returns `orderedIds` itself when the move changes nothing, so callers can\n * treat an unchanged reference as \"no move happened\".\n */\nexport function moveColumnBefore(\n orderedIds: ColumnOrderState,\n movedId: string,\n beforeId: string | null,\n): ColumnOrderState {\n if (movedId === beforeId || !orderedIds.includes(movedId)) {\n return orderedIds;\n }\n\n const without = orderedIds.filter((id) => id !== movedId);\n // Taken after the removal: an index read before it would be one too far\n // right for every move that travels rightwards.\n const target = beforeId === null ? without.length : without.indexOf(beforeId);\n if (target === -1) {\n return orderedIds;\n }\n\n const next = [...without];\n next.splice(target, 0, movedId);\n return next.every((id, index) => id === orderedIds[index])\n ? orderedIds\n : next;\n}\n\n/**\n * Whether moving `movedId` in front of `beforeId` would rearrange anything, so\n * that a drop indicator promises exactly the moves that happen.\n *\n * Defined against `moveColumnBefore` rather than restating its conditions, so\n * the two cannot disagree about what a no-op is.\n */\nexport function movesColumn(\n orderedIds: ColumnOrderState,\n movedId: string,\n beforeId: string | null,\n): boolean {\n return moveColumnBefore(orderedIds, movedId, beforeId) !== orderedIds;\n}\n\n/**\n * The id a drop on `side` of `targetId` lands in front of, which is `null`\n * past the last column.\n *\n * \"After C\" and \"before D\" name the same gap, so collapsing both to one id\n * keeps a drop target a single nullable string.\n */\nexport function resolveDropBefore(\n orderedIds: ColumnOrderState,\n targetId: string,\n side: DropSide,\n): string | null {\n if (side === \"before\") {\n return targetId;\n }\n const index = orderedIds.indexOf(targetId);\n return index === -1 ? null : (orderedIds[index + 1] ?? null);\n}\n\n/**\n * The `beforeId` a keyboard nudge on `columnId` produces, for `onDrop` to\n * apply the same way a pointer drop does — or `undefined` when the nudge is a\n * no-op: `columnId` is not in `orderedIds`, or the nudge would move it left\n * of the start.\n *\n * `null` is a real answer here, not \"no-op\": it means append to the end, the\n * same convention `moveColumnBefore` uses.\n */\nexport function resolveKeyboardDropTarget(\n orderedIds: ColumnOrderState,\n columnId: string,\n direction: -1 | 1,\n): string | null | undefined {\n const index = orderedIds.indexOf(columnId);\n if (index === -1) {\n return undefined;\n }\n\n /**\n * Moving right lands in front of the id two along: the gap immediately\n * after a column is the one it already sits in.\n */\n const targetIndex = direction === -1 ? index - 1 : index + 2;\n if (targetIndex < 0) {\n return undefined;\n }\n\n return orderedIds[targetIndex] ?? null;\n}\n","import type {\n CellSelectionMode,\n CellSelectionState,\n SelectedCellRef,\n SelectIntent,\n SelectionDiff,\n SelectionMode,\n SelectionState,\n} from \"../types\";\n\n/** Nothing changed, so nothing to report — shared to save an allocation. */\nconst NO_CHANGE: SelectionDiff = { added: [], removed: [] };\n\n/**\n * Whether two selections hold the same ids in the same order.\n *\n * Order counts because the selection's own order is what makes `at(-1)` the\n * most recently selected; a comparison that ignored it would have a transform\n * return the old reference after a reordering that a consumer can observe.\n */\nfunction sameSelection(a: SelectionState, b: SelectionState): boolean {\n return a.length === b.length && a.every((id, index) => id === b[index]);\n}\n\n/**\n * The selection with `id` added if absent and removed if present — a\n * Ctrl-click, or Space on the focused member.\n *\n * The mode is enforced here rather than by each caller, so `\"single\"` cannot\n * accumulate no matter which interaction reaches it. Returns `selection`\n * itself when `mode` is `false`, keeping the unchanged-reference contract\n * `moveColumnBefore` set: a caller treats an unchanged reference as \"nothing\n * happened\" and neither renders nor reports.\n */\nexport function toggleSelection(\n selection: SelectionState,\n id: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode === false) {\n return selection;\n }\n if (selection.includes(id)) {\n return selection.filter((selectedId) => selectedId !== id);\n }\n return mode === \"single\" ? [id] : [...selection, id];\n}\n\n/**\n * The selection reduced to `id` alone — a plain click, which discards what\n * came before rather than adding to it.\n *\n * Never deselects, so clicking the one selected row leaves it selected.\n * Letting go of it is Ctrl-click's job, and `toggleSelection`'s.\n */\nexport function selectOnly(\n selection: SelectionState,\n id: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode === false) {\n return selection;\n }\n return selection.length === 1 && selection[0] === id ? selection : [id];\n}\n\n/**\n * The selection replaced by every id from `anchorId` to `focusId` inclusive — a\n * Shift-click. The two may be given in either order.\n *\n * Spans `orderedIds`, the order as displayed, rather than the selection's own:\n * the range a user drew across the screen is the range they mean, whatever\n * order they clicked its ends in. Degrades to `selectOnly` under `\"single\"`,\n * where a range has nowhere to go.\n */\nexport function selectRange(\n selection: SelectionState,\n orderedIds: readonly string[],\n anchorId: string,\n focusId: string,\n mode: SelectionMode,\n): SelectionState {\n if (mode !== \"multiple\") {\n return selectOnly(selection, focusId, mode);\n }\n\n const anchor = orderedIds.indexOf(anchorId);\n const focus = orderedIds.indexOf(focusId);\n if (anchor === -1 || focus === -1) {\n return selection;\n }\n\n const next = orderedIds.slice(\n Math.min(anchor, focus),\n Math.max(anchor, focus) + 1,\n );\n return sameSelection(selection, next) ? selection : next;\n}\n\n/**\n * Every id in `orderedIds`, or `selection` untouched under any mode but\n * `\"multiple\"` — there is no selecting all of one thing.\n */\nexport function selectAll(\n selection: SelectionState,\n orderedIds: readonly string[],\n mode: SelectionMode,\n): SelectionState {\n if (mode !== \"multiple\") {\n return selection;\n }\n return sameSelection(selection, orderedIds) ? selection : orderedIds;\n}\n\nexport function clearSelection(selection: SelectionState): SelectionState {\n return selection.length === 0 ? selection : [];\n}\n\n/**\n * What one transition added and what it removed.\n *\n * Defined here rather than in an adapter so that every callback reporting a\n * selection change reads one answer, and a select and a deselect fired for the\n * same interaction cannot disagree about which it was.\n */\nexport function diffSelection(\n previous: SelectionState,\n next: SelectionState,\n): SelectionDiff {\n if (previous === next) {\n return NO_CHANGE;\n }\n\n const before = new Set(previous);\n const after = new Set(next);\n return {\n added: next.filter((id) => !before.has(id)),\n removed: previous.filter((id) => !after.has(id)),\n };\n}\n\nexport function isSameCell(\n a: CellSelectionState,\n b: CellSelectionState,\n): boolean {\n if (a === null || b === null) {\n return a === b;\n }\n return a.rowId === b.rowId && a.columnId === b.columnId;\n}\n\n/**\n * The cell selection moved to `cell` — a plain click. Idempotent: clicking the\n * selected cell again leaves it selected, matching `selectOnly`.\n */\nexport function selectCell(\n selection: CellSelectionState,\n cell: SelectedCellRef,\n mode: CellSelectionMode,\n): CellSelectionState {\n if (mode === false || isSameCell(selection, cell)) {\n return selection;\n }\n return cell;\n}\n\n/**\n * The cell selection moved to `cell`, or cleared when it is already there — a\n * Ctrl-click, matching `toggleSelection`.\n */\nexport function toggleCellSelection(\n selection: CellSelectionState,\n cell: SelectedCellRef,\n mode: CellSelectionMode,\n): CellSelectionState {\n if (mode === false) {\n return selection;\n }\n return isSameCell(selection, cell) ? null : cell;\n}\n\n/** The intent a click or key press carries, by the modifiers held with it. */\nexport function intentOf(event: {\n ctrlKey: boolean;\n metaKey: boolean;\n shiftKey: boolean;\n}): SelectIntent {\n if (event.shiftKey) {\n return \"range\";\n }\n // Cmd on a Mac is Ctrl everywhere else, and both mean the same thing here.\n return event.ctrlKey || event.metaKey ? \"toggle\" : \"replace\";\n}\n\n/**\n * The transform an intent asks for. A function rather than a branch inside\n * each caller, so a row and a column cannot come to mean different things by\n * the same click.\n */\nexport function applySelectionIntent(\n selection: SelectionState,\n orderedIds: readonly string[],\n anchorId: string | null,\n id: string,\n intent: SelectIntent,\n mode: SelectionMode,\n): SelectionState {\n switch (intent) {\n case \"toggle\":\n return toggleSelection(selection, id, mode);\n // A range with no anchor yet spans from the id to itself, which is the\n // plain click a first Shift-click may as well be.\n case \"range\":\n return selectRange(selection, orderedIds, anchorId ?? id, id, mode);\n default:\n return selectOnly(selection, id, mode);\n }\n}\n","import type {\n ColumnConstraints,\n ColumnDefinition,\n ColumnResizeSession,\n ColumnResolveOptions,\n ColumnSizeDefaults,\n ColumnSizingState,\n ResolvedColumn,\n} from \"../types\";\nimport { alignmentForType, getColumnId, resolveColumnLabel } from \"./grid\";\n\n/**\n * Applied to any column that does not size itself. The minimum is a width a\n * header stays legible at rather than a measured value — a column dragged\n * narrower than this reads as a rendering fault.\n */\nexport const DEFAULT_COLUMN_SIZES: ColumnSizeDefaults = {\n width: 150,\n minWidth: 40,\n maxWidth: Number.POSITIVE_INFINITY,\n};\n\n/** How far one arrow key press moves a column edge. */\nexport const KEYBOARD_STEP = 10;\n\nfunction withDefaults(\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnSizeDefaults {\n return { ...DEFAULT_COLUMN_SIZES, ...defaults };\n}\n\n/**\n * The bounds `column` may be sized between. Resolved in one place so that a\n * drag, a clamp and an auto-fit cannot disagree about a column's limits.\n */\nexport function resolveColumnConstraints<Row>(\n column: ColumnDefinition<Row, unknown>,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnConstraints {\n const resolved = withDefaults(defaults);\n return {\n minWidth: column.minWidth ?? resolved.minWidth,\n maxWidth: column.maxWidth ?? resolved.maxWidth,\n };\n}\n\n/** Holds `width` within `constraints`. */\nexport function clampColumnWidth(\n width: number,\n constraints: ColumnConstraints,\n): number {\n return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);\n}\n\n/**\n * Pairs each column with everything needed to render it — the width it sits\n * at, the label its header shows, whether it may be resized and how its cells\n * align. Widths take the first of the sizing state, the column's own `width`,\n * and the default, then clamp.\n *\n * The sizing state winning means a user's drag outlives a re-render; a column\n * absent from it still tracks a `width` edited in the definition.\n */\nexport function resolveColumnWidths<Row, Node>(\n columns: readonly ColumnDefinition<Row, Node>[],\n sizing: ColumnSizingState,\n options?: ColumnResolveOptions,\n): readonly ResolvedColumn<Row, Node>[] {\n const resolved = withDefaults(options?.sizes);\n\n return columns.map((column) => {\n const id = getColumnId(column);\n const stored = sizing[id];\n\n return {\n column,\n id,\n sized: stored !== undefined,\n width: clampColumnWidth(\n stored ?? column.width ?? resolved.width,\n resolveColumnConstraints(column, resolved),\n ),\n label: resolveColumnLabel(column),\n resizable: column.resizable ?? options?.resizable ?? false,\n reorderable: column.reorderable ?? options?.reorderable ?? false,\n alignment: column.alignment ?? alignmentForType(column.type ?? \"string\"),\n };\n });\n}\n\nexport function totalColumnWidth<Row, Node>(\n resolved: readonly ResolvedColumn<Row, Node>[],\n): number {\n return resolved.reduce((total, entry) => total + entry.width, 0);\n}\n\n/**\n * Opens a resize. The session captures its constraints and starting point up\n * front, so applying a pointer position later is arithmetic on numbers alone —\n * which is what keeps the drag testable without a DOM.\n *\n * @param startPosition Where the pointer went down, on the axis being dragged.\n */\nexport function beginColumnResize<Row>(\n column: ColumnDefinition<Row, unknown>,\n startWidth: number,\n startPosition: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): ColumnResizeSession {\n return {\n columnId: getColumnId(column),\n startWidth,\n startPosition,\n constraints: resolveColumnConstraints(column, defaults),\n };\n}\n\n/**\n * The width the dragged column takes with the pointer at `position`. The delta\n * is signed, so a right-to-left adapter negates it and nothing else changes.\n */\nexport function applyColumnResize(\n session: ColumnResizeSession,\n position: number,\n): number {\n return clampColumnWidth(\n session.startWidth + (position - session.startPosition),\n session.constraints,\n );\n}\n\ninterface SizingEntry<Row, Node> {\n entry: ResolvedColumn<Row, Node>;\n width: number;\n bound: number;\n adjustable: boolean;\n}\n\n/**\n * Whether `item` has reached `bound` in the direction it moves toward — above\n * it for a column growing to a `maxWidth`, below it for one shrinking to a\n * `minWidth`.\n */\nfunction reachedBound<Row, Node>(\n width: number,\n item: SizingEntry<Row, Node>,\n): boolean {\n return item.bound > item.width ? width >= item.bound : width <= item.bound;\n}\n\n/**\n * Moves every adjustable entry's `width` toward `bound` by its proportional\n * share of `remaining`'s current surplus or deficit, looping so a column\n * clamped at its bound in one pass frees the remainder for the others to\n * absorb in the next.\n */\nfunction redistribute<Row, Node>(\n entries: readonly SizingEntry<Row, Node>[],\n remaining: (sum: number) => number,\n): void {\n for (;;) {\n const movable = entries.filter(\n (item) => item.adjustable && !reachedBound(item.width, item),\n );\n const left = remaining(entries.reduce((t, i) => t + i.width, 0));\n if (left <= 0 || movable.length === 0) {\n break;\n }\n\n const base = movable.reduce((total, item) => total + item.width, 0);\n let clamped = false;\n\n for (const item of movable) {\n const share =\n base === 0 ? left / movable.length : (left * item.width) / base;\n const next = item.width + (item.bound > item.width ? share : -share);\n\n if (reachedBound(next, item)) {\n item.width = item.bound;\n clamped = true;\n } else {\n item.width = next;\n }\n }\n\n if (!clamped) {\n break;\n }\n }\n}\n\n/**\n * Fits columns to `availableWidth`: grows them to fill leftover space, or\n * shrinks them to relieve an overflow, in both cases proportionally to each\n * column's current width. Columns the user has sized are left alone in\n * either direction, so dragging one column neither rubber-bands it back nor\n * has it absorb a squeeze meant for the rest.\n *\n * Growing stops at each column's `maxWidth`; shrinking stops at its\n * `minWidth`. If shrinking every adjustable column to its floor still does\n * not reach `availableWidth`, the columns are returned untouched and the\n * container scrolls rather than crushing them further.\n */\nexport function fitColumnsToWidth<Row, Node>(\n resolved: readonly ResolvedColumn<Row, Node>[],\n availableWidth: number,\n defaults?: Partial<ColumnSizeDefaults>,\n): readonly ResolvedColumn<Row, Node>[] {\n const target = Math.floor(availableWidth);\n const total = totalColumnWidth(resolved);\n if (resolved.length === 0 || total === target) {\n return resolved;\n }\n\n const growing = total < target;\n const entries: SizingEntry<Row, Node>[] = resolved.map((entry) => {\n const constraints = resolveColumnConstraints(entry.column, defaults);\n return {\n entry,\n width: entry.width,\n bound: growing ? constraints.maxWidth : constraints.minWidth,\n adjustable: !entry.sized,\n };\n });\n\n if (!growing) {\n // Only unsized columns can be shrunk, so if they alone cannot reach the\n // target the grid genuinely cannot fit and should scroll, unchanged.\n const floor = entries.reduce(\n (sum, item) => sum + (item.adjustable ? item.bound : item.width),\n 0,\n );\n if (floor > target) {\n return resolved;\n }\n }\n\n redistribute(entries, (sum) => (growing ? target - sum : sum - target));\n\n // Fractional widths would leave the table a sub-pixel off its container and\n // show a scrollbar that never goes away, so round every column toward the\n // target and hand the remaining pixels back out (growing) or claw them\n // back (shrinking) one at a time.\n const rounded = entries.map((item) =>\n growing ? Math.floor(item.width) : Math.ceil(item.width),\n );\n let remainder = target - rounded.reduce((total, width) => total + width, 0);\n\n return entries.map((item, index) => {\n let width = rounded[index] ?? item.width;\n // Rounding toward the target can only ever leave it short when growing\n // (floor) or over when shrinking (ceil), so only one direction is ever\n // live here — but each still needs room left before its own bound.\n const hasRoom = growing ? width < item.bound : width > item.bound;\n\n if (remainder !== 0 && item.adjustable && hasRoom) {\n width += growing ? 1 : -1;\n remainder += growing ? -1 : 1;\n }\n\n return { ...item.entry, width };\n });\n}\n\n/**\n * The width a column needs to show `measuredWidth` of content.\n *\n * `padding` is an allowance on top of the measurement, since a column sized to\n * exactly its content can still round down into an ellipsis.\n */\nexport function sizeColumnToContent(\n measuredWidth: number,\n constraints: ColumnConstraints,\n padding = 2,\n): number {\n return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);\n}\n\n/**\n * `current` with `columnId` reverted to its width in `base` — or omitted\n * entirely if `base` had none — for Escape to restore a resize in progress.\n *\n * A column with no stored width in `base` is correctly omitted by deleting\n * the key rather than merging its `base` width back in, which would\n * otherwise leave it pinned and hidden from auto-fit.\n */\nexport function revertColumnSize(\n current: ColumnSizingState,\n base: ColumnSizingState,\n columnId: string,\n): ColumnSizingState {\n const next = Object.fromEntries(\n Object.entries(current).filter(([id]) => id !== columnId),\n );\n if (columnId in base) {\n next[columnId] = base[columnId] as number;\n }\n return next;\n}\n","import type {\n ColumnSortState,\n ColumnType,\n ResolvedColumn,\n ResolvedRow,\n SortDirection,\n} from \"../types\";\nimport { accessDotted } from \"./grid\";\n\nfunction isEmpty(value: unknown): boolean {\n return value === null || value === undefined;\n}\n\n/** A date/time value converted to a comparable instant. */\nfunction toTime(value: unknown): number {\n return value instanceof Date\n ? value.getTime()\n : new Date(String(value)).getTime();\n}\n\n/**\n * Compares two cell values the way `type` implies — numerically for the\n * numeric types, chronologically for the date/time types, lexically\n * otherwise. Defaults to string comparison when `type` is absent, mirroring\n * `ColumnDefinition.type`'s own default.\n *\n * `null`/`undefined` sort after every value on both sides. Used directly,\n * that ordering is fixed; `sortRows` applies it directionally without\n * disturbing it — see its own doc comment.\n */\nexport function compareValues(\n a: unknown,\n b: unknown,\n type?: ColumnType,\n): number {\n if (isEmpty(a)) {\n return isEmpty(b) ? 0 : 1;\n }\n if (isEmpty(b)) {\n return -1;\n }\n\n switch (type) {\n case \"number\":\n case \"decimal\":\n case \"currency\":\n case \"percent\":\n return Number(a) - Number(b);\n case \"boolean\":\n return Number(Boolean(a)) - Number(Boolean(b));\n case \"date\":\n case \"dateTime\":\n case \"time\":\n return toTime(a) - toTime(b);\n default:\n return String(a).localeCompare(String(b));\n }\n}\n\n/** This column's direction in `sort`, or `null` outside it. */\nexport function sortDirectionFor(\n sort: ColumnSortState,\n columnId: string,\n): SortDirection | null {\n return sort.find((entry) => entry.columnId === columnId)?.direction ?? null;\n}\n\n/** This column's 1-based place in `sort`, or `null` outside it. */\nexport function sortPriorityFor(\n sort: ColumnSortState,\n columnId: string,\n): number | null {\n const index = sort.findIndex((entry) => entry.columnId === columnId);\n return index === -1 ? null : index + 1;\n}\n\n/**\n * The sort with `columnId` cycled to its next direction — none → asc → desc →\n * none.\n *\n * `stack: false` (a plain click) means \"sort by only this column\": unless\n * `columnId` is already the sole entry, in which case its own cycle just\n * continues, it collapses `sort` down to `columnId` alone at `\"asc\"`,\n * regardless of any direction it held as part of a larger stack.\n *\n * `stack: true` (a Shift-click) cycles or appends `columnId` in place without\n * disturbing the rest of the stack; cycling it back to `\"none\"` removes it\n * and closes the gap, leaving the others in their existing order.\n */\nexport function toggleColumnSort(\n sort: ColumnSortState,\n columnId: string,\n options: { readonly stack: boolean },\n): ColumnSortState {\n const index = sort.findIndex((entry) => entry.columnId === columnId);\n const isSoleEntry = sort.length === 1 && index === 0;\n\n if (!options.stack && !isSoleEntry) {\n return [{ columnId, direction: \"asc\" }];\n }\n\n const current = index === -1 ? undefined : sort[index]?.direction;\n const next: SortDirection | null =\n current === undefined ? \"asc\" : current === \"asc\" ? \"desc\" : null;\n\n if (next === null) {\n return sort.filter((entry) => entry.columnId !== columnId);\n }\n if (index === -1) {\n return [...sort, { columnId, direction: next }];\n }\n return sort.map((entry, i) =>\n i === index ? { columnId, direction: next } : entry,\n );\n}\n\n/**\n * `rows` ordered by `sort` — each entry read against `columns` for its\n * `field` and `type` — with `rowIndex` renumbered to match the new\n * positions, the invariant the rest of the grid relies on for keyboard\n * navigation and `aria-rowindex`.\n *\n * Stable (`Array.prototype.sort` has been stable since ES2019), so rows\n * equal under every active key keep their relative order. A `null`/\n * `undefined` value sorts last under either direction — resolved before\n * `compareValues` is asked, and left out of the direction flip below, so\n * that toggling a column reorders the values it holds rather than moving its\n * gaps to the front.\n *\n * Returns `rows` itself, untouched, when `sort` is empty or names only\n * columns absent from `columns` — a stored sort tolerates a column removed\n * from the definition the same way a stored `ColumnOrderState` does.\n */\nexport function sortRows<Row>(\n rows: readonly ResolvedRow<Row>[],\n sort: ColumnSortState,\n columns: readonly ResolvedColumn<Row, unknown>[],\n): readonly ResolvedRow<Row>[] {\n if (sort.length === 0) {\n return rows;\n }\n\n const byId = new Map(columns.map((entry) => [entry.id, entry.column]));\n const active = sort.filter((entry) => byId.has(entry.columnId));\n if (active.length === 0) {\n return rows;\n }\n\n const sorted = [...rows].sort((a, b) => {\n for (const { columnId, direction } of active) {\n const column = byId.get(columnId);\n if (column === undefined) {\n continue;\n }\n const left = accessDotted(a.row, column.field);\n const right = accessDotted(b.row, column.field);\n const leftEmpty = isEmpty(left);\n const rightEmpty = isEmpty(right);\n if (leftEmpty || rightEmpty) {\n if (leftEmpty && rightEmpty) {\n continue;\n }\n return leftEmpty ? 1 : -1;\n }\n\n const result = compareValues(left, right, column.type);\n if (result !== 0) {\n return direction === \"asc\" ? result : -result;\n }\n }\n return 0;\n });\n\n return sorted.map((row, index) =>\n row.rowIndex === index ? row : { ...row, rowIndex: index },\n );\n}\n","import type {\n ColumnDefinition,\n ColumnType,\n FieldPath,\n FilterEntry,\n FilterState,\n ResolvedColumn,\n ResolvedRow,\n TextFilterEntry,\n} from \"../types\";\nimport { accessDotted } from \"./grid\";\n\ninterface FilterPattern {\n mode: \"exact\" | \"contains\" | \"startsWith\" | \"endsWith\";\n text: string; // anchors stripped, lowercased once\n}\n\n/**\n * Reads a query's leading/trailing `%` into a match mode — see\n * `TextFilterEntry`'s own doc comment for the four shapes. Only the first\n * and last characters are read as anchors — a `%` anywhere else in the\n * query is a literal character, not a wildcard.\n */\nfunction parseFilterPattern(query: string): FilterPattern {\n const start = query.startsWith(\"%\");\n const end = query.endsWith(\"%\");\n let text = query;\n if (start) {\n text = text.slice(1);\n }\n if (end) {\n text = text.slice(0, -1);\n }\n text = text.toLowerCase();\n\n if (start && end) {\n return { mode: \"contains\", text };\n }\n if (start) {\n return { mode: \"endsWith\", text };\n }\n if (end) {\n return { mode: \"startsWith\", text };\n }\n return { mode: \"exact\", text };\n}\n\nfunction isEmptyValue(value: unknown): boolean {\n return value === null || value === undefined;\n}\n\n/**\n * `parseFilterPattern`, compiled to a reusable test — parsed once, reused\n * per row, unlike a per-call `matchesQuery`.\n */\nfunction compileFilterQuery(query: string): (value: unknown) => boolean {\n const { mode, text } = parseFilterPattern(query);\n return (value: unknown) => {\n const candidate = (isEmptyValue(value) ? \"\" : String(value)).toLowerCase();\n switch (mode) {\n case \"exact\":\n return candidate === text;\n case \"contains\":\n return candidate.includes(text);\n case \"startsWith\":\n return candidate.startsWith(text);\n case \"endsWith\":\n return candidate.endsWith(text);\n }\n };\n}\n\n/**\n * Whether `target` (a `ValueFilterEntry`'s value) equals `value` under\n * `type` — `false` whenever `type` doesn't agree with `target`'s own kind,\n * so a number filter never silently matches a string column.\n */\nfunction matchesTypedValue(\n value: unknown,\n target: number | boolean | Date,\n type: ColumnType | undefined,\n): boolean {\n if (typeof target === \"number\") {\n if (\n type !== \"number\" &&\n type !== \"decimal\" &&\n type !== \"currency\" &&\n type !== \"percent\"\n ) {\n return false;\n }\n return typeof value === \"number\" && value === target;\n }\n if (typeof target === \"boolean\") {\n if (type !== \"boolean\") {\n return false;\n }\n return typeof value === \"boolean\" && value === target;\n }\n if (type !== \"date\" && type !== \"dateTime\" && type !== \"time\") {\n return false;\n }\n return value instanceof Date && value.getTime() === target.getTime();\n}\n\n/**\n * Whether a non-group entry names a column present in `byId` (or names\n * none at all) — lets a stored filter tolerate a column removed from the\n * definition the same way a stored `ColumnSortState` does, by dropping only\n * the entries that reference it.\n *\n * A `GroupFilterEntry` is always usable, regardless of its own contents:\n * its vacuous truth value under an empty or fully-unusable `entries` list\n * depends on its own `combinator` (`\"and\"` vacuously matches everything,\n * `\"or\"` vacuously matches nothing) — a group can't be dropped from its\n * parent's list the way an unusable simple entry can, since whether\n * dropping it would be safe depends on the *parent's* combinator too,\n * which this check has no way to know. Compiling it and letting `.every()`/\n * `.some()` resolve the vacuous case naturally is what stays correct in\n * every context; deciding upfront here is not.\n */\nfunction isEntryUsable<Row>(\n entry: FilterEntry<Row>,\n byId: ReadonlyMap<string, ColumnDefinition<Row, unknown>>,\n): boolean {\n if (\"combinator\" in entry) {\n return true;\n }\n return entry.columnId === undefined || byId.has(entry.columnId);\n}\n\n/**\n * One compiled test per usable `FilterEntry`, closing over its column\n * lookup so `filterRows`'s row loop is just `.every(test)`.\n */\nfunction compileFilterEntry<Row>(\n entry: FilterEntry<Row>,\n byId: ReadonlyMap<string, ColumnDefinition<Row, unknown>>,\n columns: readonly ResolvedColumn<Row, unknown>[],\n): (row: ResolvedRow<Row>) => boolean {\n if (\"combinator\" in entry) {\n const tests = entry.entries\n .filter((nested) => isEntryUsable(nested, byId))\n .map((nested) => compileFilterEntry(nested, byId, columns));\n return entry.combinator === \"and\"\n ? (row) => tests.every((test) => test(row))\n : (row) => tests.some((test) => test(row));\n }\n\n if (\"predicate\" in entry) {\n const { columnId, predicate } = entry;\n const column = columnId === undefined ? undefined : byId.get(columnId);\n return (row) =>\n predicate(\n column === undefined ? undefined : accessDotted(row.row, column.field),\n row.row,\n );\n }\n\n if (\"value\" in entry) {\n const { columnId, value } = entry;\n if (columnId !== undefined) {\n const column = byId.get(columnId);\n if (column === undefined) {\n return () => false;\n }\n return (row) =>\n matchesTypedValue(\n accessDotted(row.row, column.field),\n value,\n column.type,\n );\n }\n return (row) =>\n columns.some((entry) =>\n matchesTypedValue(\n accessDotted(row.row, entry.column.field),\n value,\n entry.column.type,\n ),\n );\n }\n\n const { columnId, query } = entry;\n const test = compileFilterQuery(query);\n if (columnId !== undefined) {\n const column = byId.get(columnId);\n if (column === undefined) {\n return () => false;\n }\n return (row) => test(accessDotted(row.row, column.field));\n }\n return (row) =>\n columns.some((entry) => test(accessDotted(row.row, entry.column.field)));\n}\n\n/**\n * Whether `value`, stringified, matches `query`'s SQL LIKE-style pattern —\n * case-insensitive always. A bare query is an exact match; `%text%` is\n * contains; `text%` is starts-with; `%text` is ends-with. `null`/`undefined`\n * stringify to `\"\"`.\n *\n * A one-shot convenience: parses `query` fresh on every call. `filterRows`\n * does not call this per cell — it compiles each `TextFilterEntry` once,\n * outside its row loop.\n */\nexport function matchesQuery(value: unknown, query: string): boolean {\n return compileFilterQuery(query)(value);\n}\n\n/**\n * This column's current text query, or the global entry's when `columnId`\n * is omitted; `null` if no `TextFilterEntry` names it — including when the\n * entry that exists for that key is a `ValueFilterEntry`/\n * `PredicateFilterEntry`/`GroupFilterEntry` instead, since none of those\n * hold a query string to report.\n */\nexport function filterQueryFor<Row>(\n filter: FilterState<Row>,\n columnId?: FieldPath<Row> | (string & {}),\n): string | null {\n const entry = filter.find(\n (candidate): candidate is TextFilterEntry<Row> =>\n \"query\" in candidate && candidate.columnId === columnId,\n );\n return entry?.query ?? null;\n}\n\n/**\n * The filter with the `TextFilterEntry` for `columnId` (or the global\n * entry, when `columnId` is omitted) set to `query` — replacing whatever\n * entry already holds that key, of any variant, if present; appending a new\n * `TextFilterEntry` if not. An empty `query` removes the entry instead of\n * storing a filter that can never usefully match.\n *\n * Returns `filter` itself, unchanged, when the result would be a no-op: an\n * empty `query` naming a key with no existing entry, or a `query` equal to\n * an existing `TextFilterEntry`'s.\n */\nexport function setColumnFilter<Row>(\n filter: FilterState<Row>,\n columnId: (FieldPath<Row> | (string & {})) | undefined,\n query: string,\n): FilterState<Row> {\n const index = filter.findIndex(\n (entry) => !(\"combinator\" in entry) && entry.columnId === columnId,\n );\n\n if (query === \"\") {\n return index === -1 ? filter : filter.filter((_entry, i) => i !== index);\n }\n\n const next: TextFilterEntry<Row> =\n columnId === undefined ? { query } : { columnId, query };\n\n if (index === -1) {\n return [...filter, next];\n }\n const existing = filter[index];\n if (\n existing !== undefined &&\n \"query\" in existing &&\n existing.query === query\n ) {\n return filter;\n }\n return filter.map((entry, i) => (i === index ? next : entry));\n}\n\n/** `filter` reduced to empty. Returns `filter` itself when already empty. */\nexport function clearAllFilters<Row>(\n filter: FilterState<Row>,\n): FilterState<Row> {\n return filter.length === 0 ? filter : [];\n}\n\n/**\n * `rows` reduced to those matching every entry in `filter` (AND at the top\n * level — nest a `GroupFilterEntry` for OR). `rowIndex` is renumbered to\n * match the retained positions — the same invariant `sortRows` keeps,\n * needed here independently since `filterRows` runs before `sortRows` in\n * the composed pipeline (see `resolveShownRows`).\n *\n * Order-preserving: surviving rows keep their relative order, which is what\n * makes running this before a sort produce the identical final order a\n * sort-then-filter would — only the cost differs.\n *\n * Every entry is compiled once, before the row loop.\n *\n * Returns `rows` itself, untouched, when `filter` is empty or names only\n * columns absent from `columns` — a stored filter tolerates a column\n * removed from the definition the same way a stored `ColumnSortState` does.\n */\nexport function filterRows<Row>(\n rows: readonly ResolvedRow<Row>[],\n filter: FilterState<Row>,\n columns: readonly ResolvedColumn<Row, unknown>[],\n): readonly ResolvedRow<Row>[] {\n if (filter.length === 0) {\n return rows;\n }\n\n const byId = new Map(columns.map((entry) => [entry.id, entry.column]));\n const active = filter.filter((entry) => isEntryUsable(entry, byId));\n if (active.length === 0) {\n return rows;\n }\n\n const compiled = active.map((entry) =>\n compileFilterEntry(entry, byId, columns),\n );\n const filtered = rows.filter((row) => compiled.every((test) => test(row)));\n\n return filtered.map((row, index) =>\n row.rowIndex === index ? row : { ...row, rowIndex: index },\n );\n}\n","import type {\n ColumnSortState,\n FilterState,\n ResolvedColumn,\n ResolvedRow,\n SelectedCell,\n SelectedCellRef,\n SelectedColumn,\n} from \"../types\";\nimport { accessDotted } from \"./grid\";\nimport { filterRows } from \"./filtering\";\nimport { sortRows } from \"./sorting\";\n\n/**\n * `rows`, filtered then sorted — the one function `DataGrid.tsx` (and any\n * future framework binding) calls instead of composing `filterRows`/\n * `sortRows` itself.\n *\n * Filter always runs first: an O(n) pass that shrinks what the O(n log n)\n * sort has to touch whenever a filter is active, free when it isn't. This\n * is a performance choice, not a correctness one — filtering is an\n * order-preserving predicate and sort is a stable total order over whatever\n * it's handed, so the final order is identical either way.\n */\nexport function resolveShownRows<Row>(\n rows: readonly ResolvedRow<Row>[],\n filter: FilterState<Row>,\n sort: ColumnSortState,\n columns: readonly ResolvedColumn<Row, unknown>[],\n): readonly ResolvedRow<Row>[] {\n return sortRows(filterRows(rows, filter, columns), sort, columns);\n}\n\n/**\n * `ids` resolved back to the rows behind them, keyed by `rowsById`.\n *\n * An id with no row behind it is dropped rather than left `undefined`: a\n * selection outlives a change to the data, so that filtering a row out and\n * back leaves it selected, and only what still exists can be handed to a\n * callback.\n */\nexport function resolveRows<Row>(\n rowsById: ReadonlyMap<string, ResolvedRow<Row>>,\n ids: readonly string[],\n): readonly ResolvedRow<Row>[] {\n return ids.map((id) => rowsById.get(id)).filter((row) => row !== undefined);\n}\n\n/**\n * `ids` resolved back to the columns behind them, paired with the position\n * each holds in `columns` — a stale id is dropped, matching `resolveRows`.\n */\nexport function resolveColumns<Row, Node>(\n columns: readonly ResolvedColumn<Row, Node>[],\n ids: readonly string[],\n): readonly SelectedColumn<Row, Node>[] {\n return ids\n .map((id) => {\n const columnIndex = columns.findIndex((entry) => entry.id === id);\n const column = columns[columnIndex];\n return column === undefined\n ? undefined\n : { columnId: id, column, columnIndex };\n })\n .filter((column) => column !== undefined);\n}\n\n/**\n * `cell` resolved down to the row, column and value it addresses — `null`\n * when there is no cell, or when either half of it names a row or column\n * that no longer exists.\n */\nexport function resolveCell<Row, Node>(\n rowsById: ReadonlyMap<string, ResolvedRow<Row>>,\n columns: readonly ResolvedColumn<Row, Node>[],\n cell: SelectedCellRef | null,\n): SelectedCell<Row, Node> | null {\n if (cell === null) {\n return null;\n }\n const entry = rowsById.get(cell.rowId);\n const columnIndex = columns.findIndex(\n (candidate) => candidate.id === cell.columnId,\n );\n const column = columns[columnIndex];\n if (entry === undefined || column === undefined) {\n return null;\n }\n return {\n rowId: cell.rowId,\n columnId: cell.columnId,\n row: entry.row,\n column,\n rowIndex: entry.rowIndex,\n columnIndex,\n value: accessDotted(entry.row, column.column.field),\n };\n}\n"],"mappings":"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","names":["total","test","columnId","entry"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gridkitjs/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Framework-agnostic data-grid logic: column resolution, alignment and sizing.",
5
5
  "license": "MIT",
6
6
  "author": "Blagoja Blazhevski",