@iyulab/flex-table 0.55.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.56.0] - 2026-10-07
4
+
5
+ ### Fixed
6
+
7
+ - **Row selection stays on its rows.** The checkmarks were kept by screen position, so sorting, filtering, inserting
8
+ or deleting rows moved them onto other rows without a `selection-change` — select a row, sort, and a different row
9
+ was selected (and a bulk action took it). Replacing `data` with other rows of the same length carried the
10
+ checkmarks onto the new rows. Selection is now kept by row id (see `rowKey` below).
11
+ - **`deleteRows()` without indices deletes the checked rows in a `selectable` grid.** It deleted the rows of the
12
+ cell selection (the active cell's row), so a «Delete selected» button removed a row nobody checked. With nothing
13
+ checked it now deletes nothing. A grid without checkboxes keeps deleting the cell selection's rows.
14
+
15
+ ### Added
16
+
17
+ - **`rowKey`** (`row-key`, default `'_id'` — the field `u-rich-table` reads) — the field, or a function of the row,
18
+ that names a row. A keyed row stays selected when `data` is replaced, so a selection spans server pages. A row
19
+ without a key is named by the row object (a session-local id `#n`) and leaves the selection when `data` no longer
20
+ holds it.
21
+ - `getRowId(row)`, the `selectedRowIds` getter and `setSelection(ids)` (does nothing, and fires nothing, when the
22
+ selection already is `ids`).
23
+ - `selection-change` carries `selectedIds` — every selected row, on every page — next to `selectedRows` and
24
+ `selectedIndices` (the selected rows `data` holds). `row-activate` carries the row's `id`.
25
+
26
+ ### Changed (breaking)
27
+
28
+ - The header checkbox and `selectAll()` act on the rows in view: rows selected on another page stay selected, and
29
+ unchecking the header deselects only the rows in view. `deselectAll()` still clears every page. A selected row a
30
+ filter hides stays selected (and in `selectedRows`).
31
+ - `RowSelectionState` (exported) keeps ids: `isSelected(id)`, `toggle(id)`, `select(id)`, `selectAll(ids)`,
32
+ `deselectMany(ids)`, `set(ids)`, `retain(keep)`, `selectRange(orderedIds, from, to)`, `isAllSelected(ids)`,
33
+ `isSomeSelected(ids)`, `selectedIds`. `setRowCount` and `selectedIndices` are gone.
34
+ - `clear-selection-on-data-change` keeps its meaning; it is no longer needed to stop a replacement from moving the
35
+ checkmarks.
36
+
37
+ **Migrating**: give rows a stable key (`_id`, or set `rowKey`) when selection should survive a reload or a page change.
38
+ Read `selectedIds` for «what is selected» across pages, `selectedRows` for the rows on hand. A «Delete selected» button
39
+ on a `selectable` grid can keep calling `deleteRows()`.
40
+
3
41
  ## [0.55.2] - 2026-10-07
4
42
 
5
43
  ### Documentation
package/README.md CHANGED
@@ -108,6 +108,7 @@ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both s
108
108
  | `maxRows` | `max-rows` | `number` | `0` | Max row count (0 = unlimited); blocks `addRow()` and paste expansion |
109
109
  | `maxUndoSize` | `max-undo-size` | `number` | `100` | Max undo history stack size |
110
110
  | `selectable` | `selectable` | `boolean` | `false` | Enable row-level checkbox selection |
111
+ | `rowKey` | `row-key` | `string \| (row) => unknown` | `'_id'` | What names a row — selection is kept by this id (see [Row Selection](#row-selection)) |
111
112
  | `selectionMode` | `selection-mode` | `'single' \| 'multi'` | `'multi'` | Row selection mode |
112
113
  | `dataMode` | `data-mode` | `'client' \| 'server'` | `'client'` | Client-side or server-side data processing |
113
114
  | `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Footer/summary row data (keys match column keys) |
@@ -235,7 +236,7 @@ Every row keeps its own value. Sorting, filtering, copying, CSV/XLSX export and
235
236
  | Method | Returns | Description |
236
237
  |--------|---------|-------------|
237
238
  | `addRow(row?, index?)` | `DataRow \| null` | Add a row. Returns `null` if `maxRows` reached |
238
- | `deleteRows(indices?)` | `void` | Delete rows by data index (default: selected rows) |
239
+ | `deleteRows(indices?)` | `void` | Delete rows by data index. Without indices: the checked rows when `selectable` (none checked — nothing), otherwise the rows of the cell selection |
239
240
  | `updateRows(changes)` | `void` | Batch update cells as single undo action. `changes: Array<{ row, key, value }>` |
240
241
  | `refreshData()` | `void` | Force re-render after in-place data mutation |
241
242
 
@@ -253,11 +254,21 @@ Every row keeps its own value. Sorting, filtering, copying, CSV/XLSX export and
253
254
 
254
255
  | Method | Returns | Description |
255
256
  |--------|---------|-------------|
256
- | `selectAll()` | `void` | Select all visible rows (multi mode only) |
257
- | `deselectAll()` | `void` | Deselect all rows |
258
- | `getSelectedRows()` | `{ selectedIndices, selectedRows }` | Get selected row data |
257
+ | `selectAll()` | `void` | Select every row in view, after filtering (multi mode only). Rows on other pages stay as they are |
258
+ | `deselectAll()` | `void` | Deselect every row, on every page |
259
+ | `selectWhere(predicate)` | `void` | Add the rows in view for which `predicate(row, dataIndex)` is true |
260
+ | `setSelection(ids)` | `void` | Replace the selection with these row ids. The same set again does nothing and fires nothing |
261
+ | `getSelectedRows()` | `{ selectedIds, selectedIndices, selectedRows }` | `selectedIds`: every selected row (all pages). `selectedRows` / `selectedIndices`: the selected rows `data` holds, and their positions in it |
262
+ | `getRowId(row)` | `string` | The id selection keeps for a row |
263
+ | `selectedRowIds` (getter) | `ReadonlySet<string>` | Ids of the selected rows, all pages (a copy) |
259
264
 
260
- Row selection is index-based (there is no row-key concept), so replacing `data` with a same-length but different set of rows leaves the selection pointing at the new rows occupying the old indices. If selection drives a bulk action (status changes, bulk delete, etc.), set `clear-selection-on-data-change` so a `data` swap always resets selection and re-fires `selection-change` with an empty selection:
265
+ Row selection is kept by **row id**, not by position, so a checkmark stays on its row through sorting, filtering, inserts and deletes. The id is the row's `row-key` field — `_id` by default, the same field `u-rich-table` reads — or the result of a `rowKey` function:
266
+
267
+ ```ts
268
+ table.rowKey = 'orderNo'; // or: table.rowKey = (row) => `${row.site}/${row.no}`
269
+ ```
270
+
271
+ A keyed row stays selected when `data` is replaced, so in a server-paged grid the selection spans pages (`selectedIds` names them all; `selectedRows` holds the ones on this page). A row without a key is named by the row object itself (a session-local id `#n`): it follows the row while the same object is in `data` and leaves the selection when it is gone. Set `clear-selection-on-data-change` when every new `data` should start a new selection (a new search):
261
272
 
262
273
  ```html
263
274
  <flex-table selectable clear-selection-on-data-change></flex-table>
@@ -304,7 +315,7 @@ type-checks without a cast. The React wrapper's `on*` props carry the same types
304
315
  |-------|--------|-------------|
305
316
  | `row-add` | `{ row, index }` | Row added |
306
317
  | `row-delete` | `{ indices, rows }` | Rows deleted |
307
- | `row-activate` | `{ row, index, col, key }` | Enter pressed on a non-editable cell — the grid's own contract for "activate this row" (e.g. navigate to a detail view), guaranteed even though the internal Enter handler prevents the keystroke from reliably reaching a listener the host attaches to the same element |
318
+ | `row-activate` | `{ row, id, index, col, key }` | Enter pressed on a non-editable cell — the grid's own contract for "activate this row" (e.g. navigate to a detail view), guaranteed even though the internal Enter handler prevents the keystroke from reliably reaching a listener the host attaches to the same element |
308
319
  | `batch-update` | `{ changes: [{ row, key, oldValue, newValue }] }` | Batch update applied |
309
320
  | `row-reorder` | `{ from, to }` | Row dragged to a new place (data indices) |
310
321
  | `data-import` | `{ count }` | Rows imported from a file |
@@ -336,7 +347,7 @@ type-checks without a cast. The React wrapper's `on*` props carry the same types
336
347
 
337
348
  | Event | Detail | Description |
338
349
  |-------|--------|-------------|
339
- | `selection-change` | `{ selectedIndices, selectedRows }` | Row checkbox selection changed |
350
+ | `selection-change` | `{ selectedIds, selectedIndices, selectedRows }` | Row checkbox selection changed — `selectedIds` covers every page, `selectedRows` the rows `data` holds |
340
351
 
341
352
  ### Clipboard Events
342
353
 
@@ -1,30 +1,41 @@
1
1
  import type { SelectionMode } from '../models/types.js';
2
2
  /**
3
- * Manages row-level selection state (checkbox-based).
4
- * Separate from cell-level SelectionState.
3
+ * Row-level (checkbox) selection, kept as a set of row ids — see `FlexTable.getRowId`.
4
+ * Separate from the cell-level `SelectionState`.
5
+ *
6
+ * An id names a row, not a position: sorting, filtering, inserting or deleting rows leaves the checkmarks on the
7
+ * rows they were put on. The header checkbox and "select all" act on the rows currently in view (`ids` arguments);
8
+ * ids of rows that are not in view (another server page) stay selected.
5
9
  */
6
10
  export declare class RowSelectionState {
7
11
  private _selected;
8
12
  private _mode;
9
- private _rowCount;
10
13
  get mode(): SelectionMode;
11
14
  set mode(value: SelectionMode);
12
- setRowCount(count: number): void;
13
- get selectedIndices(): number[];
15
+ /** Selected ids, in the order they were selected. */
16
+ get selectedIds(): string[];
14
17
  get selectedCount(): number;
15
- get isAllSelected(): boolean;
16
- get isSomeSelected(): boolean;
17
- isSelected(index: number): boolean;
18
- toggle(index: number): void;
19
- select(index: number): void;
20
- deselect(index: number): void;
21
- selectAll(): void;
18
+ isSelected(id: string): boolean;
19
+ /** Every one of `ids` (the rows in view) is selected — false when `ids` is empty. */
20
+ isAllSelected(ids: readonly string[]): boolean;
21
+ /** Some but not all of `ids` (the rows in view) are selected. */
22
+ isSomeSelected(ids: readonly string[]): boolean;
23
+ toggle(id: string): void;
24
+ select(id: string): void;
25
+ deselect(id: string): void;
26
+ /** Adds `ids` to the selection (multi mode only — one slot cannot hold a set). */
27
+ selectAll(ids: readonly string[]): void;
28
+ /** Removes `ids` from the selection — the rest stays. */
29
+ deselectMany(ids: readonly string[]): void;
22
30
  deselectAll(): void;
31
+ /** Replaces the selection with `ids` (single mode keeps the last). Returns whether it changed. */
32
+ set(ids: Iterable<string>): boolean;
33
+ /** Keeps only the ids `keep` accepts. Returns whether anything was dropped. */
34
+ retain(keep: (id: string) => boolean): boolean;
23
35
  /**
24
- * Selects every index between from and to, inclusive, in either direction.
25
- * Used for shift-click range selection anchored at the last toggled row.
26
- * Single mode: only the endpoint (to) ends up selected, matching toggle()'s
27
- * "one at a time" semantics — a range doesn't make sense with one slot.
36
+ * Selects `ids` between the positions of `from` and `to` in `ordered` (the rows in view, top to bottom), inclusive,
37
+ * in either direction — shift-click range selection anchored at the last toggled row. Single mode selects only
38
+ * `to`, matching `toggle()`'s one-slot semantics. An anchor that is no longer in view selects only `to`.
28
39
  */
29
- selectRange(from: number, to: number): void;
40
+ selectRange(ordered: readonly string[], from: string, to: string): void;
30
41
  }
package/dist/events.d.ts CHANGED
@@ -19,7 +19,9 @@ export interface CellChange {
19
19
  */
20
20
  export interface FlexTableEventMap {
21
21
  /** Row selection changed (`selectable`). Indices are data indices. */
22
+ /** `selectedIds` — every selected row (all pages, see `rowKey`); `selectedRows`/`selectedIndices` — the selected rows `data` holds. */
22
23
  'selection-change': CustomEvent<{
24
+ selectedIds: string[];
23
25
  selectedIndices: number[];
24
26
  selectedRows: DataRow[];
25
27
  }>;
@@ -83,8 +85,10 @@ export interface FlexTableEventMap {
83
85
  to: number;
84
86
  }>;
85
87
  /** Enter on a non-editable cell — "activate this row". `index` is the data index, `col` the visible column index. */
88
+ /** `id` — the row's id (`getRowId`); `index` — its position in `data`; `key` — the active column's key. */
86
89
  'row-activate': CustomEvent<{
87
90
  row: DataRow;
91
+ id: string;
88
92
  index: number;
89
93
  col: number;
90
94
  key: string | undefined;
@@ -1225,7 +1225,7 @@ var oe = class {
1225
1225
  }
1226
1226
  }, k = class {
1227
1227
  constructor() {
1228
- this._selected = /* @__PURE__ */ new Set(), this._mode = "multi", this._rowCount = 0;
1228
+ this._selected = /* @__PURE__ */ new Set(), this._mode = "multi";
1229
1229
  }
1230
1230
  get mode() {
1231
1231
  return this._mode;
@@ -1236,27 +1236,24 @@ var oe = class {
1236
1236
  this._selected.clear(), e !== void 0 && this._selected.add(e);
1237
1237
  }
1238
1238
  }
1239
- setRowCount(e) {
1240
- this._rowCount = e;
1241
- for (let t of this._selected) t >= e && this._selected.delete(t);
1242
- }
1243
- get selectedIndices() {
1244
- return [...this._selected].sort((e, t) => e - t);
1239
+ get selectedIds() {
1240
+ return [...this._selected];
1245
1241
  }
1246
1242
  get selectedCount() {
1247
1243
  return this._selected.size;
1248
1244
  }
1249
- get isAllSelected() {
1250
- return this._rowCount > 0 && this._selected.size === this._rowCount;
1251
- }
1252
- get isSomeSelected() {
1253
- return this._selected.size > 0 && this._selected.size < this._rowCount;
1254
- }
1255
1245
  isSelected(e) {
1256
1246
  return this._selected.has(e);
1257
1247
  }
1248
+ isAllSelected(e) {
1249
+ return e.length > 0 && e.every((e) => this._selected.has(e));
1250
+ }
1251
+ isSomeSelected(e) {
1252
+ let t = e.reduce((e, t) => e + +!!this._selected.has(t), 0);
1253
+ return t > 0 && t < e.length;
1254
+ }
1258
1255
  toggle(e) {
1259
- this._mode === "single" ? this._selected.has(e) ? this._selected.clear() : (this._selected.clear(), this._selected.add(e)) : this._selected.has(e) ? this._selected.delete(e) : this._selected.add(e);
1256
+ this._selected.has(e) ? this._selected.delete(e) : this.select(e);
1260
1257
  }
1261
1258
  select(e) {
1262
1259
  this._mode === "single" && this._selected.clear(), this._selected.add(e);
@@ -1264,22 +1261,31 @@ var oe = class {
1264
1261
  deselect(e) {
1265
1262
  this._selected.delete(e);
1266
1263
  }
1267
- selectAll() {
1268
- if (this._mode !== "single") {
1269
- this._selected.clear();
1270
- for (let e = 0; e < this._rowCount; e++) this._selected.add(e);
1271
- }
1264
+ selectAll(e) {
1265
+ if (this._mode !== "single") for (let t of e) this._selected.add(t);
1266
+ }
1267
+ deselectMany(e) {
1268
+ for (let t of e) this._selected.delete(t);
1272
1269
  }
1273
1270
  deselectAll() {
1274
1271
  this._selected.clear();
1275
1272
  }
1276
- selectRange(e, t) {
1277
- if (this._mode === "single") {
1278
- this.select(t);
1273
+ set(e) {
1274
+ let t = [...new Set(e)], n = this._mode === "single" ? t.slice(-1) : t;
1275
+ return n.length === this._selected.size && n.every((e) => this._selected.has(e)) ? !1 : (this._selected = new Set(n), !0);
1276
+ }
1277
+ retain(e) {
1278
+ let t = !1;
1279
+ for (let n of this._selected) e(n) || (this._selected.delete(n), t = !0);
1280
+ return t;
1281
+ }
1282
+ selectRange(e, t, n) {
1283
+ let r = e.indexOf(t), i = e.indexOf(n);
1284
+ if (this._mode === "single" || r < 0 || i < 0) {
1285
+ this.select(n);
1279
1286
  return;
1280
1287
  }
1281
- let n = Math.min(e, t), r = Math.max(e, t);
1282
- for (let e = n; e <= r; e++) e >= 0 && e < this._rowCount && this._selected.add(e);
1288
+ for (let t = Math.min(r, i); t <= Math.max(r, i); t++) this._selected.add(e[t]);
1283
1289
  }
1284
1290
  };
1285
1291
  //#endregion
@@ -1756,7 +1762,7 @@ var K = {
1756
1762
  lte: "≤"
1757
1763
  }, q = 120, J = 40, Y = 20, X = 32, Z = 5, Xe = 200, Q = class extends r {
1758
1764
  constructor(...e) {
1759
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "", this.noMatchingMessage = "", this.error = null, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
1765
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this.rowKey = "_id", this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "", this.noMatchingMessage = "", this.error = null, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowId = null, this._unkeyedIds = /* @__PURE__ */ new WeakMap(), this._unkeyedSeq = 0, this._visibleIdsCache = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
1760
1766
  e.composedPath()[0] !== this || this._activeCell || this._headerCol !== null || this.visibleColumns.length !== 0 && this.matches(":focus-visible") && (this._visibleRowCount > 0 ? (this._selection.setActive(0, 0), this._syncActiveCell()) : this._headerCol = 0);
1761
1767
  }, this._onFocusIn = (e) => {
1762
1768
  let t = e.composedPath()[0];
@@ -1784,7 +1790,7 @@ var K = {
1784
1790
  }
1785
1791
  set data(e) {
1786
1792
  let t = this._data, n = e !== t;
1787
- this._data = e, n && !this._inUndoRedo && (this.clearUndoOnDataChange && this._undo.clear(), this.clearSelectionOnDataChange && this._rowSelection.selectedCount > 0 && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent())), this.requestUpdate("data", t);
1793
+ this._data = e, n && !this._inUndoRedo && (this.clearUndoOnDataChange && this._undo.clear(), this._rowSelection.selectedCount > 0 && (this.clearSelectionOnDataChange ? (this._rowSelection.deselectAll(), 1) : this._pruneGoneUnkeyedRows()) && (this._rowSelectionVersion++, this._dispatchRowSelectionEvent())), this.requestUpdate("data", t);
1788
1794
  }
1789
1795
  get rowHeight() {
1790
1796
  let e = this._rowHeightExplicit ?? this._rowHeightFromCss ?? X;
@@ -1835,14 +1841,28 @@ var K = {
1835
1841
  return this._filteredIndices.length;
1836
1842
  }
1837
1843
  getSelectedRows() {
1838
- let e = this._rowSelection.selectedIndices.map((e) => this._toDataIndex(e)).filter((e) => e >= 0 && e < this.data.length);
1844
+ let e = [];
1845
+ if (this._rowSelection.selectedCount > 0) for (let t = 0; t < this.data.length; t++) this._rowSelection.isSelected(this.getRowId(this.data[t])) && e.push(t);
1839
1846
  return {
1847
+ selectedIds: this._rowSelection.selectedIds,
1840
1848
  selectedIndices: e,
1841
1849
  selectedRows: e.map((e) => this.data[e])
1842
1850
  };
1843
1851
  }
1852
+ getRowId(e) {
1853
+ let t = this.rowKey, n = typeof t == "function" ? t(e) : e[t];
1854
+ if (n != null && n !== "") return String(n);
1855
+ let r = this._unkeyedIds.get(e);
1856
+ return r === void 0 && (r = `#${++this._unkeyedSeq}`, this._unkeyedIds.set(e, r)), r;
1857
+ }
1858
+ get selectedRowIds() {
1859
+ return new Set(this._rowSelection.selectedIds);
1860
+ }
1861
+ setSelection(e) {
1862
+ this.selectable && this._rowSelection.set(e) && (this._rowSelectionVersion++, this._dispatchRowSelectionEvent(), this.requestUpdate());
1863
+ }
1844
1864
  selectAll() {
1845
- this.selectable && (this._rowSelection.selectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent());
1865
+ this.selectable && (this._rowSelection.selectAll(this._visibleRowIds()), this._rowSelectionVersion++, this._dispatchRowSelectionEvent());
1846
1866
  }
1847
1867
  deselectAll() {
1848
1868
  this.selectable && (this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent());
@@ -1851,17 +1871,33 @@ var K = {
1851
1871
  if (this.selectable) {
1852
1872
  for (let t = 0; t < this._visibleRowCount; t++) {
1853
1873
  let n = this._toDataIndex(t), r = this.data[n];
1854
- r !== void 0 && e(r, n) && this._rowSelection.select(t);
1874
+ r !== void 0 && e(r, n) && this._rowSelection.select(this.getRowId(r));
1855
1875
  }
1856
1876
  this._rowSelectionVersion++, this._dispatchRowSelectionEvent(), this.requestUpdate();
1857
1877
  }
1858
1878
  }
1859
1879
  _dispatchRowSelectionEvent() {
1860
- let { selectedIndices: e, selectedRows: t } = this.getSelectedRows();
1861
- this._emit("selection-change", {
1862
- selectedIndices: e,
1863
- selectedRows: t
1864
- });
1880
+ this._emit("selection-change", this.getSelectedRows());
1881
+ }
1882
+ _visibleRowIds() {
1883
+ let e = this._visibleIdsCache;
1884
+ if (e && e.order === this._sortedIndices && e.data === this.data && e.key === this.rowKey && e.ids.length === this._visibleRowCount) return e.ids;
1885
+ let t = [];
1886
+ for (let e = 0; e < this._visibleRowCount; e++) {
1887
+ let n = this.data[this._toDataIndex(e)];
1888
+ n !== void 0 && t.push(this.getRowId(n));
1889
+ }
1890
+ return this._visibleIdsCache = {
1891
+ order: this._sortedIndices,
1892
+ data: this.data,
1893
+ key: this.rowKey,
1894
+ ids: t
1895
+ }, t;
1896
+ }
1897
+ _pruneGoneUnkeyedRows() {
1898
+ if (!this._rowSelection.selectedIds.some((e) => e.startsWith("#"))) return !1;
1899
+ let e = new Set(this.data.map((e) => this.getRowId(e)));
1900
+ return this._rowSelection.retain((t) => !t.startsWith("#") || e.has(t));
1865
1901
  }
1866
1902
  setFilter(e, t) {
1867
1903
  this._filters = this._filters.filter((t) => t.key !== e), this._filters.push({
@@ -2037,34 +2073,35 @@ var K = {
2037
2073
  }), this._dispatchUndoStateEvent(), n;
2038
2074
  }
2039
2075
  deleteRows(e) {
2040
- let t = e ?? this._getSelectedDataRows();
2076
+ let t = e ?? (this.selectable ? this.getSelectedRows().selectedIndices : this._getSelectedDataRows());
2041
2077
  if (t.length === 0) return;
2042
2078
  let n = [...t].sort((e, t) => t - e), r = [];
2043
2079
  for (let e of n) e >= 0 && e < this.data.length && (r.push({
2044
2080
  index: e,
2045
2081
  row: this.data[e]
2046
2082
  }), this.data.splice(e, 1));
2047
- if (r.length !== 0) {
2048
- if (r.reverse(), this._undo.push({
2049
- label: "row-delete",
2050
- undo: () => {
2051
- for (let { index: e, row: t } of r) this.data.splice(e, 0, t);
2052
- this.requestUpdate();
2053
- },
2054
- redo: () => {
2055
- let e = [...r].reverse();
2056
- for (let { index: t } of e) this.data.splice(t, 1);
2057
- this.requestUpdate();
2058
- }
2059
- }), this._activeCell) {
2060
- let e = this._toDataIndex(this._activeCell.row);
2061
- t.includes(e) && (this._selection.clear(), this._activeCell = null);
2083
+ if (r.length === 0) return;
2084
+ r.reverse(), this._undo.push({
2085
+ label: "row-delete",
2086
+ undo: () => {
2087
+ for (let { index: e, row: t } of r) this.data.splice(e, 0, t);
2088
+ this.requestUpdate();
2089
+ },
2090
+ redo: () => {
2091
+ let e = [...r].reverse();
2092
+ for (let { index: t } of e) this.data.splice(t, 1);
2093
+ this.requestUpdate();
2062
2094
  }
2063
- this.requestUpdate(), this._emit("row-delete", {
2064
- indices: r.map((e) => e.index),
2065
- rows: r.map((e) => e.row)
2066
- }), this._dispatchUndoStateEvent();
2095
+ });
2096
+ let i = r.map((e) => this.getRowId(e.row)).filter((e) => this._rowSelection.isSelected(e));
2097
+ if (i.length > 0 && (this._rowSelection.deselectMany(i), this._rowSelectionVersion++, this._dispatchRowSelectionEvent()), this._activeCell) {
2098
+ let e = this._toDataIndex(this._activeCell.row);
2099
+ t.includes(e) && (this._selection.clear(), this._activeCell = null);
2067
2100
  }
2101
+ this.requestUpdate(), this._emit("row-delete", {
2102
+ indices: r.map((e) => e.index),
2103
+ rows: r.map((e) => e.row)
2104
+ }), this._dispatchUndoStateEvent();
2068
2105
  }
2069
2106
  updateRows(e) {
2070
2107
  if (e.length === 0) return;
@@ -2354,7 +2391,7 @@ var K = {
2354
2391
  };
2355
2392
  }
2356
2393
  willUpdate(e) {
2357
- (!(e.size <= 2 && !e.has("data") && !e.has("columns") && !e.has("_openFilterKey") && !e.has("_editingCell") && !e.has("_activeCell") && (e.has("_scrollTop") || e.has("_scrollLeft"))) || this._viewDirty) && (this._recomputeView(), this._viewDirty = !1), this._updateColOffsets(), this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length), this._rowSelection.setRowCount(this._visibleRowCount);
2394
+ (!(e.size <= 2 && !e.has("data") && !e.has("columns") && !e.has("_openFilterKey") && !e.has("_editingCell") && !e.has("_activeCell") && (e.has("_scrollTop") || e.has("_scrollLeft"))) || this._viewDirty) && (this._recomputeView(), this._viewDirty = !1), this._updateColOffsets(), this._selection.setDimensions(this._visibleRowCount, this.visibleColumns.length);
2358
2395
  }
2359
2396
  updated(e) {
2360
2397
  this._focusEditor(), this._syncGridFocus(), this._adjustFilterDropdown(), e.has("_headerMenu") && this._keepMenuInView(".ft-header-menu"), e.has("_bodyContextMenu") && this._bodyContextMenu && (this._keepMenuInView(".ft-body-context-menu"), this.shadowRoot?.querySelector(".ft-body-context-menu [role=\"menuitem\"]")?.focus()), e.has("stylesheets") && this._syncStylesheets();
@@ -2492,6 +2529,7 @@ var K = {
2492
2529
  let t = this._toDataIndex(this._activeCell.row), n = this.data[t];
2493
2530
  n && this._emit("row-activate", {
2494
2531
  row: n,
2532
+ id: this.getRowId(n),
2495
2533
  index: t,
2496
2534
  col: this._activeCell.col,
2497
2535
  key: e?.key
@@ -2722,7 +2760,11 @@ var K = {
2722
2760
  }
2723
2761
  if (!this._handleCtrlKey(e) && !this._handleAltKey(e, t)) {
2724
2762
  if (e.key === " " && e.shiftKey && this.selectable && this._activeCell && !e.ctrlKey && !e.metaKey && !e.altKey) {
2725
- e.preventDefault(), this._rowSelection.toggle(this._activeCell.row), this._lastCheckboxRowIndex = this._activeCell.row, this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
2763
+ e.preventDefault();
2764
+ let t = this.data[this._toDataIndex(this._activeCell.row)];
2765
+ if (!t) return;
2766
+ let n = this.getRowId(t);
2767
+ this._rowSelection.toggle(n), this._lastCheckboxRowId = n, this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
2726
2768
  return;
2727
2769
  }
2728
2770
  if (this._activeCell && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
@@ -4146,8 +4188,8 @@ var K = {
4146
4188
  ${this._rowSelection.mode === "multi" ? a`
4147
4189
  <!-- 칸 전체가 누르는 자리다(라벨) — 체크 상자는 16px 그대로. -->
4148
4190
  <label class="ft-checkbox-hit"><input type="checkbox" tabindex="-1" aria-label=${e("selectAllRows")}
4149
- .checked=${this._rowSelection.isAllSelected}
4150
- .indeterminate=${this._rowSelection.isSomeSelected}
4191
+ .checked=${this._rowSelection.isAllSelected(this._visibleRowIds())}
4192
+ .indeterminate=${this._rowSelection.isSomeSelected(this._visibleRowIds())}
4151
4193
  @change=${this._onSelectAllChange}></label>
4152
4194
  ` : ""}
4153
4195
  </div>` : "";
@@ -4194,10 +4236,14 @@ var K = {
4194
4236
  `;
4195
4237
  }
4196
4238
  _onSelectAllChange(e) {
4197
- e.target.checked ? this._rowSelection.selectAll() : this._rowSelection.deselectAll(), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
4239
+ e.target.checked ? this._rowSelection.selectAll(this._visibleRowIds()) : this._rowSelection.deselectMany(this._visibleRowIds()), this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
4198
4240
  }
4199
4241
  _onRowCheckboxChange(e, t) {
4200
- e.stopPropagation(), this._checkboxShiftPending && this._lastCheckboxRowIndex !== null ? this._rowSelection.selectRange(this._lastCheckboxRowIndex, t) : this._rowSelection.toggle(t), this._lastCheckboxRowIndex = t, this._checkboxShiftPending = !1, this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
4242
+ e.stopPropagation();
4243
+ let n = this.data[this._toDataIndex(t)];
4244
+ if (!n) return;
4245
+ let r = this.getRowId(n);
4246
+ this._checkboxShiftPending && this._lastCheckboxRowId !== null ? this._rowSelection.selectRange(this._visibleRowIds(), this._lastCheckboxRowId, r) : this._rowSelection.toggle(r), this._lastCheckboxRowId = r, this._checkboxShiftPending = !1, this._rowSelectionVersion++, this._dispatchRowSelectionEvent();
4201
4247
  }
4202
4248
  _onRowCheckboxClick(e) {
4203
4249
  this._checkboxShiftPending = e.shiftKey;
@@ -4242,7 +4288,7 @@ var K = {
4242
4288
  `;
4243
4289
  }
4244
4290
  _renderRow(t, n, r, i, o) {
4245
- let s = this.visibleColumns, c = this._toDataIndex(t), l = this.data[c], u = o ?? t * this.rowHeight, d = this.rowHeight, f = this._totalRowWidth, p = t % 2 == 0 ? "ft-row-even" : "ft-row-odd", m = this.selectable && this._rowSelection.isSelected(t), h = this._scrollLeft, g = 0, _ = this.selectable ? a`
4291
+ let s = this.visibleColumns, c = this._toDataIndex(t), l = this.data[c], u = o ?? t * this.rowHeight, d = this.rowHeight, f = this._totalRowWidth, p = t % 2 == 0 ? "ft-row-even" : "ft-row-odd", m = this.selectable && l !== void 0 && this._rowSelection.isSelected(this.getRowId(l)), h = this._scrollLeft, g = 0, _ = this.selectable ? a`
4246
4292
  <div class="ft-checkbox-cell"
4247
4293
  style="position: absolute; top: 0; left: ${h + g}px; width: ${this._checkboxColWidth}px; height: ${d}px; z-index: 2;">
4248
4294
  <!-- 행 체크박스는 Tab 정지점이 아니다 — 그리드(호스트)가 하나의 정지점이고 행 선택은 Shift+Space 다.
@@ -4426,7 +4472,7 @@ G([c({ type: Array })], Q.prototype, "columns", void 0), G([c({
4426
4472
  })], Q.prototype, "clearUndoOnDataChange", void 0), G([c({
4427
4473
  type: Boolean,
4428
4474
  attribute: "clear-selection-on-data-change"
4429
- })], Q.prototype, "clearSelectionOnDataChange", void 0), G([c({
4475
+ })], Q.prototype, "clearSelectionOnDataChange", void 0), G([c({ attribute: "row-key" })], Q.prototype, "rowKey", void 0), G([c({
4430
4476
  type: Number,
4431
4477
  attribute: "row-height"
4432
4478
  })], Q.prototype, "rowHeight", null), G([c({
@@ -28,15 +28,22 @@ export declare class FlexTable extends LitElement {
28
28
  /**
29
29
  * When true, replacing `data` externally automatically clears row selection
30
30
  * (checkbox selection) and re-dispatches `selection-change` with an empty selection.
31
- * Default: false, for consistency with `clearUndoOnDataChange`. Internal undo/redo
32
- * operations are never affected.
33
- *
34
- * Row selection is index-based (no row-key concept), so a full `data` replacement can
35
- * leave selection pointing at different underlying rows at the same indices —
36
- * recommended for any `selectable` grid whose selection drives bulk actions
37
- * (e.g. server-mode grids refreshed via `useODataSource`).
31
+ * Default: false — keyed rows stay selected across a replacement (see `rowKey`), so a
32
+ * selection can span server pages. Set it when a new `data` should start a new selection
33
+ * (a new search). Internal undo/redo operations are never affected.
38
34
  */
39
35
  clearSelectionOnDataChange: boolean;
36
+ /**
37
+ * What names a row: the field whose value identifies it (default `'_id'`, as in `u-rich-table`),
38
+ * or a function of the row. Row selection is kept by this id (`getRowId`), so checkmarks stay on
39
+ * their rows through sorting, filtering, inserts and deletes — and, for rows that have a key,
40
+ * across a `data` replacement: another server page keeps the rows selected on the last one.
41
+ *
42
+ * A row whose key is missing (`undefined`, `null` or `''`) is named by the row object itself —
43
+ * a session-local id `#n`. It follows the row while the same object is in `data`, and leaves the
44
+ * selection when `data` no longer holds that object (a replacement with new objects).
45
+ */
46
+ rowKey: string | ((row: DataRow) => unknown);
40
47
  /**
41
48
  * 행 높이(px).
42
49
  *
@@ -135,8 +142,12 @@ export declare class FlexTable extends LitElement {
135
142
  private _sortedIndices;
136
143
  private _openFilterKey;
137
144
  private _rowSelectionVersion;
138
- /** Anchor row for shift-click range selection on the row checkbox column. */
139
- private _lastCheckboxRowIndex;
145
+ /** Anchor row (its id) for shift-click range selection on the row checkbox column. */
146
+ private _lastCheckboxRowId;
147
+ /** Session-local ids of rows that have no key — by object, so they follow the row and leave with it. */
148
+ private _unkeyedIds;
149
+ private _unkeyedSeq;
150
+ private _visibleIdsCache;
140
151
  /** Set by the checkbox's own click (which carries shiftKey) just before its change event fires. */
141
152
  private _checkboxShiftPending;
142
153
  private _autocompleteState;
@@ -179,14 +190,29 @@ export declare class FlexTable extends LitElement {
179
190
  get sortCriteria(): SortCriteria[];
180
191
  /** Number of rows after filtering (before pagination). */
181
192
  get filteredRowCount(): number;
182
- /** Get data indices of currently selected rows. */
193
+ /**
194
+ * The row selection. `selectedIds` is every selected row — on every page, including rows not in
195
+ * `data` now. `selectedRows` and `selectedIndices` are the selected rows that `data` holds (in
196
+ * `data` order, filtered-out rows included) and their positions in it.
197
+ */
183
198
  getSelectedRows(): {
199
+ selectedIds: string[];
184
200
  selectedIndices: number[];
185
201
  selectedRows: DataRow[];
186
202
  };
187
- /** Select all visible rows (multi mode only). */
203
+ /** The id row selection keeps for `row` — its `rowKey` value as a string, or `#n` for a row without one. */
204
+ getRowId(row: DataRow): string;
205
+ /** Ids of the selected rows — every page, including rows not in `data` now. A copy. */
206
+ get selectedRowIds(): ReadonlySet<string>;
207
+ /**
208
+ * Replaces the row selection with `ids` (single mode keeps the last). Does nothing — and fires
209
+ * nothing — when the selection already is `ids`, so a host can set it from its own state on
210
+ * every render.
211
+ */
212
+ setSelection(ids: Iterable<string>): void;
213
+ /** Selects every row in view — after filtering (multi mode only). Rows on other pages stay as they are. */
188
214
  selectAll(): void;
189
- /** Deselect all rows. */
215
+ /** Deselects every row — on every page. */
190
216
  deselectAll(): void;
191
217
  /**
192
218
  * Selects every currently-loaded row for which `predicate` returns true, in addition to
@@ -200,6 +226,10 @@ export declare class FlexTable extends LitElement {
200
226
  */
201
227
  selectWhere(predicate: (row: DataRow, dataIndex: number) => boolean): void;
202
228
  private _dispatchRowSelectionEvent;
229
+ /** Ids of the rows in view, top to bottom (after filter and sort). Cached per view and `rowKey`. */
230
+ private _visibleRowIds;
231
+ /** Drops selected `#n` ids whose rows `data` no longer holds. Returns whether anything was dropped. */
232
+ private _pruneGoneUnkeyedRows;
203
233
  /**
204
234
  * Set a filter for a column. Replaces any existing filter on the same key.
205
235
  */
@@ -263,8 +293,9 @@ export declare class FlexTable extends LitElement {
263
293
  */
264
294
  addRow(row?: DataRow, index?: number): DataRow | null;
265
295
  /**
266
- * Delete rows at the specified data indices.
267
- * If no indices provided, deletes the currently selected rows.
296
+ * Delete rows at the specified data indices. Without indices it deletes the selected rows: the
297
+ * checked rows when the grid is `selectable` (none checked — nothing is deleted), otherwise the
298
+ * rows of the cell selection.
268
299
  */
269
300
  deleteRows(indices?: number[]): void;
270
301
  /**
@@ -1,3 +1,3 @@
1
- import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-D3E9npd9.js";
1
+ import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-M_oC8OXD.js";
2
2
  import { t as o } from "./locale-Cfm-vJji.js";
3
3
  export { a as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, o as flexTableLocale, r as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-D3E9npd9.js";
1
+ import { t as e } from "./flex-table-M_oC8OXD.js";
2
2
  import { i as t } from "./query-Dw4iO0ir.js";
3
3
  import { t as n } from "./source--q5s2Oti.js";
4
4
  import { t as r } from "./view-DiLr-TdY.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.55.2",
3
+ "version": "0.56.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -78,9 +78,12 @@ Use `updateRows([{ row, key, value }])` for programmatic edits (undoable, fires
78
78
  `table.editable = false` (a property, since a boolean attribute cannot express `false`) — Enter on a non-editable cell then fires
79
79
  `row-activate`, the grid's "open this row" contract.
80
80
 
81
- **Row selection is index-based.** Enable with `selectable` (`selection-mode="single|multi"`).
82
- There is no row key, so set `clear-selection-on-data-change` when selection drives bulk actions and
83
- `data` can be replaced. `selectWhere(predicate)` selects rows by content.
81
+ **Row selection is kept by row id.** Enable with `selectable` (`selection-mode="single|multi"`).
82
+ The id is the row's `row-key` field (`_id` by default, as in `u-rich-table`) or a `rowKey` function, so
83
+ checkmarks follow their rows through sort, filter, insert and delete, and keyed rows stay selected
84
+ across server pages (`selectedIds`). A row without a key is named by its object and leaves the selection
85
+ when `data` drops it. `clear-selection-on-data-change` starts a new selection on every `data`.
86
+ `selectWhere(predicate)` selects rows by content; `setSelection(ids)` sets it from host state.
84
87
 
85
88
  **Client vs server mode.** `data-mode="client"` (default) sorts and filters locally.
86
89
  `data-mode="server"` only emits `sort-change` / `filter-change`; you supply already-processed rows.
@@ -27,7 +27,8 @@ All names below are members of the `FlexTable` class (`<flex-table>`).
27
27
  | `loading` | `loading` | `boolean` | `false` | Loading overlay + `aria-busy` |
28
28
  | `importEnabled` | `import-enabled` | `boolean` | `false` | Drag-and-drop `.xlsx` / `.csv` import |
29
29
  | `clearUndoOnDataChange` | `clear-undo-on-data-change` | `boolean` | `false` | Replacing `data` clears undo history |
30
- | `clearSelectionOnDataChange` | `clear-selection-on-data-change` | `boolean` | `false` | Replacing `data` clears row selection |
30
+ | `clearSelectionOnDataChange` | `clear-selection-on-data-change` | `boolean` | `false` | Replacing `data` clears row selection (otherwise keyed rows stay selected across pages) |
31
+ | `rowKey` | `row-key` | `string \| (row) => unknown` | `'_id'` | Names a row; selection is kept by this id. A row without a key gets a session-local `#n` |
31
32
  | `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Adopted into the shadow root; styles `render` output |
32
33
 
33
34
  Replacing `data` with the same array reference does not trigger the clear-on-change behaviors.
@@ -95,7 +96,7 @@ Enter/Escape and blur-to-commit behavior.
95
96
  | Method | Returns | Notes |
96
97
  |---|---|---|
97
98
  | `addRow(row?, index?)` | `DataRow \| null` | `null` when `maxRows` reached |
98
- | `deleteRows(indices?)` | `void` | Data indices; default = selected rows |
99
+ | `deleteRows(indices?)` | `void` | Data indices; default = the checked rows when `selectable`, else the cell selection's rows |
99
100
  | `updateRows(changes)` | `void` | `Array<{ row, key, value }>`, one undo step |
100
101
  | `refreshData()` | `void` | Re-render after in-place mutation |
101
102
 
@@ -115,9 +116,11 @@ Enter/Escape and blur-to-commit behavior.
115
116
 
116
117
  | Method | Notes |
117
118
  |---|---|
118
- | `selectAll()` / `deselectAll()` | `selectAll` is multi mode only |
119
+ | `selectAll()` / `deselectAll()` | `selectAll`: the rows in view (multi mode only) · `deselectAll`: every page |
119
120
  | `selectWhere(predicate)` | `(row, dataIndex) => boolean` over the visible rows |
120
- | `getSelectedRows()` | `{ selectedIndices, selectedRows }` |
121
+ | `setSelection(ids)` | Replace with these row ids; the same set again fires nothing |
122
+ | `getSelectedRows()` | `{ selectedIds, selectedIndices, selectedRows }` — ids: all pages · rows: the ones `data` holds |
123
+ | `getRowId(row)` · `selectedRowIds` | A row's id · the selected ids (all pages) |
121
124
 
122
125
  ### Filtering, undo, comments, import/export
123
126
 
@@ -147,7 +150,7 @@ cast, and the React `on*` props carry the same types.
147
150
  | `validation-error` | `{ row, col, key, value, error }` |
148
151
  | `row-add` | `{ row, index }` |
149
152
  | `row-delete` | `{ indices, rows }` |
150
- | `row-activate` | `{ row, index, col, key }` — Enter on a non-editable cell |
153
+ | `row-activate` | `{ row, id, index, col, key }` — Enter on a non-editable cell |
151
154
  | `row-reorder` | `{ from, to }` |
152
155
  | `batch-update` | `{ changes }` |
153
156
  | `column-add` | `{ column, index }` |
@@ -159,7 +162,7 @@ cast, and the React `on*` props carry the same types.
159
162
  | `sort-change` | `{ criteria }` |
160
163
  | `filter-change` | `{ keys, filteredCount }` |
161
164
  | `filter-error` | `{ error, row, filterKey }` |
162
- | `selection-change` | `{ selectedIndices, selectedRows }` |
165
+ | `selection-change` | `{ selectedIds, selectedIndices, selectedRows }` |
163
166
  | `clipboard-copy` / `clipboard-cut` | `{ range, text }` (TSV) |
164
167
  | `clipboard-paste` | `{ changes, addedRows }` |
165
168
  | `clipboard-error` | `{ action: 'copy' \| 'paste', error }` — copy could not put the text on the clipboard (no `clipboard-copy`/`clipboard-cut` follows; a cut clears nothing), or paste could not read it |
@@ -78,7 +78,6 @@ function Orders() {
78
78
  loading={source.loading}
79
79
  error={source.error}
80
80
  onSortChange={source.onSortChange}
81
- clearSelectionOnDataChange
82
81
  />
83
82
  <button disabled={source.page === 0} onClick={() => source.setPage(source.page - 1)}>Prev</button>
84
83
  <span>{source.page + 1} / {Math.max(1, Math.ceil(source.totalCount / 20))}</span>