@tanstack/table-core 9.0.0-beta.56 → 9.0.0-beta.58

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.
Files changed (45) hide show
  1. package/dist/core/row-models/createCoreRowModel.js +5 -1
  2. package/dist/features/cell-selection/cellSelectionFeature.d.ts +9 -0
  3. package/dist/features/cell-selection/cellSelectionFeature.js +117 -0
  4. package/dist/features/cell-selection/cellSelectionFeature.types.d.ts +273 -0
  5. package/dist/features/cell-selection/cellSelectionFeature.utils.d.ts +288 -0
  6. package/dist/features/cell-selection/cellSelectionFeature.utils.js +732 -0
  7. package/dist/features/row-aggregation/rowAggregationFeature.types.d.ts +7 -7
  8. package/dist/features/stockFeatures.d.ts +3 -1
  9. package/dist/features/stockFeatures.js +3 -1
  10. package/dist/index.d.ts +4 -2
  11. package/dist/index.js +2 -1
  12. package/dist/static-functions.d.ts +2 -1
  13. package/dist/static-functions.js +2 -1
  14. package/dist/types/Cell.d.ts +4 -2
  15. package/dist/types/Column.d.ts +2 -2
  16. package/dist/types/ColumnDef.d.ts +10 -8
  17. package/dist/types/Row.d.ts +2 -2
  18. package/dist/types/RowModelFns.d.ts +3 -3
  19. package/dist/types/Table.d.ts +3 -1
  20. package/dist/types/TableOptions.d.ts +6 -4
  21. package/dist/types/TableState.d.ts +3 -1
  22. package/package.json +1 -1
  23. package/skills/aggregation/SKILL.md +2 -2
  24. package/skills/api-not-found/SKILL.md +1 -1
  25. package/skills/cell-selection/SKILL.md +180 -0
  26. package/skills/client-vs-server/SKILL.md +1 -1
  27. package/skills/column-faceting/SKILL.md +1 -1
  28. package/skills/column-filtering/SKILL.md +1 -1
  29. package/skills/column-ordering/SKILL.md +1 -1
  30. package/skills/column-pinning/SKILL.md +1 -1
  31. package/skills/column-resizing/SKILL.md +1 -1
  32. package/skills/column-sizing/SKILL.md +1 -1
  33. package/skills/column-visibility/SKILL.md +1 -1
  34. package/skills/core/SKILL.md +1 -1
  35. package/skills/custom-features/SKILL.md +1 -1
  36. package/skills/expanding/SKILL.md +1 -1
  37. package/skills/global-filtering/SKILL.md +1 -1
  38. package/skills/grouping/SKILL.md +1 -1
  39. package/skills/migrate-v8-to-v9/SKILL.md +1 -1
  40. package/skills/pagination/SKILL.md +1 -1
  41. package/skills/row-pinning/SKILL.md +1 -1
  42. package/skills/row-selection/SKILL.md +1 -1
  43. package/skills/sorting/SKILL.md +1 -1
  44. package/skills/table-features/SKILL.md +1 -1
  45. package/skills/typescript/SKILL.md +1 -1
@@ -0,0 +1,732 @@
1
+ import { callMemoOrStaticFn, cloneState, makeObjectMap } from "../../utils.js";
2
+ import { table_getVisibleLeafColumns } from "../column-visibility/columnVisibilityFeature.utils.js";
3
+
4
+ //#region src/features/cell-selection/cellSelectionFeature.utils.ts
5
+ /**
6
+ * Creates the default cell selection state.
7
+ *
8
+ * The feature default is an empty selection. Reset APIs use this value when
9
+ * `defaultState` is `true`.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const selection = getDefaultCellSelectionState()
14
+ * ```
15
+ */
16
+ function getDefaultCellSelectionState() {
17
+ return [];
18
+ }
19
+ /**
20
+ * Routes a cell selection updater through the table's selection change handler.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * table_setCellSelection(table, (old) => old.slice(0, -1))
25
+ * ```
26
+ */
27
+ function table_setCellSelection(table, updater) {
28
+ table.options.onCellSelectionChange?.(updater);
29
+ }
30
+ /**
31
+ * Resets `cellSelection` to the configured initial state or feature default.
32
+ *
33
+ * With no argument, the reset clones `table.initialState.cellSelection` when it
34
+ * exists. Passing `true` ignores initial state and resets to an empty selection.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * table_resetCellSelection(table, true)
39
+ * ```
40
+ */
41
+ function table_resetCellSelection(table, defaultState) {
42
+ table_setCellSelection(table, defaultState ? getDefaultCellSelectionState() : cloneState(table.initialState.cellSelection) ?? getDefaultCellSelectionState());
43
+ }
44
+ /**
45
+ * Schedules a cell selection reset after `data` changes.
46
+ *
47
+ * Ranges are stored as row and column ids, so without this a data swap would
48
+ * leave a selection pointing at rows that no longer exist, or silently
49
+ * re-select cells whenever new data reuses ids. The reset runs when
50
+ * `autoResetAll` or `autoResetCellSelection` allows it, defaulting to on.
51
+ *
52
+ * Resetting to `initialState.cellSelection` rather than to empty means the
53
+ * first row-model computation is a no-op, matching `table_autoResetExpanded`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * table_autoResetCellSelection(table)
58
+ * ```
59
+ */
60
+ function table_autoResetCellSelection(table) {
61
+ if (!table.atoms.cellSelection) return;
62
+ if (table.options.autoResetAll ?? table.options.autoResetCellSelection ?? true) table._reactivity.schedule(() => table_resetCellSelection(table));
63
+ }
64
+ /**
65
+ * Returns the visible leaf columns in the order their cells actually render.
66
+ *
67
+ * This is deliberately not `getVisibleLeafColumns()`, which is
68
+ * visibility-filtered but *not* pinning-reordered, and not `column_getIndex()`,
69
+ * which indexes that same unpinned list. Cells render start-pinned first, then
70
+ * center, then end (see `row_getVisibleCells`), so indexing a selection in the
71
+ * unpinned order would make a dragged rectangle contiguous in index space but
72
+ * visually scattered the moment a column is pinned.
73
+ *
74
+ * The pinning read is inlined rather than delegated to the column pinning
75
+ * utils so this stays correct when that feature is absent, and so the ordering
76
+ * provably matches `row_getVisibleCells`.
77
+ */
78
+ function getDisplayOrderedColumns(table) {
79
+ const columns = callMemoOrStaticFn(table, "getVisibleLeafColumns", table_getVisibleLeafColumns);
80
+ const pinning = table.atoms.columnPinning?.get();
81
+ if (!pinning || !pinning.start.length && !pinning.end.length) return columns;
82
+ const byId = makeObjectMap();
83
+ for (let i = 0; i < columns.length; i++) byId[columns[i].id] = columns[i];
84
+ const start = [];
85
+ for (let i = 0; i < pinning.start.length; i++) {
86
+ const column = byId[pinning.start[i]];
87
+ if (column) start.push(column);
88
+ }
89
+ const end = [];
90
+ for (let i = 0; i < pinning.end.length; i++) {
91
+ const column = byId[pinning.end[i]];
92
+ if (column) end.push(column);
93
+ }
94
+ const center = [];
95
+ for (let i = 0; i < columns.length; i++) {
96
+ const column = columns[i];
97
+ if (!pinning.start.includes(column.id) && !pinning.end.includes(column.id)) center.push(column);
98
+ }
99
+ return [
100
+ ...start,
101
+ ...center,
102
+ ...end
103
+ ];
104
+ }
105
+ /**
106
+ * Builds a column id to render-order index map.
107
+ *
108
+ * Registered by this feature so the lookup stays memoized even when
109
+ * `columnOrderingFeature` is absent, since that feature's `getColumnIndexes`
110
+ * static rebuilds all four maps on every call, which would make per-cell reads
111
+ * O(columns).
112
+ *
113
+ * @example
114
+ * ```ts
115
+ * const index = table_getCellSelectionColumnIndexes(table)[columnId]
116
+ * ```
117
+ */
118
+ function table_getCellSelectionColumnIndexes(table) {
119
+ const columns = getDisplayOrderedColumns(table);
120
+ const indexes = makeObjectMap();
121
+ for (let i = 0; i < columns.length; i++) indexes[columns[i].id] = i;
122
+ return indexes;
123
+ }
124
+ /**
125
+ * Resolves a row id to its display-order index, or `-1` when it no longer
126
+ * identifies a row in the current order.
127
+ *
128
+ * Callers must have already called `table.getRowsInDisplayOrder()`, which is
129
+ * what populates the display index cache each row reads.
130
+ */
131
+ function resolveRowIndex(table, rows, rowId) {
132
+ const row = table.getPrePaginatedRowModel().rowsById[rowId] ?? table.getCoreRowModel().rowsById[rowId];
133
+ if (!row) return -1;
134
+ const index = row.getDisplayIndex();
135
+ if (index < 0 || index >= rows.length || rows[index]?.id !== rowId) return -1;
136
+ return index;
137
+ }
138
+ /**
139
+ * Resolves the selected ranges into inclusive display-order index rectangles.
140
+ *
141
+ * This is the single cache every per-cell read goes through, so index lookups
142
+ * happen once per invalidation rather than once per cell. A range whose corners
143
+ * no longer resolve, for example because its anchor row was filtered out, is
144
+ * omitted rather than clamped, so it contributes nothing while remaining in
145
+ * state and returns intact when the filter clears.
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * const bounds = table_getCellSelectionBounds(table)
150
+ * ```
151
+ */
152
+ function table_getCellSelectionBounds(table) {
153
+ const ranges = table.atoms.cellSelection?.get();
154
+ if (!ranges?.length) return [];
155
+ const rows = table.getRowsInDisplayOrder();
156
+ const columnIndexes = callMemoOrStaticFn(table, "getCellSelectionColumnIndexes", table_getCellSelectionColumnIndexes);
157
+ const bounds = [];
158
+ for (let i = 0; i < ranges.length; i++) {
159
+ const range = ranges[i];
160
+ const anchorRowIndex = resolveRowIndex(table, rows, range.anchorRowId);
161
+ const focusRowIndex = resolveRowIndex(table, rows, range.focusRowId);
162
+ const anchorColumnIndex = columnIndexes[range.anchorColumnId] ?? -1;
163
+ const focusColumnIndex = columnIndexes[range.focusColumnId] ?? -1;
164
+ if (anchorRowIndex < 0 || focusRowIndex < 0 || anchorColumnIndex < 0 || focusColumnIndex < 0) continue;
165
+ bounds.push({
166
+ minRowIndex: Math.min(anchorRowIndex, focusRowIndex),
167
+ maxRowIndex: Math.max(anchorRowIndex, focusRowIndex),
168
+ minColumnIndex: Math.min(anchorColumnIndex, focusColumnIndex),
169
+ maxColumnIndex: Math.max(anchorColumnIndex, focusColumnIndex)
170
+ });
171
+ }
172
+ return bounds;
173
+ }
174
+ /**
175
+ * Tests whether an index pair falls inside any resolved rectangle.
176
+ */
177
+ function isWithinBounds(bounds, rowIndex, columnIndex) {
178
+ for (let i = 0; i < bounds.length; i++) {
179
+ const bound = bounds[i];
180
+ if (rowIndex >= bound.minRowIndex && rowIndex <= bound.maxRowIndex && columnIndex >= bound.minColumnIndex && columnIndex <= bound.maxColumnIndex) return true;
181
+ }
182
+ return false;
183
+ }
184
+ /**
185
+ * Checks whether this cell can currently be selected.
186
+ *
187
+ * A column def opting out with `enableCellSelection: false` wins over the table
188
+ * option, matching how the other per-column enable flags resolve.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * const canSelect = cell_getCanSelect(cell)
193
+ * ```
194
+ */
195
+ function cell_getCanSelect(cell) {
196
+ if (cell.column.columnDef.enableCellSelection === false) return false;
197
+ const enabled = cell.table.options.enableCellSelection;
198
+ if (typeof enabled === "function") return enabled(cell);
199
+ return enabled ?? true;
200
+ }
201
+ /**
202
+ * Resolves a cell to the coordinates every selection read needs.
203
+ *
204
+ * Shared by `getIsSelected` and `getSelectionEdges` so a render pass resolves
205
+ * each cell once. Resolving in both meant every cell paid for the bounds memo,
206
+ * the display index, and the column index map twice over.
207
+ *
208
+ * Returns `null` when the cell cannot participate in a selection at all.
209
+ */
210
+ function resolveCellPosition(cell) {
211
+ const table = cell.table;
212
+ const bounds = callMemoOrStaticFn(table, "getCellSelectionBounds", table_getCellSelectionBounds);
213
+ if (!bounds.length) return null;
214
+ if (!callMemoOrStaticFn(cell, "getCanSelect", cell_getCanSelect)) return null;
215
+ const rowIndex = cell.row.getDisplayIndex();
216
+ if (rowIndex < 0) return null;
217
+ const columnIndex = callMemoOrStaticFn(table, "getCellSelectionColumnIndexes", table_getCellSelectionColumnIndexes)[cell.column.id] ?? -1;
218
+ if (columnIndex < 0) return null;
219
+ return {
220
+ bounds,
221
+ rowIndex,
222
+ columnIndex
223
+ };
224
+ }
225
+ /**
226
+ * Checks whether this cell falls inside any selected range.
227
+ *
228
+ * Deliberately not memoized. Registering this through `assignPrototypeAPIs`
229
+ * with `memoDeps` would allocate a memo closure and dependency array per cell,
230
+ * which costs more than the handful of integer comparisons it would save.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const isSelected = cell_getIsSelected(cell)
235
+ * ```
236
+ */
237
+ function cell_getIsSelected(cell) {
238
+ const position = resolveCellPosition(cell);
239
+ if (!position) return false;
240
+ return isWithinBounds(position.bounds, position.rowIndex, position.columnIndex);
241
+ }
242
+ /**
243
+ * Checks whether this cell is the active cell.
244
+ *
245
+ * @example
246
+ * ```ts
247
+ * const isFocused = cell_getIsFocused(cell)
248
+ * ```
249
+ */
250
+ function cell_getIsFocused(cell) {
251
+ const ranges = cell.table.atoms.cellSelection?.get();
252
+ const active = ranges?.[ranges.length - 1];
253
+ if (!active) return false;
254
+ return active.anchorRowId === cell.row.id && active.anchorColumnId === cell.column.id;
255
+ }
256
+ /**
257
+ * Returns `0` for the focused cell and `-1` otherwise, for roving tabindex.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * const tabIndex = cell_getTabIndex(cell)
262
+ * ```
263
+ */
264
+ function cell_getTabIndex(cell) {
265
+ return callMemoOrStaticFn(cell, "getIsFocused", cell_getIsFocused) ? 0 : -1;
266
+ }
267
+ /**
268
+ * Returns which sides of this cell sit on the outer boundary of the selection.
269
+ *
270
+ * A side is an edge when the neighbouring cell in that direction is not itself
271
+ * covered by a range, which is what lets a consumer draw a single outline
272
+ * around an arbitrary union of rectangles.
273
+ *
274
+ * @example
275
+ * ```ts
276
+ * const { top, right, bottom, left } = cell_getSelectionEdges(cell)
277
+ * ```
278
+ */
279
+ function cell_getSelectionEdges(cell) {
280
+ const none = {
281
+ top: false,
282
+ right: false,
283
+ bottom: false,
284
+ left: false
285
+ };
286
+ const position = resolveCellPosition(cell);
287
+ if (!position) return none;
288
+ const { bounds, rowIndex, columnIndex } = position;
289
+ if (!isWithinBounds(bounds, rowIndex, columnIndex)) return none;
290
+ return {
291
+ top: !isWithinBounds(bounds, rowIndex - 1, columnIndex),
292
+ right: !isWithinBounds(bounds, rowIndex, columnIndex + 1),
293
+ bottom: !isWithinBounds(bounds, rowIndex + 1, columnIndex),
294
+ left: !isWithinBounds(bounds, rowIndex, columnIndex - 1)
295
+ };
296
+ }
297
+ /**
298
+ * Returns the active cell, i.e. the anchor of the most recent range.
299
+ *
300
+ * Focus is derived rather than stored: in spreadsheet semantics, dragging from
301
+ * A1 to C5 leaves the active cell at A1, so the active range's anchor already
302
+ * is the active cell.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * const cell = table_getFocusedCell(table)
307
+ * ```
308
+ */
309
+ function table_getFocusedCell(table) {
310
+ const ranges = table.atoms.cellSelection?.get();
311
+ const active = ranges?.[ranges.length - 1];
312
+ if (!active) return void 0;
313
+ return (table.getPrePaginatedRowModel().rowsById[active.anchorRowId] ?? table.getCoreRowModel().rowsById[active.anchorRowId])?.getAllCellsByColumnId()[active.anchorColumnId];
314
+ }
315
+ /**
316
+ * Collapses the selection to a single cell at the given coordinates.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * table_setFocusedCell(table, '3', 'firstName')
321
+ * ```
322
+ */
323
+ function table_setFocusedCell(table, rowId, columnId) {
324
+ table_selectCellRange(table, {
325
+ anchorRowId: rowId,
326
+ anchorColumnId: columnId,
327
+ focusRowId: rowId,
328
+ focusColumnId: columnId
329
+ });
330
+ }
331
+ /**
332
+ * Selects a rectangle, replacing the current selection unless `additive`.
333
+ *
334
+ * @example
335
+ * ```ts
336
+ * table_selectCellRange(table, range, { additive: true })
337
+ * ```
338
+ */
339
+ function table_selectCellRange(table, range, opts) {
340
+ table_setCellSelection(table, (old) => opts?.additive ? [...old, range] : [range]);
341
+ }
342
+ /**
343
+ * Returns the visible leaf columns that permit selection, in display order.
344
+ *
345
+ * A column-level opt-out is enough to exclude a column here; a per-cell
346
+ * predicate is not consulted, since navigation and select-all work in column
347
+ * space rather than cell space.
348
+ */
349
+ function getSelectableColumns(table) {
350
+ const columns = getDisplayOrderedColumns(table);
351
+ if (table.options.enableCellSelection === false) return [];
352
+ return columns.filter((column) => column.columnDef.enableCellSelection !== false);
353
+ }
354
+ /**
355
+ * Selects every selectable cell in the table as one range.
356
+ *
357
+ * @example
358
+ * ```ts
359
+ * table_selectAllCells(table)
360
+ * ```
361
+ */
362
+ function table_selectAllCells(table) {
363
+ const rows = table.getRowsInDisplayOrder();
364
+ const columns = getSelectableColumns(table);
365
+ if (!rows.length || !columns.length) return;
366
+ table_selectCellRange(table, {
367
+ anchorRowId: rows[0].id,
368
+ anchorColumnId: columns[0].id,
369
+ focusRowId: rows[rows.length - 1].id,
370
+ focusColumnId: columns[columns.length - 1].id
371
+ });
372
+ }
373
+ /**
374
+ * Resolves a direction into row and column deltas.
375
+ */
376
+ function getDirectionDelta(direction) {
377
+ switch (direction) {
378
+ case "up": return {
379
+ rowDelta: -1,
380
+ columnDelta: 0
381
+ };
382
+ case "down": return {
383
+ rowDelta: 1,
384
+ columnDelta: 0
385
+ };
386
+ case "left": return {
387
+ rowDelta: 0,
388
+ columnDelta: -1
389
+ };
390
+ default: return {
391
+ rowDelta: 0,
392
+ columnDelta: 1
393
+ };
394
+ }
395
+ }
396
+ /**
397
+ * Steps one cell in a direction from a starting coordinate.
398
+ *
399
+ * Columns that cannot be selected are skipped over rather than landed on, so
400
+ * arrow navigation never parks on an opted-out column. Returns `null` when the
401
+ * step would leave the grid or find no selectable column.
402
+ */
403
+ function stepCoordinate(table, rowId, columnId, direction) {
404
+ const rows = table.getRowsInDisplayOrder();
405
+ const columns = getDisplayOrderedColumns(table);
406
+ if (!rows.length || !columns.length) return null;
407
+ const { rowDelta, columnDelta } = getDirectionDelta(direction);
408
+ const rowIndex = resolveRowIndex(table, rows, rowId);
409
+ const columnIndex = columns.findIndex((column) => column.id === columnId);
410
+ if (rowIndex < 0 || columnIndex < 0) return null;
411
+ const nextRowIndex = rowIndex + rowDelta;
412
+ if (nextRowIndex < 0 || nextRowIndex >= rows.length) return null;
413
+ const selectableColumnIds = new Set(getSelectableColumns(table).map((column) => column.id));
414
+ if (!selectableColumnIds.size) return null;
415
+ let nextColumnIndex = columnIndex;
416
+ if (columnDelta) do
417
+ nextColumnIndex += columnDelta;
418
+ while (nextColumnIndex >= 0 && nextColumnIndex < columns.length && !selectableColumnIds.has(columns[nextColumnIndex].id));
419
+ else if (!selectableColumnIds.has(columnId)) for (let distance = 1; distance < columns.length; distance++) {
420
+ const before = columns[columnIndex - distance];
421
+ const after = columns[columnIndex + distance];
422
+ if (before && selectableColumnIds.has(before.id)) {
423
+ nextColumnIndex = columnIndex - distance;
424
+ break;
425
+ }
426
+ if (after && selectableColumnIds.has(after.id)) {
427
+ nextColumnIndex = columnIndex + distance;
428
+ break;
429
+ }
430
+ }
431
+ if (nextColumnIndex < 0 || nextColumnIndex >= columns.length || !selectableColumnIds.has(columns[nextColumnIndex].id)) return null;
432
+ return {
433
+ rowId: rows[nextRowIndex].id,
434
+ columnId: columns[nextColumnIndex].id
435
+ };
436
+ }
437
+ /**
438
+ * Moves the selection one step in a direction, collapsing it to a single cell.
439
+ *
440
+ * With nothing selected, this selects the first selectable cell so keyboard
441
+ * navigation has somewhere to start.
442
+ *
443
+ * @example
444
+ * ```ts
445
+ * table_moveCellSelection(table, 'down')
446
+ * ```
447
+ */
448
+ function table_moveCellSelection(table, direction) {
449
+ const ranges = table.atoms.cellSelection?.get();
450
+ const active = ranges?.[ranges.length - 1];
451
+ if (!active) {
452
+ const rows = table.getRowsInDisplayOrder();
453
+ const columns = getSelectableColumns(table);
454
+ if (!rows.length || !columns.length) return;
455
+ table_setFocusedCell(table, rows[0].id, columns[0].id);
456
+ return;
457
+ }
458
+ const next = stepCoordinate(table, active.anchorRowId, active.anchorColumnId, direction);
459
+ if (!next) return;
460
+ table_setFocusedCell(table, next.rowId, next.columnId);
461
+ }
462
+ /**
463
+ * Extends the active range one step in a direction, keeping its anchor fixed.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * table_extendCellSelection(table, 'right')
468
+ * ```
469
+ */
470
+ function table_extendCellSelection(table, direction) {
471
+ const ranges = table.atoms.cellSelection?.get();
472
+ const active = ranges?.[ranges.length - 1];
473
+ if (!active) {
474
+ table_moveCellSelection(table, direction);
475
+ return;
476
+ }
477
+ const next = stepCoordinate(table, active.focusRowId, active.focusColumnId, direction);
478
+ if (!next) return;
479
+ table_setCellSelection(table, (old) => {
480
+ if (!old.length) return old;
481
+ const nextRanges = old.slice(0, -1);
482
+ nextRanges.push({
483
+ ...old[old.length - 1],
484
+ focusRowId: next.rowId,
485
+ focusColumnId: next.columnId
486
+ });
487
+ return nextRanges;
488
+ });
489
+ }
490
+ /**
491
+ * Walks each resolved rectangle, invoking a visitor per selectable cell.
492
+ *
493
+ * Every expansion API shares this so the per-cell enable predicate is applied
494
+ * in exactly one place.
495
+ */
496
+ function forEachSelectedCell(table, visit) {
497
+ const bounds = callMemoOrStaticFn(table, "getCellSelectionBounds", table_getCellSelectionBounds);
498
+ if (!bounds.length) return;
499
+ const rows = table.getRowsInDisplayOrder();
500
+ const columns = getDisplayOrderedColumns(table);
501
+ for (let i = 0; i < bounds.length; i++) {
502
+ const bound = bounds[i];
503
+ for (let rowIndex = bound.minRowIndex; rowIndex <= bound.maxRowIndex; rowIndex++) {
504
+ const row = rows[rowIndex];
505
+ if (!row) continue;
506
+ const cellsByColumnId = row.getAllCellsByColumnId();
507
+ for (let columnIndex = bound.minColumnIndex; columnIndex <= bound.maxColumnIndex; columnIndex++) {
508
+ const column = columns[columnIndex];
509
+ if (!column) continue;
510
+ const cell = cellsByColumnId[column.id];
511
+ if (!cell) continue;
512
+ if (!callMemoOrStaticFn(cell, "getCanSelect", cell_getCanSelect)) continue;
513
+ visit(cell, i, rowIndex - bound.minRowIndex, columnIndex - bound.minColumnIndex);
514
+ }
515
+ }
516
+ }
517
+ }
518
+ /**
519
+ * Returns the ids of all selected cells, in row-major order.
520
+ *
521
+ * Cells covered by overlapping ranges are returned once, at their first
522
+ * occurrence.
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * const ids = table_getSelectedCellIds(table)
527
+ * ```
528
+ */
529
+ function table_getSelectedCellIds(table) {
530
+ const ids = [];
531
+ const seen = /* @__PURE__ */ new Set();
532
+ forEachSelectedCell(table, (cell) => {
533
+ if (seen.has(cell.id)) return;
534
+ seen.add(cell.id);
535
+ ids.push(cell.id);
536
+ });
537
+ return ids;
538
+ }
539
+ /**
540
+ * Returns each selected range's values as a row-major grid.
541
+ *
542
+ * This is the raw material for clipboard export. Serializing it to text is left
543
+ * to userland, since the delimiter, the null representation, and whether values
544
+ * containing delimiters get quoted are all application decisions.
545
+ *
546
+ * @example
547
+ * ```ts
548
+ * const [firstRange] = table_getSelectedCellRangesData(table)
549
+ * ```
550
+ */
551
+ function table_getSelectedCellRangesData(table) {
552
+ const grids = [];
553
+ forEachSelectedCell(table, (cell, rangeIndex, rowOffset) => {
554
+ const grid = grids[rangeIndex] ??= [];
555
+ (grid[rowOffset] ??= []).push(cell.getValue());
556
+ });
557
+ return grids;
558
+ }
559
+ /**
560
+ * Returns the number of selected cells.
561
+ *
562
+ * Uses rectangle arithmetic for a single range, which needs no expansion.
563
+ * Multiple ranges are enumerated so overlapping cells are counted once. A
564
+ * per-cell `enableCellSelection` predicate also requires enumeration.
565
+ *
566
+ * @example
567
+ * ```ts
568
+ * const count = table_getSelectedCellCount(table)
569
+ * ```
570
+ */
571
+ function table_getSelectedCellCount(table) {
572
+ if (table.options.enableCellSelection === false) return 0;
573
+ const bounds = callMemoOrStaticFn(table, "getCellSelectionBounds", table_getCellSelectionBounds);
574
+ if (!bounds.length) return 0;
575
+ if (bounds.length > 1 || typeof table.options.enableCellSelection === "function") {
576
+ const ids = /* @__PURE__ */ new Set();
577
+ forEachSelectedCell(table, (cell) => ids.add(cell.id));
578
+ return ids.size;
579
+ }
580
+ const columns = getDisplayOrderedColumns(table);
581
+ const bound = bounds[0];
582
+ let selectableColumns = 0;
583
+ for (let columnIndex = bound.minColumnIndex; columnIndex <= bound.maxColumnIndex; columnIndex++) {
584
+ const column = columns[columnIndex];
585
+ if (!column) continue;
586
+ if (column.columnDef.enableCellSelection !== false) selectableColumns++;
587
+ }
588
+ return (bound.maxRowIndex - bound.minRowIndex + 1) * selectableColumns;
589
+ }
590
+ /**
591
+ * Returns the ids of all rows intersected by the selection.
592
+ *
593
+ * @example
594
+ * ```ts
595
+ * const rowIds = table_getCellSelectionRowIds(table)
596
+ * ```
597
+ */
598
+ function table_getCellSelectionRowIds(table) {
599
+ const bounds = callMemoOrStaticFn(table, "getCellSelectionBounds", table_getCellSelectionBounds);
600
+ if (!bounds.length) return [];
601
+ const rows = table.getRowsInDisplayOrder();
602
+ const seen = /* @__PURE__ */ new Set();
603
+ const ids = [];
604
+ for (let i = 0; i < bounds.length; i++) {
605
+ const bound = bounds[i];
606
+ for (let index = bound.minRowIndex; index <= bound.maxRowIndex; index++) {
607
+ const row = rows[index];
608
+ if (!row || seen.has(row.id)) continue;
609
+ seen.add(row.id);
610
+ ids.push(row.id);
611
+ }
612
+ }
613
+ return ids;
614
+ }
615
+ /**
616
+ * Returns the ids of all columns intersected by the selection.
617
+ *
618
+ * @example
619
+ * ```ts
620
+ * const columnIds = table_getCellSelectionColumnIds(table)
621
+ * ```
622
+ */
623
+ function table_getCellSelectionColumnIds(table) {
624
+ const bounds = callMemoOrStaticFn(table, "getCellSelectionBounds", table_getCellSelectionBounds);
625
+ if (!bounds.length) return [];
626
+ const columns = getDisplayOrderedColumns(table);
627
+ const seen = /* @__PURE__ */ new Set();
628
+ const ids = [];
629
+ for (let i = 0; i < bounds.length; i++) {
630
+ const bound = bounds[i];
631
+ for (let index = bound.minColumnIndex; index <= bound.maxColumnIndex; index++) {
632
+ const column = columns[index];
633
+ if (!column || seen.has(column.id)) continue;
634
+ if (column.columnDef.enableCellSelection === false) continue;
635
+ seen.add(column.id);
636
+ ids.push(column.id);
637
+ }
638
+ }
639
+ return ids;
640
+ }
641
+ /**
642
+ * Creates a handler that begins a selection at this cell.
643
+ *
644
+ * Follows `header_getResizeHandler`: the enable check is resolved once outside
645
+ * the returned closure and guarded again inside it, the document is injectable
646
+ * for SSR and cross-document rendering, and the document-level `mouseup`
647
+ * listener is attached here so a drag released outside the table still ends.
648
+ *
649
+ * @example
650
+ * ```tsx
651
+ * <td onMouseDown={cell.getSelectionStartHandler()} />
652
+ * ```
653
+ */
654
+ function cell_getSelectionStartHandler(cell, _contextDocument) {
655
+ const canSelect = cell_getCanSelect(cell);
656
+ return (e) => {
657
+ if (!canSelect) return;
658
+ const table = cell.table;
659
+ const options = table.options;
660
+ const contextDocument = _contextDocument ?? (typeof document !== "undefined" ? document : null);
661
+ const isRangeEvent = options.enableCellRangeSelection !== false && (options.isCellRangeSelectionEvent?.(e) ?? false);
662
+ const isMultiRangeEvent = options.enableMultiCellRangeSelection !== false && (options.isMultiCellRangeSelectionEvent?.(e) ?? false);
663
+ if (options.enableCellSelectionDrag !== false && options.enableCellRangeSelection !== false && contextDocument) {
664
+ table._isSelectingCells = true;
665
+ const upHandler = () => {
666
+ contextDocument.removeEventListener("mouseup", upHandler);
667
+ table._isSelectingCells = false;
668
+ };
669
+ contextDocument.addEventListener("mouseup", upHandler);
670
+ }
671
+ const rowId = cell.row.id;
672
+ const columnId = cell.column.id;
673
+ table_setCellSelection(table, (old) => {
674
+ const active = old[old.length - 1];
675
+ if (isRangeEvent && active) {
676
+ const ranges = old.slice(0, -1);
677
+ ranges.push({
678
+ ...active,
679
+ focusRowId: rowId,
680
+ focusColumnId: columnId
681
+ });
682
+ return ranges;
683
+ }
684
+ const range = {
685
+ anchorRowId: rowId,
686
+ anchorColumnId: columnId,
687
+ focusRowId: rowId,
688
+ focusColumnId: columnId
689
+ };
690
+ return isMultiRangeEvent ? [...old, range] : [range];
691
+ });
692
+ };
693
+ }
694
+ /**
695
+ * Creates a handler that extends the active range to this cell during a drag.
696
+ *
697
+ * No rAF coalescing is needed here, unlike the resize handler: `mouseenter`
698
+ * fires once per cell boundary crossed rather than continuously, and deferring
699
+ * it by a frame would only delay the highlight.
700
+ *
701
+ * @example
702
+ * ```tsx
703
+ * <td onMouseEnter={cell.getSelectionExtendHandler()} />
704
+ * ```
705
+ */
706
+ function cell_getSelectionExtendHandler(cell) {
707
+ const canSelect = cell_getCanSelect(cell);
708
+ return (_e) => {
709
+ if (!canSelect) return;
710
+ const table = cell.table;
711
+ if (!table._isSelectingCells) return;
712
+ const ranges = table.atoms.cellSelection?.get();
713
+ const active = ranges?.[ranges.length - 1];
714
+ if (!active) return;
715
+ const rowId = cell.row.id;
716
+ const columnId = cell.column.id;
717
+ if (active.focusRowId === rowId && active.focusColumnId === columnId) return;
718
+ table_setCellSelection(table, (old) => {
719
+ if (!old.length) return old;
720
+ const next = old.slice(0, -1);
721
+ next.push({
722
+ ...old[old.length - 1],
723
+ focusRowId: rowId,
724
+ focusColumnId: columnId
725
+ });
726
+ return next;
727
+ });
728
+ };
729
+ }
730
+
731
+ //#endregion
732
+ export { cell_getCanSelect, cell_getIsFocused, cell_getIsSelected, cell_getSelectionEdges, cell_getSelectionExtendHandler, cell_getSelectionStartHandler, cell_getTabIndex, getDefaultCellSelectionState, table_autoResetCellSelection, table_extendCellSelection, table_getCellSelectionBounds, table_getCellSelectionColumnIds, table_getCellSelectionColumnIndexes, table_getCellSelectionRowIds, table_getFocusedCell, table_getSelectedCellCount, table_getSelectedCellIds, table_getSelectedCellRangesData, table_moveCellSelection, table_resetCellSelection, table_selectAllCells, table_selectCellRange, table_setCellSelection, table_setFocusedCell };