@ixfx/components 0.15.5 → 0.16.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/bundle/index.d.ts +917 -44
- package/bundle/index.js +702 -696
- 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-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
- package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
- package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
- package/dist/detail-list.d.ts +4 -4
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-JPgQSmLW.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-CdEkScn-.js} +276 -75
- package/dist/{grid-list-0e7snW6t.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
- package/dist/grid-list.d.ts +3 -3
- package/dist/grid-list.js +1 -1
- package/dist/incr-search.d.ts +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
- package/dist/index-CYRc6Dah.d.ts +475 -0
- package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
- package/dist/index.d.ts +334 -23
- package/dist/index.js +2247 -867
- 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-BFRfn0ve.js +2237 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- 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_VxOHlo.js} +298 -79
- package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
- package/dist/tree-CftHwtEb.js +146 -0
- package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
- 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-1V1Vg2O5.js +1993 -0
- package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-BPDmZx6T.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 +66 -1
- package/docs-user/detail-list.md +54 -0
- package/docs-user/dock.md +54 -12
- package/docs-user/flat-list.md +27 -0
- package/docs-user/grid-list.md +64 -11
- 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 +1 -1
|
@@ -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
|
|
|
@@ -549,6 +550,7 @@ still shadow-internal.
|
|
|
549
550
|
| `appendRows` | `(rows)` | Add rows without a full re-sort/rebuild — for streaming |
|
|
550
551
|
| `selectKeys` / `set selectedKeys` | `(keys)` | Replace the selection by row key |
|
|
551
552
|
| `tickleRow` | `(key)` | Move the row cursor and scroll it into view |
|
|
553
|
+
| `getViewState` / `setViewState` / `clearPendingViewState` | see [View state](#view-state) | Snapshot / restore selection, cursor, scroll, groups, columns, sort |
|
|
552
554
|
| `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
555
|
|
|
554
556
|
| Getter | Type | |
|
|
@@ -567,6 +569,69 @@ when the key sets actually differ from the last dispatch. Mirrors
|
|
|
567
569
|
|
|
568
570
|
---
|
|
569
571
|
|
|
572
|
+
## View state
|
|
573
|
+
|
|
574
|
+
The grid implements the shared view-state API (`docs/infra-view-state.md`):
|
|
575
|
+
`getViewState()`, `setViewState()`, `clearPendingViewState()` and a debounced
|
|
576
|
+
`view-state-change` event. `bindViewState()` persists it in two lines:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
import { bindViewState } from '@ixfx/components';
|
|
580
|
+
|
|
581
|
+
bindViewState(grid, {
|
|
582
|
+
load: () => JSON.parse(localStorage.getItem(`orders-grid`) ?? `null`),
|
|
583
|
+
save: s => localStorage.setItem(`orders-grid`, JSON.stringify(s)),
|
|
584
|
+
});
|
|
585
|
+
grid.columns = defs;
|
|
586
|
+
for await (const batch of stream)
|
|
587
|
+
grid.appendRows(batch); // saved selection / cursor / scroll apply as rows arrive
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
`kind: 'data-grid'`. Fields (all optional on `setViewState`; present fields
|
|
591
|
+
replace, absent ones are untouched):
|
|
592
|
+
|
|
593
|
+
| Field | Shape | |
|
|
594
|
+
|---|---|---|
|
|
595
|
+
| `selected` | `string[]` | Selected row keys (the checked set in `checked` mode) |
|
|
596
|
+
| `focus` | `string` | Row cursor key — recorded only after keyboard navigation |
|
|
597
|
+
| `scroll` | `{ top, left?, anchor?: { key, offset } }` | `anchor` is the first visible row's key; `left` is raw px |
|
|
598
|
+
| `expanded` | `Record<groupKey, boolean>` | Groups default open, so only collapsed groups (`false`) are recorded |
|
|
599
|
+
| `columns` | `Record<columnKey, { width?, visible?, order? }>` | `order` is a rank: lower is further left |
|
|
600
|
+
| `sort` | `{ key, direction } \| null` | `null` = unsorted |
|
|
601
|
+
| `groupBy` | `string \| null` | Only recorded when `allowGrouping` is on (the user can change it); a function `groupBy` is never recorded |
|
|
602
|
+
|
|
603
|
+
**Pending state.** Keys that aren't in the data stay pending and are applied
|
|
604
|
+
when a matching row, group or column appears, which covers restoring before
|
|
605
|
+
data, `rows` resets and rebuilds, and `appendRows()` streams. When a selected
|
|
606
|
+
row leaves `rows` it is deselected silently and its key goes pending. When it
|
|
607
|
+
comes back it is reselected and `datagrid-select` fires. Any explicit
|
|
608
|
+
selection change (`selectKeys`, click, key, `Esc`) drops pending selection.
|
|
609
|
+
Resolving pending state on `appendRows()` looks only at the appended slice.
|
|
610
|
+
|
|
611
|
+
**Columns.** Saved width and visibility for a column apply once, when the
|
|
612
|
+
column is first defined. After that the column definition is the source of
|
|
613
|
+
truth, so a host that later redefines the column wins. A column that is
|
|
614
|
+
removed from `columns` keeps its last width and visibility pending until it
|
|
615
|
+
returns. A saved `order` reorders the columns it names within the positions
|
|
616
|
+
they occupy. Columns it doesn't name keep their place. It is kept until every
|
|
617
|
+
column it names has appeared, so late columns still land in their saved
|
|
618
|
+
position. Dragging a header to reorder drops it. Applied layout goes onto
|
|
619
|
+
copies of the column objects, and `columns` is reassigned.
|
|
620
|
+
|
|
621
|
+
**Rows keyed by index.** With the default `getRowKey`, a row with neither
|
|
622
|
+
`key` nor `id` is keyed by its sorted index. Index keys change on every sort,
|
|
623
|
+
insert or rebuild, so these rows are never recorded in `selected`, `focus` or
|
|
624
|
+
the scroll anchor (`scroll.top` / `left` are still recorded). Supply a
|
|
625
|
+
`getRowKey` (or `key` / `id` fields) to make their state restorable. A
|
|
626
|
+
custom `getRowKey` is trusted to be stable.
|
|
627
|
+
|
|
628
|
+
Change events: `user` for selection, cursor moves, scrolling, sort clicks,
|
|
629
|
+
column resize / auto-size / reorder / visibility, group collapse and group-by
|
|
630
|
+
menu choices; `restore` for state applied from `setViewState()` or pending
|
|
631
|
+
state.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
570
635
|
## Demo
|
|
571
636
|
|
|
572
637
|
`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)
|
|
@@ -587,6 +588,59 @@ replaced) falls back to a full rebuild.
|
|
|
587
588
|
|
|
588
589
|
---
|
|
589
590
|
|
|
591
|
+
## View state
|
|
592
|
+
|
|
593
|
+
Snapshot and restore *how* the list is showing its data, separately from the
|
|
594
|
+
data, as plain JSON keyed by item / group key. See
|
|
595
|
+
[`docs/infra-view-state.md`](../../docs/infra-view-state.md) for the shared
|
|
596
|
+
contract.
|
|
597
|
+
|
|
598
|
+
```ts
|
|
599
|
+
import { bindViewState } from '@ixfx/components';
|
|
600
|
+
|
|
601
|
+
bindViewState(list, {
|
|
602
|
+
load: () => JSON.parse(localStorage.getItem(`files`) ?? `null`),
|
|
603
|
+
save: s => localStorage.setItem(`files`, JSON.stringify(s)),
|
|
604
|
+
});
|
|
605
|
+
list.provider = fetchFiles; // pending state applies as batches arrive
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
`kind: 'detail-list'`. Fields (`DetailListViewState`):
|
|
609
|
+
|
|
610
|
+
| Field | Shape | Notes |
|
|
611
|
+
|---|---|---|
|
|
612
|
+
| `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. |
|
|
613
|
+
| `expanded` | `Record<string, boolean>` | `{ [groupKey]: false }` per collapsed group. Groups default open, so only collapses are recorded; `true` re-opens. |
|
|
614
|
+
| `focus` | `string` | Keyboard-cursor key, recorded only when the cursor was moved by keyboard. Restoring it puts the list in keyboard mode. |
|
|
615
|
+
| `scroll` | `{ top, left?, anchor? }` | Horizontal: `left`, plus `anchor` = the top item of the first visible column and the px offset into it. |
|
|
616
|
+
|
|
617
|
+
`sort` is not view state: the list has no UI to change it, so it stays host
|
|
618
|
+
configuration.
|
|
619
|
+
|
|
620
|
+
- **Pending.** Keys not in the data stay pending and apply when a matching
|
|
621
|
+
item appears — via `items`, a provider batch (only the new slice is
|
|
622
|
+
searched), `addItem` / `setItems`, or a light-DOM append. Selected items that
|
|
623
|
+
leave the data (rebuild, `clearItems`, removed `<li>`) are deselected
|
|
624
|
+
silently and their keys go pending.
|
|
625
|
+
- Restored selection fires `list-select`; restored collapses fire
|
|
626
|
+
`list-group-toggle` for groups that are present. Any explicit selection
|
|
627
|
+
change drops pending selection.
|
|
628
|
+
- Changing `groupBy` drops collapses of the current groups but keeps pending
|
|
629
|
+
(never-matched) collapsed keys, so a state restored before `groupBy` is set
|
|
630
|
+
still applies.
|
|
631
|
+
- A pending scroll waits until its anchor item is laid out and the columns are
|
|
632
|
+
wide enough; a wheel, pointer or navigation key cancels it.
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
list.getViewState(); // pending included
|
|
636
|
+
list.getViewState({ includePending: false });
|
|
637
|
+
list.setViewState({ selected: [`row-3`] }); // partial: only fields present apply
|
|
638
|
+
list.clearPendingViewState();
|
|
639
|
+
list.addEventListener(`view-state-change`, e => e.detail /* { state, reason } */);
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
---
|
|
643
|
+
|
|
590
644
|
## CSS variables
|
|
591
645
|
|
|
592
646
|
| Variable | Default | Description |
|
package/docs-user/dock.md
CHANGED
|
@@ -63,7 +63,7 @@ The tree is plain and mutable, owned exclusively by the host.
|
|
|
63
63
|
|
|
64
64
|
Named containers: give a stack leaf `name` (e.g. `"primary-sidebar"`) in the
|
|
65
65
|
initial layout JSON. Names must be unique per layout (first match wins),
|
|
66
|
-
|
|
66
|
+
are saved and restored with the [view state](#view-state), and render as
|
|
67
67
|
`data-dock-name` on the leaf. Explicit leaf close still removes the stack.
|
|
68
68
|
When a `tabs` drop empties a named stack into an *unnamed* one, the name
|
|
69
69
|
(and its content rules) follows the panels to the merged leaf instead of
|
|
@@ -117,7 +117,7 @@ split resizes, and host resizes; handles are hidden during dock drags.
|
|
|
117
117
|
`setLeafExclusiveContents(name, kinds)` admits only those kinds into the
|
|
118
118
|
named leaf, `setLeafDenyContents(name, kinds)` refuses those kinds.
|
|
119
119
|
`clearLeafContentRestrictions(name)` lifts both; `[]` seals (exclusive) or
|
|
120
|
-
lifts (deny). Rules are host policy — kept across `
|
|
120
|
+
lifts (deny). Rules are host policy — kept across `setViewState()`, never
|
|
121
121
|
serialized; already-docked panels are grandfathered.
|
|
122
122
|
- Leaf placement policy — `setLeafPlacementPolicy(name, policy)` is a superset
|
|
123
123
|
of the two calls above (`allowKinds` / `denyKinds`) plus `allowEdgeSplit`
|
|
@@ -181,7 +181,7 @@ While hidden:
|
|
|
181
181
|
## Events
|
|
182
182
|
|
|
183
183
|
`ixfx-dock-host`: `dock-change` (`{ layout }` after structural mutations),
|
|
184
|
-
`dock-panel-added`, `dock-panel-removed`, `dock-panel-activated`,
|
|
184
|
+
`view-state-change` (debounced, see [View state](#view-state)), `dock-panel-added`, `dock-panel-removed`, `dock-panel-activated`,
|
|
185
185
|
`dock-panel-drag-start`, `dock-panel-moved`, `dock-stack-empty`,
|
|
186
186
|
`dock-tab-contextmenu`.
|
|
187
187
|
|
|
@@ -196,7 +196,7 @@ While hidden:
|
|
|
196
196
|
- `dock-stack-empty` — `{ name, hostEmpty }` when a stack loses its last panel
|
|
197
197
|
(`name` is the container name or `null`). `hostEmpty` is true when the host
|
|
198
198
|
holds no panels at all — the cue to close an owning group. Fires before the
|
|
199
|
-
matching `dock-change`; not fired by `
|
|
199
|
+
matching `dock-change`; not fired by `setViewState()`.
|
|
200
200
|
- `dock-tab-contextmenu` — `{ panelId, clientX, clientY }`, cancelable
|
|
201
201
|
(`preventDefault()` suppresses the native menu). Visible tabs only; with a
|
|
202
202
|
hidden strip handle `contextmenu` on your own element.
|
|
@@ -226,8 +226,9 @@ The host consumes `ixfx-split-layout` events: `ixfx-split-resize` /
|
|
|
226
226
|
## Public API — `ixfx-dock-host`
|
|
227
227
|
|
|
228
228
|
```ts
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
getViewState(options?): DockHostViewState // { v, kind: 'dock-host', layout }; folds live split weights
|
|
230
|
+
setViewState(state: Partial<DockHostViewState>): void
|
|
231
|
+
clearPendingViewState(): void // no-op: placeholders live in the layout tree
|
|
231
232
|
addPanel(panel: DockPanelElement | DockPanelInit, target?: DockAddPanelTarget): boolean
|
|
232
233
|
type DockAddPanelTarget = {
|
|
233
234
|
container?: string; // named stack leaf — takes precedence over `stack`
|
|
@@ -427,13 +428,54 @@ it can measure.
|
|
|
427
428
|
window. `acceptExternalPanel` returns `null` if that id is already docked in
|
|
428
429
|
the target host (adopt / focus it yourself instead).
|
|
429
430
|
|
|
430
|
-
##
|
|
431
|
+
## View state
|
|
431
432
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
433
|
+
The dock implements the shared view-state API
|
|
434
|
+
([docs/infra-view-state.md](../../docs/infra-view-state.md)). The component
|
|
435
|
+
never persists anything; the host does, e.g. with `bindViewState()`:
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { bindViewState } from '@ixfx/components';
|
|
439
|
+
|
|
440
|
+
bindViewState(dock, {
|
|
441
|
+
load: () => JSON.parse(localStorage.getItem(`workspace`) ?? `null`),
|
|
442
|
+
save: s => localStorage.setItem(`workspace`, JSON.stringify(s)),
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
| Field | Type | Meaning |
|
|
447
|
+
|---|---|---|
|
|
448
|
+
| `layout` | `DockLayout` | The whole tree: splits with `sizes`, stacks with tab order, `active`, `name`, `collapsed` / `preCollapseSize`, strip flags. Keyed by panel id. |
|
|
449
|
+
|
|
450
|
+
The dock layout is close to structure, so the saved state *is* the tree
|
|
451
|
+
(collapse and the active tab stay inside it rather than in separate
|
|
452
|
+
`expanded` / `selected` fields).
|
|
453
|
+
|
|
454
|
+
- `getViewState()` folds live gutter weights from each rendered
|
|
455
|
+
`ixfx-split-layout` (via `getSizes()`) into `DockSplitNode.sizes` first, so
|
|
456
|
+
a save → drag → save round-trip preserves user adjustments.
|
|
457
|
+
- **Panels not in the host yet.** `setViewState()` keeps every panel id the
|
|
458
|
+
layout names, held or not. One the host doesn't hold is a *placeholder*:
|
|
459
|
+
it keeps its place in the tree (shown as a tab labelled with its id) and
|
|
460
|
+
the panel slots into that place when it arrives — appended as a light-DOM
|
|
461
|
+
child, `addPanel()` **without** a target, `acceptExternalPanel()` without
|
|
462
|
+
`at`, or a cross-host adoption. An explicit `addPanel()` target still wins.
|
|
463
|
+
Placeholders are saved by `getViewState()`; `includePending: false` leaves
|
|
464
|
+
them out (stacks they empty are dropped unless named). Remove one with
|
|
465
|
+
`removePanel(id)`; `clearPendingViewState()` is a no-op.
|
|
466
|
+
- Pooled panels the layout doesn't name are appended to the first stack
|
|
467
|
+
admitting their kind.
|
|
468
|
+
- A panel that is closed / removed (`removePanel`, close button, removed from
|
|
469
|
+
the DOM, moved to another host) leaves the layout — closing is a structural
|
|
470
|
+
change, so its place is not remembered.
|
|
471
|
+
- State set before the host is connected wins over the `layout` attribute.
|
|
472
|
+
- A malformed `layout` (or a state for another `kind`) is ignored with a
|
|
473
|
+
warning.
|
|
474
|
+
- `view-state-change` fires (debounced) after structural changes (drops,
|
|
475
|
+
add / remove, close, leaf moves), gutter resizes, collapse / expand and
|
|
476
|
+
tab activation. `reason` is `data` when the pool changed under the host,
|
|
477
|
+
`restore` for `setViewState()`. The dock's internal split layouts' own
|
|
478
|
+
`view-state-change` events don't escape the host.
|
|
437
479
|
|
|
438
480
|
## CSS variables
|
|
439
481
|
|
package/docs-user/flat-list.md
CHANGED
|
@@ -51,6 +51,7 @@ A node counts as a branch unless `item.isLeaf` is `true` or its `children` is `[
|
|
|
51
51
|
| `beginEdit(key)` | Rename a row in place. Needs `validateEdit` and `item.editable`; `false` otherwise |
|
|
52
52
|
| `registry` / `dispatch(id, args)` | The inner list's command registry, e.g. `dispatch('list.search.filter')` |
|
|
53
53
|
| `list` | The inner `ixfx-vertical-list` |
|
|
54
|
+
| `getViewState(options?)` / `setViewState(state)` / `clearPendingViewState()` | See [View state](#view-state) |
|
|
54
55
|
|
|
55
56
|
## Events
|
|
56
57
|
|
|
@@ -77,6 +78,32 @@ The inner list's `list-*` events also cross the shadow boundary, with row elemen
|
|
|
77
78
|
|
|
78
79
|
Selection is cleared on every level change. Arrow Left and Backspace are ignored while the search field is focused.
|
|
79
80
|
|
|
81
|
+
## View state
|
|
82
|
+
|
|
83
|
+
The level shown and its selection, cursor and scroll, as plain JSON for the
|
|
84
|
+
host to persist (see [docs/infra-view-state.md](../../docs/infra-view-state.md)):
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
{ v: 1, kind: 'flat-list', path: ['docs', 'work'], selected: ['a.txt'], focus: 'a.txt', scroll: { top: 0 } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`selected`, `focus` and `scroll` belong to the level at `path`. They are held
|
|
91
|
+
until that level is shown, then handed to the inner list, which applies them
|
|
92
|
+
as its rows arrive (see the vertical-list README).
|
|
93
|
+
|
|
94
|
+
- Keys not loaded yet stay pending: the list shows the deepest level it can
|
|
95
|
+
reach and continues as data arrives. Showing a level loads it (via
|
|
96
|
+
`loadChildren` / `model`), so a saved path through lazy levels loads one
|
|
97
|
+
level at a time.
|
|
98
|
+
- If new data drops the level shown, the list steps up to the closest
|
|
99
|
+
ancestor and the level (with its selection) goes pending; it is returned to
|
|
100
|
+
when its keys reappear. This also applies to a `path` assigned before the
|
|
101
|
+
data has loaded.
|
|
102
|
+
- An explicit navigation (gesture, `navigate*()`, or assigning `path`) drops
|
|
103
|
+
a pending level; an explicit selection drops a pending selection.
|
|
104
|
+
- The inner crumb's and list's own `view-state-change` events are stopped at
|
|
105
|
+
this element; it fires its own.
|
|
106
|
+
|
|
80
107
|
## Drops
|
|
81
108
|
|
|
82
109
|
With `items-droppable`, a drag over a row, or over empty space, is accepted unless `canDrop` refuses it. A drop fires `flat-list-drop` with that row's node, or the **current level's node** for empty space. Dropping on a branch row reports it and does **not** enter it. Native OS drags work: `dataTransfer` carries `files`, `text/uri-list` and any custom MIME type the source wrote. The row under the drag gets the vertical-list [drop highlight](../vertical-list/README.md#drop-targets).
|