@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.
- package/bundle/index.d.ts +986 -49
- package/bundle/index.js +765 -729
- package/dist/ac-token.js +1 -1
- package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
- package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
- package/dist/crumbs.d.ts +2 -115
- package/dist/crumbs.js +1 -1179
- package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
- package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
- package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
- package/dist/detail-list.d.ts +4 -4
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.d.ts} +27 -4
- package/dist/flat-list.d.ts +2 -2
- package/dist/flat-list.js +123 -4
- package/dist/{grid-list-y6dWJYu3.js → grid-list-D0dJNVEF.js} +278 -76
- package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
- package/dist/grid-list.d.ts +3 -3
- package/dist/grid-list.js +1 -1
- package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/index-Bnm-kKGC.d.ts +475 -0
- package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
- package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
- package/dist/index.d.ts +341 -21
- package/dist/index.js +2286 -884
- package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
- package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
- package/dist/miller-DUqiUAWQ.js +2230 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- package/dist/narrowed-text.js +1 -1
- package/dist/pending-BGidHppL.js +91 -0
- package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
- package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
- package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
- package/dist/split-layout.d.ts +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/sunburst-tree.d.ts +2 -2
- package/dist/sunburst-tree.js +1 -1
- package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_M0YTEA.js} +299 -80
- package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
- package/dist/tree-CftHwtEb.js +146 -0
- package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
- package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
- package/dist/tree.d.ts +3 -3
- package/dist/tree.js +1 -1
- package/dist/types-LU1u0CB8.d.ts +87 -0
- package/dist/vertical-list-DyKPKGxK.js +1993 -0
- package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.d.ts} +31 -3
- package/dist/vertical-list.d.ts +3 -3
- package/dist/vertical-list.js +1 -1789
- package/dist/view-state-controller-BdyXWUUM.js +305 -0
- package/docs-user/README.md +2 -0
- package/docs-user/crumbs.md +27 -0
- package/docs-user/data-grid.md +79 -5
- package/docs-user/detail-list.md +69 -0
- package/docs-user/dock.md +63 -12
- package/docs-user/flat-list.md +27 -0
- package/docs-user/grid-list.md +69 -13
- package/docs-user/hyperbolic-tree.md +31 -4
- package/docs-user/icicle-tree.md +36 -0
- package/docs-user/index.json +12 -2
- package/docs-user/llms.txt +2 -0
- package/docs-user/miller.md +52 -21
- package/docs-user/multicol-list.md +50 -8
- package/docs-user/split-layout.md +45 -17
- package/docs-user/sunburst-tree.md +32 -2
- package/docs-user/tree.md +45 -6
- package/docs-user/treemap.md +32 -0
- package/docs-user/twosplit-layout.md +22 -0
- package/docs-user/user-catalog.md +1 -1
- package/docs-user/vertical-list.md +55 -7
- package/llms.txt +2 -0
- package/package.json +2 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
//#region src/common/view-state/types.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared view-state contract for list-like components.
|
|
4
|
+
*
|
|
5
|
+
* View state is everything about *how* a component is showing its data —
|
|
6
|
+
* selection, expansion, scroll, column sizes — kept separate from the data
|
|
7
|
+
* itself. It is plain JSON (arrays and records, never Sets or Maps) so a host
|
|
8
|
+
* can `JSON.stringify` it straight into whatever storage it likes. Components
|
|
9
|
+
* never persist it themselves; see `bindViewState()`.
|
|
10
|
+
*
|
|
11
|
+
* Every field is keyed by the stable item / column / group key, never by
|
|
12
|
+
* index, so state can be re-applied to data that has been rebuilt, re-sorted
|
|
13
|
+
* or streamed in over time. Keys the current data doesn't contain are kept
|
|
14
|
+
* as pending and applied once a matching item appears.
|
|
15
|
+
*/
|
|
16
|
+
/** Current schema version. Bump on breaking shape changes. */
|
|
17
|
+
const VIEW_STATE_VERSION = 1;
|
|
18
|
+
/** Name of the debounced change event every view-stateful component fires. */
|
|
19
|
+
const VIEW_STATE_CHANGE = `view-state-change`;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/common/view-state/scroll-anchor.ts
|
|
22
|
+
/**
|
|
23
|
+
* Capture a {@link ScrollState} for a DOM-rendered list. `items` must be in
|
|
24
|
+
* visual order along `axis`; the first one whose far edge is below / right of
|
|
25
|
+
* the viewport start is the anchor (found by binary search).
|
|
26
|
+
*/
|
|
27
|
+
function captureElementScroll(viewport, items, getKey, axis = `vertical`) {
|
|
28
|
+
const top = viewport.scrollTop;
|
|
29
|
+
const left = viewport.scrollLeft;
|
|
30
|
+
const base = left === 0 ? { top } : {
|
|
31
|
+
top,
|
|
32
|
+
left
|
|
33
|
+
};
|
|
34
|
+
if (items.length === 0) return base;
|
|
35
|
+
const vpRect = viewport.getBoundingClientRect();
|
|
36
|
+
const start = axis === `vertical` ? vpRect.top : vpRect.left;
|
|
37
|
+
const edges = (el) => {
|
|
38
|
+
const r = el.getBoundingClientRect();
|
|
39
|
+
return axis === `vertical` ? {
|
|
40
|
+
lo: r.top,
|
|
41
|
+
hi: r.bottom
|
|
42
|
+
} : {
|
|
43
|
+
lo: r.left,
|
|
44
|
+
hi: r.right
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
let lo = 0;
|
|
48
|
+
let hi = items.length - 1;
|
|
49
|
+
let found = -1;
|
|
50
|
+
while (lo <= hi) {
|
|
51
|
+
const mid = lo + hi >> 1;
|
|
52
|
+
if (edges(items[mid]).hi > start) {
|
|
53
|
+
found = mid;
|
|
54
|
+
hi = mid - 1;
|
|
55
|
+
} else lo = mid + 1;
|
|
56
|
+
}
|
|
57
|
+
if (found === -1) return base;
|
|
58
|
+
for (let i = found; i < items.length; i++) {
|
|
59
|
+
const key = getKey(items[i]);
|
|
60
|
+
if (key !== void 0) return {
|
|
61
|
+
...base,
|
|
62
|
+
anchor: {
|
|
63
|
+
key,
|
|
64
|
+
offset: Math.round(start - edges(items[i]).lo)
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Scroll `viewport` so `item` sits at the anchor's offset. Returns `false`
|
|
72
|
+
* when the viewport can't scroll far enough yet (content still arriving),
|
|
73
|
+
* so the caller can keep the anchor pending.
|
|
74
|
+
*/
|
|
75
|
+
function restoreElementScroll(viewport, item, offset, axis = `vertical`) {
|
|
76
|
+
const vpRect = viewport.getBoundingClientRect();
|
|
77
|
+
const r = item.getBoundingClientRect();
|
|
78
|
+
if (axis === `vertical`) return scrollToOffset(viewport, viewport.scrollTop + (r.top - vpRect.top) + offset, axis);
|
|
79
|
+
return scrollToOffset(viewport, viewport.scrollLeft + (r.left - vpRect.left) + offset, axis);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Set the scroll offset along `axis`. Returns `false` when `target` is beyond
|
|
83
|
+
* the current scroll range (it is clamped and should be retried later).
|
|
84
|
+
*/
|
|
85
|
+
function scrollToOffset(viewport, target, axis = `vertical`) {
|
|
86
|
+
const max = axis === `vertical` ? viewport.scrollHeight - viewport.clientHeight : viewport.scrollWidth - viewport.clientWidth;
|
|
87
|
+
const clamped = Math.max(0, Math.min(target, max));
|
|
88
|
+
if (axis === `vertical`) viewport.scrollTop = clamped;
|
|
89
|
+
else viewport.scrollLeft = clamped;
|
|
90
|
+
return target <= max + 1;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Anchor for a virtualised list with arithmetic row geometry: pass the first
|
|
94
|
+
* visible index, the key at that index and that row's start offset.
|
|
95
|
+
*/
|
|
96
|
+
function anchorFromRow(scrollOffset, key, rowStart, crossOffset = 0) {
|
|
97
|
+
const base = crossOffset === 0 ? { top: scrollOffset } : {
|
|
98
|
+
top: scrollOffset,
|
|
99
|
+
left: crossOffset
|
|
100
|
+
};
|
|
101
|
+
if (key === void 0) return base;
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
anchor: {
|
|
105
|
+
key,
|
|
106
|
+
offset: Math.round(scrollOffset - rowStart)
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const SCROLL_KEYS = /* @__PURE__ */ new Set([
|
|
111
|
+
`ArrowUp`,
|
|
112
|
+
`ArrowDown`,
|
|
113
|
+
`ArrowLeft`,
|
|
114
|
+
`ArrowRight`,
|
|
115
|
+
`PageUp`,
|
|
116
|
+
`PageDown`,
|
|
117
|
+
`Home`,
|
|
118
|
+
`End`,
|
|
119
|
+
` `
|
|
120
|
+
]);
|
|
121
|
+
/**
|
|
122
|
+
* Call `onIntent` the first time the user does something that scrolls or
|
|
123
|
+
* moves within `target` (wheel, touch, pointer on the scrollbar, navigation
|
|
124
|
+
* key). Used to cancel a pending scroll restore — the user's own scroll wins.
|
|
125
|
+
* Programmatic `scrollTop` writes do not trigger it. Returns a disposer.
|
|
126
|
+
*/
|
|
127
|
+
function onUserScrollIntent(target, onIntent) {
|
|
128
|
+
const events = [
|
|
129
|
+
`wheel`,
|
|
130
|
+
`touchstart`,
|
|
131
|
+
`pointerdown`,
|
|
132
|
+
`keydown`
|
|
133
|
+
];
|
|
134
|
+
const handler = (event) => {
|
|
135
|
+
if (event.type === `keydown` && !SCROLL_KEYS.has(event.key)) return;
|
|
136
|
+
onIntent();
|
|
137
|
+
};
|
|
138
|
+
for (const type of events) target.addEventListener(type, handler, {
|
|
139
|
+
passive: true,
|
|
140
|
+
capture: true
|
|
141
|
+
});
|
|
142
|
+
return () => {
|
|
143
|
+
for (const type of events) target.removeEventListener(type, handler, { capture: true });
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/common/view-state/view-state-controller.ts
|
|
148
|
+
const REASON_RANK = {
|
|
149
|
+
restore: 0,
|
|
150
|
+
data: 1,
|
|
151
|
+
user: 2
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Plumbing shared by every view-stateful component:
|
|
155
|
+
*
|
|
156
|
+
* - **change events** — `notify(reason)` coalesces changes into one debounced
|
|
157
|
+
* `view-state-change` event (strongest reason wins: user > data > restore),
|
|
158
|
+
* flushed immediately on disconnect so the last change is never lost.
|
|
159
|
+
* - **restore marking** — changes made inside `restoring(fn)` are reported as
|
|
160
|
+
* `restore`, so persistence code can skip writing back what it just loaded.
|
|
161
|
+
* - **validation** — `accepts(state)` rejects a state for another kind / version.
|
|
162
|
+
* - **pending scroll** — a scroll position waiting for its anchor item (or for
|
|
163
|
+
* enough content). Cancelled by the user's own scroll intent on the target
|
|
164
|
+
* given to `observeScrollTarget()`.
|
|
165
|
+
*
|
|
166
|
+
* The component owns its facets (selection, expansion, columns) and decides
|
|
167
|
+
* what's pending; this controller only carries the cross-cutting parts.
|
|
168
|
+
*/
|
|
169
|
+
var ViewStateController = class {
|
|
170
|
+
#host;
|
|
171
|
+
#opts;
|
|
172
|
+
#timer;
|
|
173
|
+
#reason;
|
|
174
|
+
#restoreDepth = 0;
|
|
175
|
+
#pendingScroll;
|
|
176
|
+
/** Set when a restore wrote the scroll offset; the scroll event that follows is not the user's. */
|
|
177
|
+
#restoreScrolled = false;
|
|
178
|
+
#disposeScrollIntent;
|
|
179
|
+
#scrollTarget;
|
|
180
|
+
constructor(host, options) {
|
|
181
|
+
this.#host = host;
|
|
182
|
+
this.#opts = options;
|
|
183
|
+
host.addController(this);
|
|
184
|
+
}
|
|
185
|
+
get kind() {
|
|
186
|
+
return this.#opts.kind;
|
|
187
|
+
}
|
|
188
|
+
hostConnected() {
|
|
189
|
+
if (this.#scrollTarget && !this.#disposeScrollIntent) this.observeScrollTarget(this.#scrollTarget);
|
|
190
|
+
}
|
|
191
|
+
hostDisconnected() {
|
|
192
|
+
this.flush();
|
|
193
|
+
this.#disposeScrollIntent?.();
|
|
194
|
+
this.#disposeScrollIntent = void 0;
|
|
195
|
+
}
|
|
196
|
+
/** Record a change. Inside `restoring()` the reason is forced to `restore`. */
|
|
197
|
+
notify(reason = `user`) {
|
|
198
|
+
const effective = this.#restoreDepth > 0 ? `restore` : reason;
|
|
199
|
+
if (this.#reason === void 0 || REASON_RANK[effective] > REASON_RANK[this.#reason]) this.#reason = effective;
|
|
200
|
+
if (this.#timer !== void 0) clearTimeout(this.#timer);
|
|
201
|
+
this.#timer = setTimeout(() => this.flush(), this.#opts.debounceMs ?? 150);
|
|
202
|
+
}
|
|
203
|
+
/** Fire any pending change event now. */
|
|
204
|
+
flush() {
|
|
205
|
+
if (this.#timer !== void 0) {
|
|
206
|
+
clearTimeout(this.#timer);
|
|
207
|
+
this.#timer = void 0;
|
|
208
|
+
}
|
|
209
|
+
const reason = this.#reason;
|
|
210
|
+
if (reason === void 0) return;
|
|
211
|
+
this.#reason = void 0;
|
|
212
|
+
this.#host.dispatchEvent(new CustomEvent(VIEW_STATE_CHANGE, {
|
|
213
|
+
detail: {
|
|
214
|
+
state: this.#opts.capture(),
|
|
215
|
+
reason
|
|
216
|
+
},
|
|
217
|
+
bubbles: true
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
/** Run `fn` with every `notify()` inside it reported as `restore`. */
|
|
221
|
+
restoring(fn) {
|
|
222
|
+
this.#restoreDepth++;
|
|
223
|
+
try {
|
|
224
|
+
return fn();
|
|
225
|
+
} finally {
|
|
226
|
+
this.#restoreDepth--;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** True while inside `restoring()`. */
|
|
230
|
+
get isRestoring() {
|
|
231
|
+
return this.#restoreDepth > 0;
|
|
232
|
+
}
|
|
233
|
+
/** Whether `state` is for this component kind and schema version. Warns when not. */
|
|
234
|
+
accepts(state) {
|
|
235
|
+
if (state === null || typeof state !== `object`) return false;
|
|
236
|
+
if (state.v !== void 0 && state.v !== 1) {
|
|
237
|
+
console.warn(`[${this.#opts.kind}] ignoring view state with version ${String(state.v)}`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (state.kind !== void 0 && state.kind !== this.#opts.kind) {
|
|
241
|
+
console.warn(`[${this.#opts.kind}] ignoring view state for kind '${state.kind}'`);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Stamp `v` / `kind` onto `fields` and apply `include`. Components build
|
|
248
|
+
* their full field set and pass it through here from `getViewState()`.
|
|
249
|
+
*/
|
|
250
|
+
shape(fields, options) {
|
|
251
|
+
const out = {
|
|
252
|
+
v: 1,
|
|
253
|
+
kind: this.#opts.kind
|
|
254
|
+
};
|
|
255
|
+
const include = options?.include;
|
|
256
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
257
|
+
if (value === void 0) continue;
|
|
258
|
+
if (include && !include.includes(key)) continue;
|
|
259
|
+
out[key] = value;
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Watch `target` for user scroll intent (wheel / touch / pointer / nav key);
|
|
265
|
+
* any intent drops the pending scroll. Call again if the scroll container
|
|
266
|
+
* is replaced.
|
|
267
|
+
*/
|
|
268
|
+
observeScrollTarget(target) {
|
|
269
|
+
this.#disposeScrollIntent?.();
|
|
270
|
+
this.#scrollTarget = target;
|
|
271
|
+
this.#disposeScrollIntent = onUserScrollIntent(target, () => {
|
|
272
|
+
this.#pendingScroll = void 0;
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
get pendingScroll() {
|
|
276
|
+
return this.#pendingScroll;
|
|
277
|
+
}
|
|
278
|
+
set pendingScroll(scroll) {
|
|
279
|
+
this.#pendingScroll = scroll;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Try the pending scroll with `apply`; it returns `true` once the position
|
|
283
|
+
* was fully reached (anchor present, content long enough), which clears it.
|
|
284
|
+
* Call after each render that may have added items.
|
|
285
|
+
*/
|
|
286
|
+
applyPendingScroll(apply) {
|
|
287
|
+
const scroll = this.#pendingScroll;
|
|
288
|
+
if (!scroll) return;
|
|
289
|
+
this.#restoreScrolled = true;
|
|
290
|
+
if (apply(scroll)) this.#pendingScroll = void 0;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Call from the viewport's `scroll` listener. Ignored while a scroll restore
|
|
294
|
+
* is still pending; the event caused by a restore's own write is reported
|
|
295
|
+
* as `restore`, anything else as `user`.
|
|
296
|
+
*/
|
|
297
|
+
notifyScroll() {
|
|
298
|
+
if (this.#pendingScroll) return;
|
|
299
|
+
const reason = this.#restoreScrolled ? `restore` : `user`;
|
|
300
|
+
this.#restoreScrolled = false;
|
|
301
|
+
this.notify(reason);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
//#endregion
|
|
305
|
+
export { restoreElementScroll as a, VIEW_STATE_VERSION as c, onUserScrollIntent as i, anchorFromRow as n, scrollToOffset as o, captureElementScroll as r, VIEW_STATE_CHANGE as s, ViewStateController as t };
|
package/docs-user/README.md
CHANGED
|
@@ -23,6 +23,7 @@ This directory contains documentation for each component and some architectural
|
|
|
23
23
|
- [grid-list](./grid-list.md)
|
|
24
24
|
- [grouped-item-lister](./grouped-item-lister.md)
|
|
25
25
|
- [hyperbolic-tree](./hyperbolic-tree.md)
|
|
26
|
+
- [icicle-tree](./icicle-tree.md)
|
|
26
27
|
- [icons](./icons.md)
|
|
27
28
|
- [incr-search](./incr-search.md)
|
|
28
29
|
- [labelled-radial-input](./labelled-radial-input.md)
|
|
@@ -58,6 +59,7 @@ This directory contains documentation for each component and some architectural
|
|
|
58
59
|
- [toolbar](./toolbar.md)
|
|
59
60
|
- [transitory-label](./transitory-label.md)
|
|
60
61
|
- [tree](./tree.md)
|
|
62
|
+
- [treemap](./treemap.md)
|
|
61
63
|
- [twosplit-layout](./twosplit-layout.md)
|
|
62
64
|
- [util](./util.md)
|
|
63
65
|
- [vertical-list](./vertical-list.md)
|
package/docs-user/crumbs.md
CHANGED
|
@@ -38,6 +38,7 @@ nav.model = {
|
|
|
38
38
|
| `inputMode` | `string` | `''` | Input mode for styling (`'keyboard'`, etc.) |
|
|
39
39
|
| `padEnding` | `boolean` | `false` | Pad trailing space to enable left-align scroll |
|
|
40
40
|
| `withUp` | `boolean` | `false` | Show an Up button to navigate to parent |
|
|
41
|
+
| `restoreLoads` | `boolean` | `true` | Let a restored location load unloaded branches via `loadChildren` |
|
|
41
42
|
|
|
42
43
|
### Events
|
|
43
44
|
|
|
@@ -55,6 +56,32 @@ nav.model = {
|
|
|
55
56
|
|-------------------------|---------|------------------------------------------------|
|
|
56
57
|
| `navigateTo(keys)` | `boolean` | Navigate to a node by key path. Returns false if not found. |
|
|
57
58
|
| `navigateToNode(predicate)` | `boolean` | Navigate to the first node matching a predicate. |
|
|
59
|
+
| `getViewState(options?)` | `CrumbNavigationViewState` | Snapshot the location. See [View state](#view-state). |
|
|
60
|
+
| `setViewState(state)` | `void` | Go to a saved location, now or as its nodes arrive. |
|
|
61
|
+
| `clearPendingViewState()` | `void` | Forget a saved location not reached yet. |
|
|
62
|
+
|
|
63
|
+
### View state
|
|
64
|
+
|
|
65
|
+
The current location as plain JSON, for the host to persist (see
|
|
66
|
+
[docs/infra-view-state.md](../../docs/infra-view-state.md)):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
{ v: 1, kind: 'crumb-navigation', path: ['dev', 'workspace'] } // keys below the root; [] = root
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The location *is* the selection, so there is no separate `selected` field, and
|
|
73
|
+
it is a key path rather than a lone key so a deep saved location can be loaded
|
|
74
|
+
down to. The keyboard cursor is transient and not recorded.
|
|
75
|
+
|
|
76
|
+
- Keys not loaded yet stay pending: the crumb moves to the deepest loaded
|
|
77
|
+
ancestor and continues as nodes arrive (new `root`, or children filled in
|
|
78
|
+
followed by `requestUpdate()`). With `restoreLoads` it calls `loadChildren`
|
|
79
|
+
itself, one branch at a time. Each step fires `select`.
|
|
80
|
+
- New `root` data keeps the location by key. If it's gone, the crumb steps
|
|
81
|
+
back to the closest ancestor silently and returns when the key reappears.
|
|
82
|
+
- Any explicit navigation — a click, `select()`, `navigateTo()`, or the host
|
|
83
|
+
assigning `selectedNode` — drops a pending location. Set the initial
|
|
84
|
+
`selectedNode` *before* `setViewState()` / `bindViewState()`.
|
|
58
85
|
|
|
59
86
|
### CSS custom properties
|
|
60
87
|
|
package/docs-user/data-grid.md
CHANGED
|
@@ -223,7 +223,8 @@ grid.tickledKey; // row key under the cursor, or undefined
|
|
|
223
223
|
```
|
|
224
224
|
|
|
225
225
|
The setter fires `datagrid-select` `{ selectedKeys, previous }` only when the
|
|
226
|
-
selection actually changes.
|
|
226
|
+
selection actually changes. Selected rows that leave `rows` are deselected
|
|
227
|
+
silently, and reselected if they return. See [View state](#view-state).
|
|
227
228
|
|
|
228
229
|
---
|
|
229
230
|
|
|
@@ -505,10 +506,19 @@ rows dropped from the flat list) modes. No default shortcut:
|
|
|
505
506
|
grid.registry.invoke('list.search.filter'); // or .navigate / .select
|
|
506
507
|
```
|
|
507
508
|
|
|
508
|
-
A row is matched against
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
509
|
+
A row is matched against the displayed (formatted) text of one column: the
|
|
510
|
+
`search-column` attribute (`searchColumn` property) names it, defaulting to the
|
|
511
|
+
first visible column. Searching every column makes dates and sizes match
|
|
512
|
+
queries like `12`, which is rarely what Find is for. Set the `rowSearchText`
|
|
513
|
+
property, `(row) => string`, to match against something else, e.g. several
|
|
514
|
+
columns joined. `select` mode needs `selection-mode="multiple"` to hold more
|
|
515
|
+
than one row.
|
|
516
|
+
|
|
517
|
+
Matched characters in the search column are highlighted under the
|
|
518
|
+
`data-grid-search` key, coloured by `--search-highlight-bg` /
|
|
519
|
+
`--search-highlight-color` (default `--accent` / `--accent-text`). Matches
|
|
520
|
+
against a custom `rowSearchText` aren't highlighted, since they can't be
|
|
521
|
+
mapped onto a cell, and neither are cells drawn by a column `renderer`. `filterPredicate`
|
|
512
522
|
(a `(row) => boolean` property) filters rows directly. `search-modes` limits the
|
|
513
523
|
offered modes. Full model: [`docs/infra-incremental.md`](../../docs/infra-incremental.md).
|
|
514
524
|
|
|
@@ -549,6 +559,7 @@ still shadow-internal.
|
|
|
549
559
|
| `appendRows` | `(rows)` | Add rows without a full re-sort/rebuild — for streaming |
|
|
550
560
|
| `selectKeys` / `set selectedKeys` | `(keys)` | Replace the selection by row key |
|
|
551
561
|
| `tickleRow` | `(key)` | Move the row cursor and scroll it into view |
|
|
562
|
+
| `getViewState` / `setViewState` / `clearPendingViewState` | see [View state](#view-state) | Snapshot / restore selection, cursor, scroll, groups, columns, sort |
|
|
552
563
|
| `autoSizeColumn` | `(key)=>boolean` | Resize a column to fit its contents, animating the width. Returns `false` (no-op) for an unknown column, `resizable: false`, or a `renderer` column without `measure`/`fixedExtraWidth` — see [Auto-sizing a column](#auto-sizing-a-column). |
|
|
553
564
|
|
|
554
565
|
| Getter | Type | |
|
|
@@ -567,6 +578,69 @@ when the key sets actually differ from the last dispatch. Mirrors
|
|
|
567
578
|
|
|
568
579
|
---
|
|
569
580
|
|
|
581
|
+
## View state
|
|
582
|
+
|
|
583
|
+
The grid implements the shared view-state API (`docs/infra-view-state.md`):
|
|
584
|
+
`getViewState()`, `setViewState()`, `clearPendingViewState()` and a debounced
|
|
585
|
+
`view-state-change` event. `bindViewState()` persists it in two lines:
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
import { bindViewState } from '@ixfx/components';
|
|
589
|
+
|
|
590
|
+
bindViewState(grid, {
|
|
591
|
+
load: () => JSON.parse(localStorage.getItem(`orders-grid`) ?? `null`),
|
|
592
|
+
save: s => localStorage.setItem(`orders-grid`, JSON.stringify(s)),
|
|
593
|
+
});
|
|
594
|
+
grid.columns = defs;
|
|
595
|
+
for await (const batch of stream)
|
|
596
|
+
grid.appendRows(batch); // saved selection / cursor / scroll apply as rows arrive
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`kind: 'data-grid'`. Fields (all optional on `setViewState`; present fields
|
|
600
|
+
replace, absent ones are untouched):
|
|
601
|
+
|
|
602
|
+
| Field | Shape | |
|
|
603
|
+
|---|---|---|
|
|
604
|
+
| `selected` | `string[]` | Selected row keys (the checked set in `checked` mode) |
|
|
605
|
+
| `focus` | `string` | Row cursor key — recorded only after keyboard navigation |
|
|
606
|
+
| `scroll` | `{ top, left?, anchor?: { key, offset } }` | `anchor` is the first visible row's key; `left` is raw px |
|
|
607
|
+
| `expanded` | `Record<groupKey, boolean>` | Groups default open, so only collapsed groups (`false`) are recorded |
|
|
608
|
+
| `columns` | `Record<columnKey, { width?, visible?, order? }>` | `order` is a rank: lower is further left |
|
|
609
|
+
| `sort` | `{ key, direction } \| null` | `null` = unsorted |
|
|
610
|
+
| `groupBy` | `string \| null` | Only recorded when `allowGrouping` is on (the user can change it); a function `groupBy` is never recorded |
|
|
611
|
+
|
|
612
|
+
**Pending state.** Keys that aren't in the data stay pending and are applied
|
|
613
|
+
when a matching row, group or column appears, which covers restoring before
|
|
614
|
+
data, `rows` resets and rebuilds, and `appendRows()` streams. When a selected
|
|
615
|
+
row leaves `rows` it is deselected silently and its key goes pending. When it
|
|
616
|
+
comes back it is reselected and `datagrid-select` fires. Any explicit
|
|
617
|
+
selection change (`selectKeys`, click, key, `Esc`) drops pending selection.
|
|
618
|
+
Resolving pending state on `appendRows()` looks only at the appended slice.
|
|
619
|
+
|
|
620
|
+
**Columns.** Saved width and visibility for a column apply once, when the
|
|
621
|
+
column is first defined. After that the column definition is the source of
|
|
622
|
+
truth, so a host that later redefines the column wins. A column that is
|
|
623
|
+
removed from `columns` keeps its last width and visibility pending until it
|
|
624
|
+
returns. A saved `order` reorders the columns it names within the positions
|
|
625
|
+
they occupy. Columns it doesn't name keep their place. It is kept until every
|
|
626
|
+
column it names has appeared, so late columns still land in their saved
|
|
627
|
+
position. Dragging a header to reorder drops it. Applied layout goes onto
|
|
628
|
+
copies of the column objects, and `columns` is reassigned.
|
|
629
|
+
|
|
630
|
+
**Rows keyed by index.** With the default `getRowKey`, a row with neither
|
|
631
|
+
`key` nor `id` is keyed by its sorted index. Index keys change on every sort,
|
|
632
|
+
insert or rebuild, so these rows are never recorded in `selected`, `focus` or
|
|
633
|
+
the scroll anchor (`scroll.top` / `left` are still recorded). Supply a
|
|
634
|
+
`getRowKey` (or `key` / `id` fields) to make their state restorable. A
|
|
635
|
+
custom `getRowKey` is trusted to be stable.
|
|
636
|
+
|
|
637
|
+
Change events: `user` for selection, cursor moves, scrolling, sort clicks,
|
|
638
|
+
column resize / auto-size / reorder / visibility, group collapse and group-by
|
|
639
|
+
menu choices; `restore` for state applied from `setViewState()` or pending
|
|
640
|
+
state.
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
570
644
|
## Demo
|
|
571
645
|
|
|
572
646
|
`demo/data-grid.html` + `demo/data-grid.ts`: 200 rows, `Department` random, `Score` float, `Name`/`Department` editable (`user` mode, slow double-click; `Tab`/`Up`/`Down` navigation; `Enter` from tickled row). Controls: Selection/Interaction/Group by/Editability/`column-menu` + `Edit first row` (manual), a `Stream +5,000 rows` button (`appendRows`), and `selectKeys` / `tickleRow` buttons.
|
package/docs-user/detail-list.md
CHANGED
|
@@ -38,6 +38,7 @@ Selection semantics match `ixfx-vertical-list` — same modes, same `list-*` eve
|
|
|
38
38
|
- [Asynchronous data](#asynchronous-data)
|
|
39
39
|
- [Promise provider](#promise-provider)
|
|
40
40
|
- [Streaming provider](#streaming-provider)
|
|
41
|
+
- [View state](#view-state)
|
|
41
42
|
- [CSS variables](#css-variables)
|
|
42
43
|
- [CSS parts](#css-parts)
|
|
43
44
|
- [Performance](#performance)
|
|
@@ -161,6 +162,7 @@ list.keyProperty = 'name'; // Preserves selection across rebuilds
|
|
|
161
162
|
| `textColorProperty` | `string` | Property name for the caption's CSS colour |
|
|
162
163
|
| `badgeProperty` | `string` | Property name for a Finder-style badge |
|
|
163
164
|
| `formatter` | `(item: unknown, index: number) => string \| Node` | Custom renderer |
|
|
165
|
+
| `searchText` | `(item: unknown, index: number) => string` | Text incremental search matches (default: the caption if a string, else the row's whole text) |
|
|
164
166
|
|
|
165
167
|
```typescript
|
|
166
168
|
list.formatter = (item, index) => {
|
|
@@ -176,6 +178,18 @@ put, and the new row animates in like `addItem()`. Pass new objects (or change
|
|
|
176
178
|
`displayProperty`, `formatter`, …) to rebuild rows. With a `formatter`, a row
|
|
177
179
|
is only kept at the same index, since the formatter receives the index.
|
|
178
180
|
|
|
181
|
+
Because reused rows aren't re-run through the `formatter`, state it reads from
|
|
182
|
+
outside the item (sizes, fit mode, …) doesn't reach existing rows when the same
|
|
183
|
+
objects are reassigned. Call `list.reformat()` to rebuild every row, or assign a
|
|
184
|
+
new `formatter` function (a new identity counts as a shape change).
|
|
185
|
+
|
|
186
|
+
When `formatter` returns a `Node`, search falls back to the row's whole text
|
|
187
|
+
(sublines, sizes, dates). Set `searchText` to match on just the name:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
list.searchText = item => (item as { name: string }).name;
|
|
191
|
+
```
|
|
192
|
+
|
|
179
193
|
Retrieve the original data for an element:
|
|
180
194
|
|
|
181
195
|
```typescript
|
|
@@ -458,6 +472,8 @@ Search matches against `data-search-label` (or `textContent` as fallback). Use `
|
|
|
458
472
|
<li data-search-label="document a alpha">Document A</li>
|
|
459
473
|
```
|
|
460
474
|
|
|
475
|
+
For object items, `searchText` sets `data-search-label` per row (see [Object items](#object-items)).
|
|
476
|
+
|
|
461
477
|
### Highlight styling
|
|
462
478
|
|
|
463
479
|
Matched characters are highlighted with the `detail-list-search` highlight key. Style matches with:
|
|
@@ -587,6 +603,59 @@ replaced) falls back to a full rebuild.
|
|
|
587
603
|
|
|
588
604
|
---
|
|
589
605
|
|
|
606
|
+
## View state
|
|
607
|
+
|
|
608
|
+
Snapshot and restore *how* the list is showing its data, separately from the
|
|
609
|
+
data, as plain JSON keyed by item / group key. See
|
|
610
|
+
[`docs/infra-view-state.md`](../../docs/infra-view-state.md) for the shared
|
|
611
|
+
contract.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { bindViewState } from '@ixfx/components';
|
|
615
|
+
|
|
616
|
+
bindViewState(list, {
|
|
617
|
+
load: () => JSON.parse(localStorage.getItem(`files`) ?? `null`),
|
|
618
|
+
save: s => localStorage.setItem(`files`, JSON.stringify(s)),
|
|
619
|
+
});
|
|
620
|
+
list.provider = fetchFiles; // pending state applies as batches arrive
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
`kind: 'detail-list'`. Fields (`DetailListViewState`):
|
|
624
|
+
|
|
625
|
+
| Field | Shape | Notes |
|
|
626
|
+
|---|---|---|
|
|
627
|
+
| `selected` | `string[]` | Selected item keys (`data-key` / `data-value`, or `keyProperty`). In `checked` mode this is the checked set — `data-checked` is derived from the selection, so there is no separate field. |
|
|
628
|
+
| `expanded` | `Record<string, boolean>` | `{ [groupKey]: false }` per collapsed group. Groups default open, so only collapses are recorded; `true` re-opens. |
|
|
629
|
+
| `focus` | `string` | Keyboard-cursor key, recorded only when the cursor was moved by keyboard. Restoring it puts the list in keyboard mode. |
|
|
630
|
+
| `scroll` | `{ top, left?, anchor? }` | Horizontal: `left`, plus `anchor` = the top item of the first visible column and the px offset into it. |
|
|
631
|
+
|
|
632
|
+
`sort` is not view state: the list has no UI to change it, so it stays host
|
|
633
|
+
configuration.
|
|
634
|
+
|
|
635
|
+
- **Pending.** Keys not in the data stay pending and apply when a matching
|
|
636
|
+
item appears — via `items`, a provider batch (only the new slice is
|
|
637
|
+
searched), `addItem` / `setItems`, or a light-DOM append. Selected items that
|
|
638
|
+
leave the data (rebuild, `clearItems`, removed `<li>`) are deselected
|
|
639
|
+
silently and their keys go pending.
|
|
640
|
+
- Restored selection fires `list-select`; restored collapses fire
|
|
641
|
+
`list-group-toggle` for groups that are present. Any explicit selection
|
|
642
|
+
change drops pending selection.
|
|
643
|
+
- Changing `groupBy` drops collapses of the current groups but keeps pending
|
|
644
|
+
(never-matched) collapsed keys, so a state restored before `groupBy` is set
|
|
645
|
+
still applies.
|
|
646
|
+
- A pending scroll waits until its anchor item is laid out and the columns are
|
|
647
|
+
wide enough; a wheel, pointer or navigation key cancels it.
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
list.getViewState(); // pending included
|
|
651
|
+
list.getViewState({ includePending: false });
|
|
652
|
+
list.setViewState({ selected: [`row-3`] }); // partial: only fields present apply
|
|
653
|
+
list.clearPendingViewState();
|
|
654
|
+
list.addEventListener(`view-state-change`, e => e.detail /* { state, reason } */);
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
---
|
|
658
|
+
|
|
590
659
|
## CSS variables
|
|
591
660
|
|
|
592
661
|
| Variable | Default | Description |
|