@ixfx/components 0.15.5 → 0.16.1

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 (80) hide show
  1. package/bundle/index.d.ts +986 -49
  2. package/bundle/index.js +765 -729
  3. package/dist/ac-token.js +1 -1
  4. package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
  5. package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
  6. package/dist/crumbs.d.ts +2 -115
  7. package/dist/crumbs.js +1 -1179
  8. package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
  9. package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
  10. package/dist/data-grid.d.ts +3 -3
  11. package/dist/data-grid.js +1 -1
  12. package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
  13. package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
  14. package/dist/detail-list.d.ts +4 -4
  15. package/dist/detail-list.js +1 -1
  16. package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.d.ts} +27 -4
  17. package/dist/flat-list.d.ts +2 -2
  18. package/dist/flat-list.js +123 -4
  19. package/dist/{grid-list-y6dWJYu3.js → grid-list-D0dJNVEF.js} +278 -76
  20. package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
  21. package/dist/grid-list.d.ts +3 -3
  22. package/dist/grid-list.js +1 -1
  23. package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
  24. package/dist/incr-search.d.ts +1 -1
  25. package/dist/incr-search.js +1 -1
  26. package/dist/index-9ttcdwlL.d.ts +71 -0
  27. package/dist/index-Bnm-kKGC.d.ts +475 -0
  28. package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
  29. package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
  30. package/dist/index.d.ts +341 -21
  31. package/dist/index.js +2286 -884
  32. package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
  33. package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
  34. package/dist/miller-DUqiUAWQ.js +2230 -0
  35. package/dist/miller.d.ts +4 -437
  36. package/dist/miller.js +1 -1815
  37. package/dist/narrowed-text.js +1 -1
  38. package/dist/pending-BGidHppL.js +91 -0
  39. package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
  40. package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
  41. package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
  42. package/dist/split-layout.d.ts +1 -1
  43. package/dist/split-layout.js +1 -1
  44. package/dist/sunburst-tree.d.ts +2 -2
  45. package/dist/sunburst-tree.js +1 -1
  46. package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_M0YTEA.js} +299 -80
  47. package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
  48. package/dist/tree-CftHwtEb.js +146 -0
  49. package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
  50. package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
  51. package/dist/tree.d.ts +3 -3
  52. package/dist/tree.js +1 -1
  53. package/dist/types-LU1u0CB8.d.ts +87 -0
  54. package/dist/vertical-list-DyKPKGxK.js +1993 -0
  55. package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.d.ts} +31 -3
  56. package/dist/vertical-list.d.ts +3 -3
  57. package/dist/vertical-list.js +1 -1789
  58. package/dist/view-state-controller-BdyXWUUM.js +305 -0
  59. package/docs-user/README.md +2 -0
  60. package/docs-user/crumbs.md +27 -0
  61. package/docs-user/data-grid.md +79 -5
  62. package/docs-user/detail-list.md +69 -0
  63. package/docs-user/dock.md +63 -12
  64. package/docs-user/flat-list.md +27 -0
  65. package/docs-user/grid-list.md +69 -13
  66. package/docs-user/hyperbolic-tree.md +31 -4
  67. package/docs-user/icicle-tree.md +36 -0
  68. package/docs-user/index.json +12 -2
  69. package/docs-user/llms.txt +2 -0
  70. package/docs-user/miller.md +52 -21
  71. package/docs-user/multicol-list.md +50 -8
  72. package/docs-user/split-layout.md +45 -17
  73. package/docs-user/sunburst-tree.md +32 -2
  74. package/docs-user/tree.md +45 -6
  75. package/docs-user/treemap.md +32 -0
  76. package/docs-user/twosplit-layout.md +22 -0
  77. package/docs-user/user-catalog.md +1 -1
  78. package/docs-user/vertical-list.md +55 -7
  79. package/llms.txt +2 -0
  80. package/package.json +2 -2
@@ -3,10 +3,12 @@ import { n as tickledStyles, t as tickledItemStyles } from "./tickled-styles-BrA
3
3
  import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import "./icon-1gpwtPx8.js";
6
- import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES } from "./incr-search-JIhwB49y.js";
6
+ import { i as ListSearchController, n as listSearchStyles, o as registerListSearchCommands, r as LIST_SEARCH_MODES } from "./incr-search-CgbL-OlS.js";
7
7
  import { t as handleStyles } from "./handle-DAXs7kbJ.js";
8
8
  import { a as listItemFadeKeyframes, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
9
+ import { a as capKeys, l as keysToArray, n as EMPTY_KEYS, o as flagsFromRecord, r as MAX_PENDING_KEYS } from "./pending-BGidHppL.js";
9
10
  import "./interaction-E-uMGHkJ.js";
11
+ import { n as anchorFromRow, o as scrollToOffset, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
10
12
  import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
11
13
  import { n as appearanceColor, t as appearanceAbsent } from "./item-appearance-BjcMyauw.js";
12
14
  import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
@@ -16,6 +18,191 @@ import { LitElement, css, html, nothing } from "lit";
16
18
  import { property, query, state } from "lit/decorators.js";
17
19
  import { createCommandRegistry } from "@clinth/ui-commands";
18
20
  import { repeat } from "lit/directives/repeat.js";
21
+ //#region src/data-grid/data-grid-view-state.ts
22
+ const EMPTY_COLUMNS = /* @__PURE__ */ new Map();
23
+ const EMPTY_ORDER = /* @__PURE__ */ new Map();
24
+ /**
25
+ * Selected keys whose row is gone go pending (deselected silently); pending
26
+ * keys whose row now exists are selected unless `canRestore` is false (e.g.
27
+ * `selectionMode="none"`), in which case they stay pending. With `single`, at
28
+ * most one key stays selected (a restored key wins) and nothing more stays
29
+ * pending.
30
+ */
31
+ function reconcileSelectedKeys(selected, pending, has, single, canRestore = true) {
32
+ if (selected.size === 0 && pending.size === 0) return {
33
+ selected,
34
+ pending,
35
+ restored: false,
36
+ changed: false
37
+ };
38
+ let next;
39
+ let nextPending;
40
+ for (const key of selected) {
41
+ if (has(key)) continue;
42
+ next ??= new Set(selected);
43
+ next.delete(key);
44
+ nextPending ??= new Set(pending);
45
+ nextPending.add(key);
46
+ }
47
+ const present = canRestore ? [...pending].filter(has) : [];
48
+ const resolved = resolvePendingKeys(next ?? selected, nextPending ?? pending, present, single);
49
+ return {
50
+ ...resolved,
51
+ changed: resolved.changed || next !== void 0
52
+ };
53
+ }
54
+ /**
55
+ * Select the `present` pending keys (already known to exist). Used on its own
56
+ * for streamed appends, where only the new slice can match — so the cost is
57
+ * proportional to the slice, not the row count.
58
+ */
59
+ function resolvePendingKeys(selected, pending, present, single) {
60
+ if (present.length === 0) return {
61
+ selected,
62
+ pending: capKeys(pending),
63
+ restored: false,
64
+ changed: false
65
+ };
66
+ const next = new Set(selected);
67
+ const nextPending = new Set(pending);
68
+ for (const key of present) {
69
+ next.add(key);
70
+ nextPending.delete(key);
71
+ }
72
+ if (single) return {
73
+ selected: /* @__PURE__ */ new Set([present.at(-1)]),
74
+ pending: EMPTY_KEYS,
75
+ restored: true,
76
+ changed: true
77
+ };
78
+ return {
79
+ selected: next,
80
+ pending: capKeys(nextPending),
81
+ restored: true,
82
+ changed: true
83
+ };
84
+ }
85
+ /** Pending keys that appear in `sliceKeys` — iterates the slice, not the pending set. */
86
+ function matchSlice(pending, sliceKeys) {
87
+ if (pending.size === 0) return [];
88
+ return sliceKeys.filter((k) => pending.has(k));
89
+ }
90
+ /** Collapsed group keys as an `expanded` record (`false` = collapsed). */
91
+ function collapsedToRecord(collapsed, include = () => true) {
92
+ const out = {};
93
+ for (const key of collapsed) if (include(key)) out[key] = false;
94
+ return out;
95
+ }
96
+ /** `expanded` record → collapsed group keys. Groups default open, so only `false` matters. */
97
+ function collapsedFromRecord(record) {
98
+ const keys = [...flagsFromRecord(record)].filter(([, open]) => !open).map(([k]) => k);
99
+ return new Set(capKeys(new Set(keys)));
100
+ }
101
+ /** Parse a saved `columns` record into pending layout and a saved order. Invalid values are dropped. */
102
+ function columnStateFromRecord(record) {
103
+ const pending = /* @__PURE__ */ new Map();
104
+ const order = /* @__PURE__ */ new Map();
105
+ for (const [key, s] of Object.entries(record)) {
106
+ if (s === null || typeof s !== `object`) continue;
107
+ const width = typeof s.width === `number` && Number.isFinite(s.width) && s.width > 0 ? s.width : void 0;
108
+ const visible = typeof s.visible === `boolean` ? s.visible : void 0;
109
+ if (width !== void 0 || visible !== void 0) pending.set(key, {
110
+ ...width === void 0 ? {} : { width },
111
+ ...visible === void 0 ? {} : { visible }
112
+ });
113
+ if (typeof s.order === `number` && Number.isFinite(s.order)) order.set(key, s.order);
114
+ }
115
+ return {
116
+ pending,
117
+ order
118
+ };
119
+ }
120
+ /**
121
+ * - columns in `prev` but not in `columns` → their width / visibility go pending
122
+ * - pending layout for columns now defined → applied (on a copy of the column)
123
+ * - a saved `order` reorders the columns it names within the slots they
124
+ * occupy, leaving other columns where they are; it is dropped once every
125
+ * column it names is defined (so the host can reorder freely afterwards)
126
+ *
127
+ * Returns the input array when nothing changed, so `columns` identity (a sort
128
+ * input) only changes when it must.
129
+ */
130
+ function reconcileColumns(prev, columns, pending, order) {
131
+ const keys = new Set(columns.map((c) => c.key));
132
+ let nextPending;
133
+ for (const col of prev) {
134
+ if (keys.has(col.key) || pending.has(col.key)) continue;
135
+ nextPending ??= new Map(pending);
136
+ nextPending.set(col.key, layoutOf(col));
137
+ }
138
+ let next;
139
+ columns.forEach((col, i) => {
140
+ const saved = (nextPending ?? pending).get(col.key);
141
+ if (!saved) return;
142
+ nextPending ??= new Map(pending);
143
+ nextPending.delete(col.key);
144
+ if ((saved.width === void 0 || saved.width === col.width) && (saved.visible === void 0 || saved.visible === (col.visible !== false))) return;
145
+ next ??= [...columns];
146
+ next[i] = {
147
+ ...col,
148
+ ...saved
149
+ };
150
+ });
151
+ let nextOrder = order;
152
+ if (order.size > 0) {
153
+ const working = next ?? columns;
154
+ const slots = working.flatMap((c, i) => order.has(c.key) ? [i] : []);
155
+ const ranked = slots.map((i) => working[i]).sort((a, b) => order.get(a.key) - order.get(b.key));
156
+ if (ranked.some((c, j) => c !== working[slots[j]])) {
157
+ next ??= [...columns];
158
+ slots.forEach((slot, j) => {
159
+ next[slot] = ranked[j];
160
+ });
161
+ }
162
+ if ([...order.keys()].every((k) => keys.has(k))) nextOrder = EMPTY_ORDER;
163
+ }
164
+ return {
165
+ columns: next ?? columns,
166
+ pending: nextPending ? capMap(nextPending) : pending,
167
+ order: nextOrder,
168
+ changed: next !== void 0 || nextPending !== void 0 || nextOrder !== order
169
+ };
170
+ }
171
+ /** Defined columns (and, with `pending`, saved layout for undefined ones) as a JSON record. */
172
+ function captureColumns(columns, pending, order, includePending) {
173
+ const out = {};
174
+ if (includePending) {
175
+ for (const [key, layout] of pending) out[key] = withOrder(layout, order.get(key));
176
+ for (const [key, rank] of order) if (!(key in out) && !columns.some((c) => c.key === key)) out[key] = { order: rank };
177
+ }
178
+ columns.forEach((col, i) => {
179
+ out[col.key] = withOrder(layoutOf(col), order.size > 0 ? order.get(col.key) : i);
180
+ });
181
+ return out;
182
+ }
183
+ /** Whether `value` is a usable sort state (`null` = unsorted). */
184
+ function isSortState(value) {
185
+ if (value === null) return true;
186
+ if (typeof value !== `object`) return false;
187
+ const s = value;
188
+ return typeof s.key === `string` && (s.direction === `asc` || s.direction === `desc`);
189
+ }
190
+ function layoutOf(col) {
191
+ return {
192
+ ...col.width === void 0 ? {} : { width: col.width },
193
+ visible: col.visible !== false
194
+ };
195
+ }
196
+ function withOrder(layout, order) {
197
+ return order === void 0 ? { ...layout } : {
198
+ ...layout,
199
+ order
200
+ };
201
+ }
202
+ function capMap(map, max = MAX_PENDING_KEYS) {
203
+ return map.size <= max ? map : new Map([...map].slice(map.size - max));
204
+ }
205
+ //#endregion
19
206
  //#region src/data-grid/shift-wheel.ts
20
207
  /**
21
208
  * Shift + vertical mouse wheel → horizontal scroll.
@@ -66,18 +253,28 @@ function attachShiftWheel(getScroller) {
66
253
  }
67
254
  //#endregion
68
255
  //#region src/data-grid/data-grid.ts
256
+ /** `CSS.highlights` key for incremental-search matches, styled in `listSearchStyles`. */
257
+ const SEARCH_HIGHLIGHT_KEY = `data-grid-search`;
69
258
  const EMPTY_CELL_LOOK = {
70
259
  textColor: void 0,
71
260
  icon: void 0,
72
261
  iconColor: void 0,
73
262
  badge: void 0
74
263
  };
264
+ /**
265
+ * Default `getRowKey`: `row.key`, then `row.id`, else the row's sorted index.
266
+ * Index keys aren't stable, so view state never records them.
267
+ */
268
+ const defaultGetRowKey = (r, i) => r.key ?? r.id ?? String(i);
75
269
  /** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
76
270
  function sameKeys(a, b) {
77
271
  if (a === void 0 || a.length !== b.length) return false;
78
272
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
79
273
  return true;
80
274
  }
275
+ function sameSet(a, b) {
276
+ return a.size === b.size && [...a].every((k) => b.has(k));
277
+ }
81
278
  /** Element-wise reference equality, treating `undefined` (never computed) as different. */
82
279
  function sameRefs(a, b) {
83
280
  return a !== void 0 && a.length === b.length && a.every((v, i) => v === b[i]);
@@ -91,7 +288,7 @@ let DataGridElement = class DataGridElement extends LitElement {
91
288
  this.interactionMode = `standard`;
92
289
  this.sortState = null;
93
290
  this.rowHeight = 28;
94
- this.getRowKey = (r, i) => r.key ?? r.id ?? String(i);
291
+ this.getRowKey = defaultGetRowKey;
95
292
  this.editable = false;
96
293
  this.editability = `user`;
97
294
  this.cellOverflow = `ellipsis`;
@@ -103,6 +300,7 @@ let DataGridElement = class DataGridElement extends LitElement {
103
300
  this.textColorColumn = ``;
104
301
  this.iconColumn = ``;
105
302
  this.badgeColumn = ``;
303
+ this.searchColumn = ``;
106
304
  this.itemsDraggable = false;
107
305
  this.columnButtonIcon = `ellipsis`;
108
306
  this._columnFormatters = /* @__PURE__ */ new Map();
@@ -185,6 +383,15 @@ let DataGridElement = class DataGridElement extends LitElement {
185
383
  this.#viewportChangeRaf = 0;
186
384
  this.#resortRaf = 0;
187
385
  this._drag = null;
386
+ this.#viewState = new ViewStateController(this, {
387
+ kind: `data-grid`,
388
+ capture: () => this.getViewState()
389
+ });
390
+ this.#pendingSelected = EMPTY_KEYS;
391
+ this.#pendingColumns = EMPTY_COLUMNS;
392
+ this.#columnOrder = EMPTY_ORDER;
393
+ this.#reconciledColumns = [];
394
+ this.#groupKeys = /* @__PURE__ */ new Set();
188
395
  this._onFocusOut = (e) => {
189
396
  if (this._suppressBlurCommit) return;
190
397
  if (this.editability === `immediate`) return;
@@ -204,180 +411,18 @@ let DataGridElement = class DataGridElement extends LitElement {
204
411
  this._scrollLeft = el.scrollLeft;
205
412
  const after = this.#windowRows(2);
206
413
  if (before.start !== after.start || before.end !== after.end) this.requestUpdate();
414
+ if (!this.#viewState.pendingScroll) this.#viewState.notify();
207
415
  cancelAnimationFrame(this.#viewportChangeRaf);
208
416
  this.#viewportChangeRaf = 0;
209
417
  this.#emitViewportChangeIfChanged();
210
418
  });
211
419
  };
212
420
  this._onKeyDown = (e) => {
213
- if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === `a`) {
214
- if (this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode` || this.interactionMode === `standard`) {
215
- e.preventDefault();
216
- const prev = new Set(this._selectedKeys);
217
- if (this.groupBy && !this.crossGroupSelection && this._anchorKey) {
218
- const g = this._getGroupKey(this._getRowForKey(this._anchorKey) ?? { key: this._anchorKey });
219
- this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row` && this._getGroupKey(f.row) === g).map((f) => f.key));
220
- } else this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
221
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
222
- detail: {
223
- selectedKeys: new Set(this._selectedKeys),
224
- previous: prev
225
- },
226
- bubbles: true,
227
- composed: true
228
- }));
229
- }
230
- return;
231
- }
232
- if (this._editing && (e.key === `ArrowDown` || e.key === `ArrowUp`)) {
233
- if ((this.shadowRoot?.activeElement)?.tagName === `SELECT`) return;
234
- e.preventDefault();
235
- const cur = this._editing;
236
- const dir = e.key === `ArrowDown` ? 1 : -1;
237
- const colKey = cur.colKey;
238
- this._suppressBlurCommit = true;
239
- this._commitEdit();
240
- const rowKeys = this._flat.filter((f) => f.type === `row`).map((f) => f.key);
241
- const idx = rowKeys.indexOf(cur.rowKey);
242
- let nextIdx = idx + dir;
243
- while (nextIdx >= 0 && nextIdx < rowKeys.length) {
244
- const nk = rowKeys[nextIdx];
245
- const row = this.#rowEntry(nk)?.row;
246
- const col = this._visibleColumns.find((c) => c.key === colKey);
247
- if (row && col && this._isEditable(row, col)) {
248
- this._startEdit(nk, colKey);
249
- this._tickled.handleKeyboardNavigation(nextIdx > idx ? `next` : `previous`);
250
- this._scrollRowIntoView(nk);
251
- break;
252
- }
253
- nextIdx += dir;
254
- }
255
- if (nextIdx < 0 || nextIdx >= rowKeys.length) {
256
- this._tickled.handleKeyboardNavigation(dir > 0 ? `next` : `previous`);
257
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
258
- }
259
- requestAnimationFrame(() => {
260
- this._suppressBlurCommit = false;
261
- });
262
- return;
263
- }
264
- if (e.key === `ArrowDown` || e.key === `ArrowUp` || e.key === `Home` || e.key === `End`) {
265
- e.preventDefault();
266
- this._tickled.setInputMode(`keyboard`);
267
- }
268
- switch (e.key) {
269
- case `ArrowDown`:
270
- this._tickled.handleKeyboardNavigation(`next`);
271
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
272
- if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
273
- if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
274
- break;
275
- case `ArrowUp`:
276
- this._tickled.handleKeyboardNavigation(`previous`);
277
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
278
- if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
279
- if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
280
- break;
281
- case `Home`:
282
- this._tickled.handleKeyboardNavigation(`first`);
283
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
284
- break;
285
- case `End`:
286
- this._tickled.handleKeyboardNavigation(`last`);
287
- if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
288
- break;
289
- case `Enter`: {
290
- const key = this._tickled.tickledId;
291
- if (!key) break;
292
- e.preventDefault();
293
- if (!this._editing && this.editability === `user`) {
294
- const entry = this.#rowEntry(key);
295
- if (entry) {
296
- const row = entry.row;
297
- if (this._lastClickedCell?.rowKey === key) {
298
- const col = this._visibleColumns.find((c) => c.key === this._lastClickedCell.colKey);
299
- if (col && this._isEditable(row, col)) {
300
- this._startEdit(key, col.key);
301
- break;
302
- }
303
- }
304
- const firstEditable = this._visibleColumns.find((c) => this._isEditable(row, c));
305
- if (firstEditable) {
306
- this._startEdit(key, firstEditable.key);
307
- break;
308
- }
309
- }
310
- }
311
- if (!this._editing) {
312
- const entry = this.#rowEntry(key);
313
- const idx = entry?.idx ?? -1;
314
- if (this._selectedKeys.has(key)) {
315
- this._activateRow(key, entry?.row, idx);
316
- break;
317
- }
318
- if (this.interactionMode !== `manual` && this.interactionMode !== `checked`) this._replaceSelection(key);
319
- this.dispatchEvent(new CustomEvent(`datagrid-cell-click`, {
320
- detail: {
321
- rowKey: key,
322
- colKey: ``,
323
- rowIndex: idx,
324
- colIndex: -1
325
- },
326
- bubbles: true,
327
- composed: true
328
- }));
329
- }
330
- break;
331
- }
332
- case ` `: {
333
- if (this._editing) {
334
- if ((this.columns.find((c) => c.key === this._editing.colKey)?.dataType ?? `text`) === `boolean`) {
335
- e.preventDefault();
336
- const isImmediate = this.editability === `immediate`;
337
- const rowKey = this._editing.rowKey;
338
- const colKey = this._editing.colKey;
339
- const active = this.shadowRoot?.activeElement;
340
- let next;
341
- if (active?.type === `checkbox`) {
342
- next = !active.checked;
343
- active.checked = next;
344
- } else {
345
- const row = this.#rowEntry(rowKey)?.row;
346
- next = !(this._editValue !== `` ? Boolean(this._editValue) : Boolean(row?.[colKey]));
347
- }
348
- if (isImmediate) this._commitImmediate(rowKey, colKey, next);
349
- else {
350
- this._editValue = next;
351
- this._commitEdit();
352
- }
353
- }
354
- break;
355
- }
356
- const key = this._tickled.tickledId;
357
- if (!key) break;
358
- e.preventDefault();
359
- if (this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `standard` || this.interactionMode === `vscode` || this.interactionMode === `manual`) this._toggleSelection(key);
360
- else this._replaceSelection(key);
361
- break;
362
- }
363
- case `Escape`: if (this._editing || this._editingRow) {
364
- e.preventDefault();
365
- this._editing = null;
366
- this._editingRow = null;
367
- this.requestUpdate();
368
- } else if (this._selectedKeys.size) {
369
- e.preventDefault();
370
- const prev = new Set(this._selectedKeys);
371
- this._selectedKeys = /* @__PURE__ */ new Set();
372
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
373
- detail: {
374
- selectedKeys: /* @__PURE__ */ new Set(),
375
- previous: prev
376
- },
377
- bubbles: true,
378
- composed: true
379
- }));
380
- }
421
+ const before = this._tickled.tickledId;
422
+ this.#handleKeyDown(e);
423
+ if (this._tickled.tickledId !== before) {
424
+ this.#pendingFocus = void 0;
425
+ this.#viewState.notify();
381
426
  }
382
427
  };
383
428
  this._suppressBlurCommit = false;
@@ -660,8 +705,13 @@ let DataGridElement = class DataGridElement extends LitElement {
660
705
  }
661
706
  #rowSearchText(row) {
662
707
  if (this.rowSearchText) return this.rowSearchText(row);
663
- const rec = row;
664
- return this._visibleColumns.map((c) => String(rec[c.key] ?? ``)).join(` `);
708
+ const col = this.#searchColumn();
709
+ if (col === void 0) return ``;
710
+ const value = this._formatValue(row[col.key], col, row);
711
+ return typeof value === `string` || typeof value === `number` ? String(value) : ``;
712
+ }
713
+ #searchColumn() {
714
+ return (this.searchColumn === `` ? void 0 : this._visibleColumns.find((c) => c.key === this.searchColumn)) ?? this._visibleColumns[0];
665
715
  }
666
716
  #createSearchAdapter() {
667
717
  return {
@@ -685,7 +735,12 @@ let DataGridElement = class DataGridElement extends LitElement {
685
735
  const k = this.#searchKeyByRow.get(row);
686
736
  return k === void 0 || matched.has(k);
687
737
  };
688
- }
738
+ },
739
+ highlightRoot: () => this.rowSearchText ? void 0 : this.renderRoot,
740
+ highlightSelector: `.row .cell`,
741
+ highlightKeyOf: (el) => el.getAttribute(`data-col`) === this.#searchColumn()?.key ? el.closest(`.row`)?.getAttribute(`data-key`) ?? void 0 : void 0,
742
+ highlightText: (el) => Array.from(el.childNodes, (n) => n.nodeType === Node.TEXT_NODE ? n.textContent : ``).join(``),
743
+ highlightKey: SEARCH_HIGHLIGHT_KEY
689
744
  };
690
745
  }
691
746
  #effectiveSearchModes() {
@@ -711,6 +766,21 @@ let DataGridElement = class DataGridElement extends LitElement {
711
766
  #lastViewportVisibleKeys;
712
767
  #resortRaf;
713
768
  #detachShiftWheel;
769
+ #viewState;
770
+ /** Saved selection for row keys not (yet) in `rows`. */
771
+ #pendingSelected;
772
+ #pendingFocus;
773
+ /** Saved width / visibility for column keys not (yet) in `columns`. */
774
+ #pendingColumns;
775
+ /** Saved column order, kept until every column it names is defined. */
776
+ #columnOrder;
777
+ /** `columns` as of the last column reconcile, to spot removed columns. */
778
+ #reconciledColumns;
779
+ /** Every row by key, built lazily (only when a key isn't in `_flat`); reset on resort. */
780
+ #rowIndex;
781
+ #rowIndexKeyFn;
782
+ /** Group keys in the current flat model. */
783
+ #groupKeys;
714
784
  connectedCallback() {
715
785
  super.connectedCallback();
716
786
  if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
@@ -754,6 +824,7 @@ let DataGridElement = class DataGridElement extends LitElement {
754
824
  this.#viewportResizeObserver = new ResizeObserver(() => this.#measureViewport());
755
825
  this.#viewportResizeObserver.observe(this._viewportEl);
756
826
  this.#measureViewport();
827
+ this.#viewState.observeScrollTarget(this._viewportEl);
757
828
  }
758
829
  /**
759
830
  * Re-read the viewport's size and update `_viewportH`/`_viewportW` if it
@@ -788,6 +859,8 @@ let DataGridElement = class DataGridElement extends LitElement {
788
859
  }
789
860
  updated(changed) {
790
861
  super.updated(changed);
862
+ this.#viewState.applyPendingScroll((s) => this.#applyScroll(s));
863
+ if (this.search.isOpen) this.search.repaintHighlights();
791
864
  if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`) || changed.has(`filterPredicate`) || changed.has(`rowHeight`)) this.#scheduleViewportChangeEmit();
792
865
  if (changed.has(`_editing`) || changed.has(`_editingRow`)) this.updateComplete.then(() => {
793
866
  let sel = null;
@@ -803,7 +876,8 @@ let DataGridElement = class DataGridElement extends LitElement {
803
876
  super.willUpdate(changed);
804
877
  this.setAttribute(`data-input-mode`, this._tickled.inputMode);
805
878
  if (changed.has(`searchModes`)) this.#syncSearchCommands();
806
- this.#syncDerivedRows();
879
+ if (changed.has(`columns`)) this.#reconcileColumns();
880
+ if (this.#syncDerivedRows()) this.#reconcileRows();
807
881
  this._visibleColumns = this.columns.filter((c) => c.visible !== false);
808
882
  let off = 0;
809
883
  this._colOffsets = this._visibleColumns.map((c) => {
@@ -840,11 +914,14 @@ let DataGridElement = class DataGridElement extends LitElement {
840
914
  if (resort) {
841
915
  this.#sortInputs = sortInputs;
842
916
  this._sortedRows = this._applySort([...this.rows]);
917
+ this.#rowIndex = void 0;
843
918
  }
844
919
  if (resort || !sameRefs(this.#flatInputs, flatInputs)) {
845
920
  this.#flatInputs = flatInputs;
846
921
  this._rebuildFlat();
922
+ return true;
847
923
  }
924
+ return false;
848
925
  }
849
926
  /** Mark the derived rows as matching the current inputs (after an incremental update). */
850
927
  #markDerivedRowsSynced() {
@@ -904,6 +981,7 @@ let DataGridElement = class DataGridElement extends LitElement {
904
981
  for (let i = 0; i < this._sortedRows.length; i++) if (!pred || pred(this._sortedRows[i])) sourceIdx.push(i);
905
982
  const flat = [];
906
983
  const keys = [];
984
+ this.#groupKeys = /* @__PURE__ */ new Set();
907
985
  const pushRow = (idx) => {
908
986
  const row = this._sortedRows[idx];
909
987
  const key = this.getRowKey(row, idx);
@@ -924,6 +1002,7 @@ let DataGridElement = class DataGridElement extends LitElement {
924
1002
  if (arr) arr.push(idx);
925
1003
  else groups.set(k, [idx]);
926
1004
  }
1005
+ this.#groupKeys = new Set(groups.keys());
927
1006
  for (const [gKey, idxs] of groups) {
928
1007
  const label = this.groupLabel ? this.groupLabel(gKey, idxs.map((i) => this._sortedRows[i])) : gKey;
929
1008
  const gId = `__group__${gKey}`;
@@ -963,6 +1042,7 @@ let DataGridElement = class DataGridElement extends LitElement {
963
1042
  if (next.has(gKey)) next.delete(gKey);
964
1043
  else next.add(gKey);
965
1044
  this._collapsedGroups = next;
1045
+ this.#viewState.notify();
966
1046
  }
967
1047
  _applySort(rows) {
968
1048
  if (!this.sortState) return rows;
@@ -1001,6 +1081,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1001
1081
  };
1002
1082
  else next = null;
1003
1083
  this.sortState = next;
1084
+ this.#viewState.notify();
1004
1085
  this.dispatchEvent(new CustomEvent(`datagrid-sort`, {
1005
1086
  detail: {
1006
1087
  sort: next,
@@ -1071,6 +1152,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1071
1152
  _onVisibilityToggle(col, checked) {
1072
1153
  col.visible = checked;
1073
1154
  this.columns = [...this.columns];
1155
+ this.#viewState.notify();
1074
1156
  this.dispatchEvent(new CustomEvent(`datagrid-column-visibility`, {
1075
1157
  detail: {
1076
1158
  key: col.key,
@@ -1175,6 +1257,8 @@ let DataGridElement = class DataGridElement extends LitElement {
1175
1257
  const [moved] = vis.splice(fromIdx, 1);
1176
1258
  vis.splice(toIdx, 0, moved);
1177
1259
  this.columns = [...vis, ...hidden];
1260
+ this.#columnOrder = EMPTY_ORDER;
1261
+ this.#viewState.notify();
1178
1262
  this.dispatchEvent(new CustomEvent(`datagrid-column-reorder`, {
1179
1263
  detail: {
1180
1264
  from: fromIdx,
@@ -1222,10 +1306,10 @@ let DataGridElement = class DataGridElement extends LitElement {
1222
1306
  * set when the selection actually changes.
1223
1307
  */
1224
1308
  selectKeys(keys) {
1309
+ this.#pendingSelected = EMPTY_KEYS;
1225
1310
  let next = new Set([...keys].filter((k) => this._getRowForKey(k) !== void 0));
1226
- const multi = this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
1227
1311
  if (this.selectionMode === `none`) next = /* @__PURE__ */ new Set();
1228
- else if (!multi && next.size > 1) {
1312
+ else if (!this.#isMultiSelect() && next.size > 1) {
1229
1313
  const last = [...next].at(-1);
1230
1314
  next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
1231
1315
  }
@@ -1233,10 +1317,18 @@ let DataGridElement = class DataGridElement extends LitElement {
1233
1317
  if (prev.size === next.size && [...next].every((k) => prev.has(k))) return;
1234
1318
  this._selectedKeys = next;
1235
1319
  this._anchorKey = [...next].at(-1);
1320
+ this.#dispatchSelect(prev);
1321
+ }
1322
+ #isMultiSelect() {
1323
+ return this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
1324
+ }
1325
+ #dispatchSelect(previous) {
1326
+ if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
1327
+ this.#viewState.notify();
1236
1328
  this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1237
1329
  detail: {
1238
- selectedKeys: new Set(next),
1239
- previous: prev
1330
+ selectedKeys: new Set(this._selectedKeys),
1331
+ previous
1240
1332
  },
1241
1333
  bubbles: true,
1242
1334
  composed: true
@@ -1251,6 +1343,8 @@ let DataGridElement = class DataGridElement extends LitElement {
1251
1343
  if (!this.#flatIndexByKey.has(key) || key.startsWith(`__group__`)) return;
1252
1344
  this._tickled.focus(key);
1253
1345
  this._scrollRowIntoView(key);
1346
+ this.#pendingFocus = void 0;
1347
+ this.#viewState.notify();
1254
1348
  }
1255
1349
  /**
1256
1350
  * Append rows without the full O(n log n) rebuild that reassigning `rows`
@@ -1273,6 +1367,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1273
1367
  this._sortedRows = [...this._sortedRows, ...appended];
1274
1368
  this.#appendToFlat(appended, startIdx);
1275
1369
  this.#markDerivedRowsSynced();
1370
+ this.#resolveAppended(appended, startIdx);
1276
1371
  if (this.sortState != null) {
1277
1372
  cancelAnimationFrame(this.#resortRaf);
1278
1373
  this.#resortRaf = requestAnimationFrame(() => {
@@ -1311,14 +1406,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1311
1406
  this._selectedKeys = /* @__PURE__ */ new Set([key]);
1312
1407
  } else this._selectedKeys = /* @__PURE__ */ new Set([key]);
1313
1408
  this._anchorKey = key;
1314
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1315
- detail: {
1316
- selectedKeys: new Set(this._selectedKeys),
1317
- previous: prev
1318
- },
1319
- bubbles: true,
1320
- composed: true
1321
- }));
1409
+ this.#dispatchSelect(prev);
1322
1410
  }
1323
1411
  _toggleSelection(key) {
1324
1412
  const prev = new Set(this._selectedKeys);
@@ -1334,14 +1422,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1334
1422
  }
1335
1423
  this._selectedKeys = next;
1336
1424
  this._anchorKey = key;
1337
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1338
- detail: {
1339
- selectedKeys: new Set(this._selectedKeys),
1340
- previous: prev
1341
- },
1342
- bubbles: true,
1343
- composed: true
1344
- }));
1425
+ this.#dispatchSelect(prev);
1345
1426
  }
1346
1427
  _rangeSelectTo(key) {
1347
1428
  if (!this._anchorKey) {
@@ -1372,28 +1453,171 @@ let DataGridElement = class DataGridElement extends LitElement {
1372
1453
  const hi = Math.max(ga, gb);
1373
1454
  const prev = new Set(this._selectedKeys);
1374
1455
  this._selectedKeys = new Set(groupKeys.slice(lo, hi + 1));
1375
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1376
- detail: {
1377
- selectedKeys: new Set(this._selectedKeys),
1378
- previous: prev
1379
- },
1380
- bubbles: true,
1381
- composed: true
1382
- }));
1456
+ this.#dispatchSelect(prev);
1383
1457
  return;
1384
1458
  }
1385
1459
  const lo = Math.min(a, b);
1386
1460
  const hi = Math.max(a, b);
1387
1461
  const prev = new Set(this._selectedKeys);
1388
1462
  this._selectedKeys = new Set(keys.slice(lo, hi + 1));
1389
- this.dispatchEvent(new CustomEvent(`datagrid-select`, {
1390
- detail: {
1391
- selectedKeys: new Set(this._selectedKeys),
1392
- previous: prev
1393
- },
1394
- bubbles: true,
1395
- composed: true
1396
- }));
1463
+ this.#dispatchSelect(prev);
1464
+ }
1465
+ #handleKeyDown(e) {
1466
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === `a`) {
1467
+ if (this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode` || this.interactionMode === `standard`) {
1468
+ e.preventDefault();
1469
+ const prev = new Set(this._selectedKeys);
1470
+ if (this.groupBy && !this.crossGroupSelection && this._anchorKey) {
1471
+ const g = this._getGroupKey(this._getRowForKey(this._anchorKey) ?? { key: this._anchorKey });
1472
+ this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row` && this._getGroupKey(f.row) === g).map((f) => f.key));
1473
+ } else this._selectedKeys = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
1474
+ this.#dispatchSelect(prev);
1475
+ }
1476
+ return;
1477
+ }
1478
+ if (this._editing && (e.key === `ArrowDown` || e.key === `ArrowUp`)) {
1479
+ if ((this.shadowRoot?.activeElement)?.tagName === `SELECT`) return;
1480
+ e.preventDefault();
1481
+ const cur = this._editing;
1482
+ const dir = e.key === `ArrowDown` ? 1 : -1;
1483
+ const colKey = cur.colKey;
1484
+ this._suppressBlurCommit = true;
1485
+ this._commitEdit();
1486
+ const rowKeys = this._flat.filter((f) => f.type === `row`).map((f) => f.key);
1487
+ const idx = rowKeys.indexOf(cur.rowKey);
1488
+ let nextIdx = idx + dir;
1489
+ while (nextIdx >= 0 && nextIdx < rowKeys.length) {
1490
+ const nk = rowKeys[nextIdx];
1491
+ const row = this.#rowEntry(nk)?.row;
1492
+ const col = this._visibleColumns.find((c) => c.key === colKey);
1493
+ if (row && col && this._isEditable(row, col)) {
1494
+ this._startEdit(nk, colKey);
1495
+ this._tickled.handleKeyboardNavigation(nextIdx > idx ? `next` : `previous`);
1496
+ this._scrollRowIntoView(nk);
1497
+ break;
1498
+ }
1499
+ nextIdx += dir;
1500
+ }
1501
+ if (nextIdx < 0 || nextIdx >= rowKeys.length) {
1502
+ this._tickled.handleKeyboardNavigation(dir > 0 ? `next` : `previous`);
1503
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1504
+ }
1505
+ requestAnimationFrame(() => {
1506
+ this._suppressBlurCommit = false;
1507
+ });
1508
+ return;
1509
+ }
1510
+ if (e.key === `ArrowDown` || e.key === `ArrowUp` || e.key === `Home` || e.key === `End`) {
1511
+ e.preventDefault();
1512
+ this._tickled.setInputMode(`keyboard`);
1513
+ }
1514
+ switch (e.key) {
1515
+ case `ArrowDown`:
1516
+ this._tickled.handleKeyboardNavigation(`next`);
1517
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1518
+ if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
1519
+ if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
1520
+ break;
1521
+ case `ArrowUp`:
1522
+ this._tickled.handleKeyboardNavigation(`previous`);
1523
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1524
+ if (this.interactionMode === `implicit` && this._tickled.tickledId) this._replaceSelection(this._tickled.tickledId);
1525
+ if (e.shiftKey && (this.interactionMode === `standard` || this.interactionMode === `vscode`) && this._tickled.tickledId) this._rangeSelectTo(this._tickled.tickledId);
1526
+ break;
1527
+ case `Home`:
1528
+ this._tickled.handleKeyboardNavigation(`first`);
1529
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1530
+ break;
1531
+ case `End`:
1532
+ this._tickled.handleKeyboardNavigation(`last`);
1533
+ if (this._tickled.tickledId) this._scrollRowIntoView(this._tickled.tickledId);
1534
+ break;
1535
+ case `Enter`: {
1536
+ const key = this._tickled.tickledId;
1537
+ if (!key) break;
1538
+ e.preventDefault();
1539
+ if (!this._editing && this.editability === `user`) {
1540
+ const entry = this.#rowEntry(key);
1541
+ if (entry) {
1542
+ const row = entry.row;
1543
+ if (this._lastClickedCell?.rowKey === key) {
1544
+ const col = this._visibleColumns.find((c) => c.key === this._lastClickedCell.colKey);
1545
+ if (col && this._isEditable(row, col)) {
1546
+ this._startEdit(key, col.key);
1547
+ break;
1548
+ }
1549
+ }
1550
+ const firstEditable = this._visibleColumns.find((c) => this._isEditable(row, c));
1551
+ if (firstEditable) {
1552
+ this._startEdit(key, firstEditable.key);
1553
+ break;
1554
+ }
1555
+ }
1556
+ }
1557
+ if (!this._editing) {
1558
+ const entry = this.#rowEntry(key);
1559
+ const idx = entry?.idx ?? -1;
1560
+ if (this._selectedKeys.has(key)) {
1561
+ this._activateRow(key, entry?.row, idx);
1562
+ break;
1563
+ }
1564
+ if (this.interactionMode !== `manual` && this.interactionMode !== `checked`) this._replaceSelection(key);
1565
+ this.dispatchEvent(new CustomEvent(`datagrid-cell-click`, {
1566
+ detail: {
1567
+ rowKey: key,
1568
+ colKey: ``,
1569
+ rowIndex: idx,
1570
+ colIndex: -1
1571
+ },
1572
+ bubbles: true,
1573
+ composed: true
1574
+ }));
1575
+ }
1576
+ break;
1577
+ }
1578
+ case ` `: {
1579
+ if (this._editing) {
1580
+ if ((this.columns.find((c) => c.key === this._editing.colKey)?.dataType ?? `text`) === `boolean`) {
1581
+ e.preventDefault();
1582
+ const isImmediate = this.editability === `immediate`;
1583
+ const rowKey = this._editing.rowKey;
1584
+ const colKey = this._editing.colKey;
1585
+ const active = this.shadowRoot?.activeElement;
1586
+ let next;
1587
+ if (active?.type === `checkbox`) {
1588
+ next = !active.checked;
1589
+ active.checked = next;
1590
+ } else {
1591
+ const row = this.#rowEntry(rowKey)?.row;
1592
+ next = !(this._editValue !== `` ? Boolean(this._editValue) : Boolean(row?.[colKey]));
1593
+ }
1594
+ if (isImmediate) this._commitImmediate(rowKey, colKey, next);
1595
+ else {
1596
+ this._editValue = next;
1597
+ this._commitEdit();
1598
+ }
1599
+ }
1600
+ break;
1601
+ }
1602
+ const key = this._tickled.tickledId;
1603
+ if (!key) break;
1604
+ e.preventDefault();
1605
+ if (this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `standard` || this.interactionMode === `vscode` || this.interactionMode === `manual`) this._toggleSelection(key);
1606
+ else this._replaceSelection(key);
1607
+ break;
1608
+ }
1609
+ case `Escape`: if (this._editing || this._editingRow) {
1610
+ e.preventDefault();
1611
+ this._editing = null;
1612
+ this._editingRow = null;
1613
+ this.requestUpdate();
1614
+ } else if (this._selectedKeys.size) {
1615
+ e.preventDefault();
1616
+ const prev = new Set(this._selectedKeys);
1617
+ this._selectedKeys = /* @__PURE__ */ new Set();
1618
+ this.#dispatchSelect(prev);
1619
+ }
1620
+ }
1397
1621
  }
1398
1622
  _isEditable(row, col) {
1399
1623
  if (this.editability === `disabled`) return false;
@@ -1609,6 +1833,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1609
1833
  const w = this._measureAutoWidth(col);
1610
1834
  col.width = w;
1611
1835
  this.requestUpdate();
1836
+ this.#viewState.notify();
1612
1837
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1613
1838
  detail: {
1614
1839
  key: col.key,
@@ -1636,6 +1861,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1636
1861
  requestAnimationFrame(() => {
1637
1862
  col.width = target;
1638
1863
  this.requestUpdate();
1864
+ this.#viewState.notify();
1639
1865
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1640
1866
  detail: {
1641
1867
  key,
@@ -1672,6 +1898,7 @@ let DataGridElement = class DataGridElement extends LitElement {
1672
1898
  window.removeEventListener(`pointerup`, onUp);
1673
1899
  this._resizingColKey = void 0;
1674
1900
  const w = Math.max(col.minWidth ?? 40, Math.min(col.maxWidth ?? 600, startW + ev.clientX - startX));
1901
+ this.#viewState.notify();
1675
1902
  this.dispatchEvent(new CustomEvent(`datagrid-column-resize`, {
1676
1903
  detail: {
1677
1904
  key: col.key,
@@ -1684,6 +1911,179 @@ let DataGridElement = class DataGridElement extends LitElement {
1684
1911
  window.addEventListener(`pointermove`, onMove);
1685
1912
  window.addEventListener(`pointerup`, onUp);
1686
1913
  }
1914
+ /**
1915
+ * Snapshot selection, cursor, scroll, collapsed groups, column layout, sort
1916
+ * and (with `allowGrouping`) grouping as plain JSON, keyed by row / group /
1917
+ * column key. Includes pending state for keys not currently present. Rows
1918
+ * keyed by index (no `key` / `id` under the default `getRowKey`) are never
1919
+ * recorded.
1920
+ */
1921
+ getViewState(options) {
1922
+ const withPending = options?.includePending ?? true;
1923
+ const cursor = this._tickled.inputMode === `keyboard` ? this._tickled.tickledId : void 0;
1924
+ const focus = cursor !== void 0 && this.#isStableKey(cursor) ? cursor : void 0;
1925
+ const groupBy = this.groupBy === null || typeof this.groupBy === `string` ? this.groupBy : void 0;
1926
+ return this.#viewState.shape({
1927
+ selected: keysToArray([...this._selectedKeys].filter((k) => this.#isStableKey(k)), withPending ? this.#pendingSelected : []),
1928
+ focus: focus ?? (withPending ? this.#pendingFocus : void 0),
1929
+ scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? this.#captureScroll(),
1930
+ expanded: collapsedToRecord(this._collapsedGroups, (k) => withPending || this.#groupKeys.has(k)),
1931
+ columns: captureColumns(this.columns, this.#pendingColumns, this.#columnOrder, withPending),
1932
+ sort: this.sortState,
1933
+ groupBy: this.allowGrouping ? groupBy : void 0
1934
+ }, options);
1935
+ }
1936
+ /**
1937
+ * Apply saved view state. Fields present replace the current ones; row and
1938
+ * column keys that aren't defined yet are applied as they appear (including
1939
+ * rows streamed in with `appendRows()`).
1940
+ */
1941
+ setViewState(state) {
1942
+ if (!this.#viewState.accepts(state)) return;
1943
+ this.#viewState.restoring(() => {
1944
+ if (state.columns && typeof state.columns === `object`) {
1945
+ const { pending, order } = columnStateFromRecord(state.columns);
1946
+ this.#pendingColumns = pending;
1947
+ this.#columnOrder = order;
1948
+ this.#reconcileColumns();
1949
+ }
1950
+ if (state.sort !== void 0 && isSortState(state.sort)) this.sortState = state.sort;
1951
+ if (state.groupBy === null || typeof state.groupBy === `string`) this.groupBy = state.groupBy;
1952
+ if (state.expanded && typeof state.expanded === `object`) this._collapsedGroups = collapsedFromRecord(state.expanded);
1953
+ if (state.focus !== void 0) this.#pendingFocus = state.focus;
1954
+ if (state.scroll) this.#viewState.pendingScroll = state.scroll;
1955
+ if (Array.isArray(state.selected)) {
1956
+ const prev = this._selectedKeys;
1957
+ this.#pendingSelected = capKeys(new Set(state.selected.filter((k) => typeof k === `string`)));
1958
+ this._selectedKeys = /* @__PURE__ */ new Set();
1959
+ this.#reconcileRows(prev);
1960
+ } else this.#reconcileRows();
1961
+ this.#viewState.notify();
1962
+ });
1963
+ this.requestUpdate();
1964
+ }
1965
+ clearPendingViewState() {
1966
+ this.#pendingSelected = EMPTY_KEYS;
1967
+ this.#pendingFocus = void 0;
1968
+ this.#pendingColumns = EMPTY_COLUMNS;
1969
+ this.#columnOrder = EMPTY_ORDER;
1970
+ this.#viewState.pendingScroll = void 0;
1971
+ if ([...this._collapsedGroups].some((k) => !this.#groupKeys.has(k))) this._collapsedGroups = new Set([...this._collapsedGroups].filter((k) => this.#groupKeys.has(k)));
1972
+ }
1973
+ /** Apply pending column layout to defined columns; demote layout of removed ones. */
1974
+ #reconcileColumns() {
1975
+ const r = reconcileColumns(this.#reconciledColumns, this.columns, this.#pendingColumns, this.#columnOrder);
1976
+ this.#pendingColumns = r.pending;
1977
+ this.#columnOrder = r.order;
1978
+ this.#reconciledColumns = r.columns;
1979
+ if (r.columns !== this.columns) {
1980
+ this.columns = r.columns;
1981
+ this.#viewState.restoring(() => this.#viewState.notify());
1982
+ }
1983
+ }
1984
+ /**
1985
+ * Re-key selection and cursor onto the current rows: state for rows that
1986
+ * left goes pending (silently), pending state for rows now present is
1987
+ * applied. `baseline` is the selection to report as `previous` when the
1988
+ * caller already replaced it (`setViewState`).
1989
+ */
1990
+ #reconcileRows(baseline) {
1991
+ this.#viewState.restoring(() => {
1992
+ const sel = reconcileSelectedKeys(this._selectedKeys, this.#pendingSelected, (k) => this.#hasRow(k), !this.#isMultiSelect(), this.selectionMode !== `none`);
1993
+ this.#pendingSelected = sel.pending;
1994
+ if (sel.changed) this.#setRestoredSelection(sel.selected);
1995
+ const before = baseline ?? this._selectedKeys;
1996
+ if (baseline ? !sameSet(baseline, this._selectedKeys) : sel.restored) this.#dispatchSelect(before);
1997
+ const cursor = this._tickled.tickledId;
1998
+ if (cursor !== void 0 && !this.#flatIndexByKey.has(cursor)) {
1999
+ if (this._tickled.inputMode === `keyboard` && this.#isStableKey(cursor)) this.#pendingFocus ??= cursor;
2000
+ this._tickled.clearTickled();
2001
+ }
2002
+ this.#applyPendingFocus();
2003
+ });
2004
+ }
2005
+ /** Streamed append: match pending state against the new slice only (never the whole row set). */
2006
+ #resolveAppended(appended, startIdx) {
2007
+ const wantSelection = this.#pendingSelected.size > 0 && this.selectionMode !== `none`;
2008
+ if (wantSelection || this.#rowIndex !== void 0) {
2009
+ const keys = appended.map((r, j) => this.getRowKey(r, startIdx + j));
2010
+ keys.forEach((k, j) => this.#rowIndex?.set(k, appended[j]));
2011
+ const present = wantSelection ? matchSlice(this.#pendingSelected, keys) : [];
2012
+ if (present.length > 0) this.#viewState.restoring(() => {
2013
+ const prev = this._selectedKeys;
2014
+ const sel = resolvePendingKeys(prev, this.#pendingSelected, present, !this.#isMultiSelect());
2015
+ this.#pendingSelected = sel.pending;
2016
+ this.#setRestoredSelection(sel.selected);
2017
+ this.#dispatchSelect(prev);
2018
+ });
2019
+ }
2020
+ if (this.#pendingFocus !== void 0) this.#viewState.restoring(() => this.#applyPendingFocus());
2021
+ }
2022
+ #setRestoredSelection(selected) {
2023
+ const lastKey = this._anchorKey;
2024
+ this._selectedKeys = new Set(selected);
2025
+ this._anchorKey = lastKey !== void 0 && selected.has(lastKey) ? lastKey : [...selected].at(-1);
2026
+ }
2027
+ #applyPendingFocus() {
2028
+ const key = this.#pendingFocus;
2029
+ if (key === void 0 || this._getRowForKey(key) === void 0) return;
2030
+ this.#pendingFocus = void 0;
2031
+ this._tickled.setInputMode(`keyboard`);
2032
+ this._tickled.setTickled(key);
2033
+ this.#viewState.notify();
2034
+ }
2035
+ /** Whether a row with this key is in `rows` (visible, filtered out or in a collapsed group). */
2036
+ #hasRow(key) {
2037
+ const i = this.#flatIndexByKey.get(key);
2038
+ if (i !== void 0) return this._flat[i].type === `row`;
2039
+ return this.#rowsByKey().has(key);
2040
+ }
2041
+ /** Every row by key — built only when a key isn't in `_flat`, then reused until the next resort. */
2042
+ #rowsByKey() {
2043
+ if (this.#rowIndex === void 0 || this.#rowIndexKeyFn !== this.getRowKey) {
2044
+ this.#rowIndexKeyFn = this.getRowKey;
2045
+ this.#rowIndex = new Map(this._sortedRows.map((r, j) => [this.getRowKey(r, j), r]));
2046
+ }
2047
+ return this.#rowIndex;
2048
+ }
2049
+ /** Index-derived keys (default `getRowKey`, row without `key` / `id`) can't be restored, so aren't recorded. */
2050
+ #isStableKey(key) {
2051
+ if (this.getRowKey !== defaultGetRowKey) return true;
2052
+ const row = this._getRowForKey(key) ?? this.#rowsByKey().get(key);
2053
+ return row !== void 0 && (row.key ?? row.id) !== void 0;
2054
+ }
2055
+ #captureScroll() {
2056
+ const vp = this._viewportEl;
2057
+ if (!vp) return void 0;
2058
+ const top = vp.scrollTop;
2059
+ const rh = this.rowHeight;
2060
+ const first = Math.max(0, Math.floor(top / rh));
2061
+ const end = Math.min(this._flat.length, first + 64);
2062
+ for (let i = first; i < end; i++) {
2063
+ const entry = this._flat[i];
2064
+ if (entry.type === `row`) return anchorFromRow(top, this.#isStableKey(entry.key) ? entry.key : void 0, i * rh, vp.scrollLeft);
2065
+ }
2066
+ return anchorFromRow(top, void 0, 0, vp.scrollLeft);
2067
+ }
2068
+ /** Returns `true` once the saved position is fully reached. */
2069
+ #applyScroll(scroll) {
2070
+ const vp = this._viewportEl;
2071
+ if (!vp) return false;
2072
+ let top = scroll.top;
2073
+ if (scroll.anchor) {
2074
+ const i = this.#flatIndexByKey.get(scroll.anchor.key);
2075
+ if (i === void 0) return false;
2076
+ top = i * this.rowHeight + scroll.anchor.offset;
2077
+ }
2078
+ const reachedY = scrollToOffset(vp, top);
2079
+ const reachedX = scroll.left === void 0 || scrollToOffset(vp, scroll.left, `horizontal`);
2080
+ const before = this.#windowRows(2);
2081
+ this._scrollTop = vp.scrollTop;
2082
+ this._scrollLeft = vp.scrollLeft;
2083
+ const after = this.#windowRows(2);
2084
+ if (before.start !== after.start || before.end !== after.end) this.requestUpdate();
2085
+ return reachedY && reachedX;
2086
+ }
1687
2087
  /** Height of the row area: the viewport minus the sticky header row inside it. */
1688
2088
  #bodyHeight() {
1689
2089
  return this._viewportEl.clientHeight - (this._headerRowEl?.offsetHeight ?? 0);
@@ -2013,6 +2413,7 @@ let DataGridElement = class DataGridElement extends LitElement {
2013
2413
  const v = value ?? ``;
2014
2414
  this.groupBy = v || null;
2015
2415
  this._collapsedGroups = /* @__PURE__ */ new Set();
2416
+ this.#viewState.notify();
2016
2417
  this.dispatchEvent(new CustomEvent(`datagrid-group-change`, {
2017
2418
  detail: { groupBy: this.groupBy },
2018
2419
  bubbles: true,
@@ -2101,6 +2502,10 @@ __decorate([property({
2101
2502
  })], DataGridElement.prototype, "badgeColumn", void 0);
2102
2503
  __decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
2103
2504
  __decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
2505
+ __decorate([property({
2506
+ type: String,
2507
+ attribute: `search-column`
2508
+ })], DataGridElement.prototype, "searchColumn", void 0);
2104
2509
  __decorate([property({
2105
2510
  type: String,
2106
2511
  attribute: `search-modes`,