@joewinke/jatui 0.26.0 → 0.27.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/CLAUDE.md +106 -1
- package/dist/actions/keyboardScope.d.ts +58 -0
- package/dist/actions/keyboardScope.js +110 -0
- package/dist/actions/listNav.d.ts +48 -3
- package/dist/actions/listNav.js +267 -77
- package/dist/actions/longpress.d.ts +37 -0
- package/dist/actions/longpress.js +105 -0
- package/dist/actions/stickToBottom.d.ts +66 -0
- package/dist/actions/stickToBottom.js +155 -0
- package/dist/actions/swipe.d.ts +64 -0
- package/dist/actions/swipe.js +179 -0
- package/dist/components/BottomSheet.svelte +280 -0
- package/dist/components/BottomSheet.svelte.d.ts +27 -0
- package/dist/components/ConfirmDialog.svelte +21 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
- package/dist/components/ConfirmModal.svelte +15 -0
- package/dist/components/ConfirmModal.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +18 -1
- package/dist/components/Drawer.svelte.d.ts +9 -1
- package/dist/components/InputDialog.svelte +21 -0
- package/dist/components/InputDialog.svelte.d.ts +11 -1
- package/dist/components/Modal.svelte +18 -1
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/basic/Empty.svelte +173 -9
- package/dist/components/basic/Empty.svelte.d.ts +50 -1
- package/dist/components/basic/ErrorState.svelte +128 -0
- package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
- package/dist/components/basic/JumpToLatest.svelte +60 -0
- package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
- package/dist/components/basic/PageHeader.svelte +216 -0
- package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
- package/dist/components/basic/ProgressButton.svelte +221 -0
- package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
- package/dist/components/graph/GraphApiBridge.svelte +36 -0
- package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
- package/dist/components/graph/GraphCanvas.svelte +212 -27
- package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
- package/dist/components/graph/api.d.ts +52 -0
- package/dist/components/graph/api.js +66 -0
- package/dist/components/graph/index.d.ts +2 -0
- package/dist/components/graph/index.js +1 -0
- package/dist/components/graph/theme.js +13 -0
- package/dist/components/messaging/MessageItem.svelte +5 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
- package/dist/components/table/DataTable.svelte +452 -78
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
- package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
- package/dist/components/table/columnStorage.d.ts +29 -0
- package/dist/components/table/columnStorage.js +61 -0
- package/dist/components/table/groupRows.d.ts +72 -0
- package/dist/components/table/groupRows.js +118 -0
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +121 -20
- package/dist/index.d.ts +24 -2
- package/dist/index.js +36 -1
- package/dist/utils/ansiToHtml.d.ts +83 -0
- package/dist/utils/ansiToHtml.js +543 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
- package/dist/utils/keyboardShortcutRegistry.js +1070 -76
- package/dist/utils/pollBackoff.d.ts +83 -0
- package/dist/utils/pollBackoff.js +151 -0
- package/dist/utils/snapFlick.d.ts +59 -0
- package/dist/utils/snapFlick.js +66 -0
- package/dist/video/advisors/qr.d.ts +1 -1
- package/dist/virtual/createRowWindow.d.ts +75 -0
- package/dist/virtual/createRowWindow.js +129 -0
- package/dist/virtual/index.d.ts +4 -0
- package/dist/virtual/index.js +5 -0
- package/dist/virtual/rowWindowCore.d.ts +202 -0
- package/dist/virtual/rowWindowCore.js +510 -0
- package/package.json +12 -2
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rowWindowCore — the framework-free engine behind `createRowWindow`
|
|
3
|
+
* (jatui-u6d4e.1, spec: jat docs/design/datatable-virtualisation-group-rows.md §3.1).
|
|
4
|
+
*
|
|
5
|
+
* Variable-height windowing for SPACER-ROW layouts: the consumer renders
|
|
6
|
+
* `paddingTop` px of spacer, the rendered slice in document flow, then
|
|
7
|
+
* `paddingBottom` px of spacer. Nothing is absolutely positioned, so a
|
|
8
|
+
* `<table>` keeps `table-layout: fixed` widths and native sticky `<thead>`s.
|
|
9
|
+
*
|
|
10
|
+
* Layering:
|
|
11
|
+
* - Pure helpers (`normalizeStickyLevels`, `activeStickyIndices`,
|
|
12
|
+
* `extractWindowIndices`, `layoutWindow`, `findScrollRoot`) hold the
|
|
13
|
+
* window policy — threshold, pinned keys, sticky group headers, spacer
|
|
14
|
+
* maths. DOM-free, so `npm run test:unit` covers them under node:test.
|
|
15
|
+
* - `RowWindowEngine` drives `@tanstack/virtual-core` (an OPTIONAL peer,
|
|
16
|
+
* which is why this module only ships behind `@joewinke/jatui/virtual`)
|
|
17
|
+
* for measured sizes, scroll-offset tracking and scroll-anchor correction
|
|
18
|
+
* when a row above the viewport changes height.
|
|
19
|
+
* - `createRowWindow.svelte.ts` is the thin runes wrapper.
|
|
20
|
+
*/
|
|
21
|
+
import { Virtualizer, defaultRangeExtractor, elementScroll, observeElementOffset, observeElementRect, observeWindowOffset, observeWindowRect, windowScroll } from '@tanstack/virtual-core';
|
|
22
|
+
export const EMPTY_SNAPSHOT = Object.freeze({
|
|
23
|
+
items: Object.freeze([]),
|
|
24
|
+
paddingTop: 0,
|
|
25
|
+
paddingBottom: 0,
|
|
26
|
+
totalSize: 0,
|
|
27
|
+
windowed: false,
|
|
28
|
+
count: 0
|
|
29
|
+
});
|
|
30
|
+
// ─── Pure helpers ───────────────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* `stickyIndices` accepts one flat list (one header level) or one list per
|
|
33
|
+
* level, outermost first (`[projectHeaderIdx[], epicHeaderIdx[]]`). Returns the
|
|
34
|
+
* per-level form, each level sorted ascending with out-of-range entries dropped.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizeStickyLevels(input, count) {
|
|
37
|
+
if (!input || input.length === 0)
|
|
38
|
+
return [];
|
|
39
|
+
const levels = Array.isArray(input[0])
|
|
40
|
+
? input
|
|
41
|
+
: [input];
|
|
42
|
+
return levels.map((level) => level
|
|
43
|
+
.filter((i) => Number.isInteger(i) && i >= 0 && i < count)
|
|
44
|
+
.slice()
|
|
45
|
+
.sort((a, b) => a - b));
|
|
46
|
+
}
|
|
47
|
+
/** Last element of sorted `arr` that is `<= max` and `> floor`, else -1. */
|
|
48
|
+
function lastAtOrBelow(arr, max, floor) {
|
|
49
|
+
let lo = 0;
|
|
50
|
+
let hi = arr.length - 1;
|
|
51
|
+
let found = -1;
|
|
52
|
+
while (lo <= hi) {
|
|
53
|
+
const mid = (lo + hi) >> 1;
|
|
54
|
+
if (arr[mid] <= max) {
|
|
55
|
+
found = arr[mid];
|
|
56
|
+
lo = mid + 1;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
hi = mid - 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return found > floor ? found : -1;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The sticky header that is "active" at `startIndex` (the first visible item),
|
|
66
|
+
* per level. For level n it is the last level-n header at or above
|
|
67
|
+
* `startIndex` that comes AFTER level n-1's active header, so an epic header
|
|
68
|
+
* from the previous project never sticks under the current project's header.
|
|
69
|
+
*/
|
|
70
|
+
export function activeStickyIndices(levels, startIndex) {
|
|
71
|
+
const out = [];
|
|
72
|
+
let floor = -1;
|
|
73
|
+
for (const level of levels) {
|
|
74
|
+
const idx = lastAtOrBelow(level, startIndex, floor);
|
|
75
|
+
if (idx >= 0) {
|
|
76
|
+
out.push(idx);
|
|
77
|
+
floor = idx;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The rangeExtractor: which indices to render for a visible `range`. The
|
|
84
|
+
* visible slice plus overscan, every pinned index, and the active sticky
|
|
85
|
+
* header of each level, sorted and de-duplicated. Below `threshold`, all.
|
|
86
|
+
*/
|
|
87
|
+
export function extractWindowIndices(range, policy) {
|
|
88
|
+
const { count } = range;
|
|
89
|
+
if (count <= 0)
|
|
90
|
+
return [];
|
|
91
|
+
if (policy.threshold > 0 && count < policy.threshold) {
|
|
92
|
+
return Array.from({ length: count }, (_, i) => i);
|
|
93
|
+
}
|
|
94
|
+
const base = defaultRangeExtractor(range);
|
|
95
|
+
const extra = [];
|
|
96
|
+
for (const i of activeStickyIndices(policy.stickyLevels, range.startIndex))
|
|
97
|
+
extra.push(i);
|
|
98
|
+
for (const i of policy.pinnedIndices)
|
|
99
|
+
if (i >= 0 && i < count)
|
|
100
|
+
extra.push(i);
|
|
101
|
+
if (extra.length === 0)
|
|
102
|
+
return base;
|
|
103
|
+
const lo = base.length ? base[0] : Infinity;
|
|
104
|
+
const hi = base.length ? base[base.length - 1] : -Infinity;
|
|
105
|
+
const outside = extra.filter((i) => i < lo || i > hi);
|
|
106
|
+
if (outside.length === 0)
|
|
107
|
+
return base;
|
|
108
|
+
return Array.from(new Set([...base, ...outside])).sort((a, b) => a - b);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Turn virtual-core's measured items (absolute offsets that include
|
|
112
|
+
* `scrollMargin`) into the spacer-row snapshot the consumer renders.
|
|
113
|
+
*/
|
|
114
|
+
export function layoutWindow(virtualItems, opts) {
|
|
115
|
+
const items = [];
|
|
116
|
+
let prevEnd = 0;
|
|
117
|
+
for (let k = 0; k < virtualItems.length; k++) {
|
|
118
|
+
const v = virtualItems[k];
|
|
119
|
+
const start = v.start - opts.scrollMargin;
|
|
120
|
+
const end = start + v.size;
|
|
121
|
+
items.push({
|
|
122
|
+
index: v.index,
|
|
123
|
+
key: v.key,
|
|
124
|
+
start,
|
|
125
|
+
size: v.size,
|
|
126
|
+
end,
|
|
127
|
+
gapBefore: k === 0 ? 0 : Math.max(0, start - prevEnd),
|
|
128
|
+
sticky: opts.stickySet.has(v.index),
|
|
129
|
+
pinned: opts.pinnedSet.has(v.index)
|
|
130
|
+
});
|
|
131
|
+
prevEnd = end;
|
|
132
|
+
}
|
|
133
|
+
const totalSize = Math.max(opts.totalSize, prevEnd);
|
|
134
|
+
return {
|
|
135
|
+
items,
|
|
136
|
+
paddingTop: items.length ? Math.max(0, items[0].start) : 0,
|
|
137
|
+
paddingBottom: items.length ? Math.max(0, totalSize - prevEnd) : totalSize,
|
|
138
|
+
totalSize,
|
|
139
|
+
windowed: opts.windowed,
|
|
140
|
+
count: opts.count
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Cheap structural equality so unchanged layouts don't churn reactive state. */
|
|
144
|
+
export function sameSnapshot(a, b) {
|
|
145
|
+
if (a.paddingTop !== b.paddingTop ||
|
|
146
|
+
a.paddingBottom !== b.paddingBottom ||
|
|
147
|
+
a.totalSize !== b.totalSize ||
|
|
148
|
+
a.windowed !== b.windowed ||
|
|
149
|
+
a.count !== b.count ||
|
|
150
|
+
a.items.length !== b.items.length) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
for (let i = 0; i < a.items.length; i++) {
|
|
154
|
+
const x = a.items[i];
|
|
155
|
+
const y = b.items[i];
|
|
156
|
+
if (x.index !== y.index ||
|
|
157
|
+
x.key !== y.key ||
|
|
158
|
+
x.start !== y.start ||
|
|
159
|
+
x.size !== y.size ||
|
|
160
|
+
x.gapBefore !== y.gapBefore ||
|
|
161
|
+
x.sticky !== y.sticky ||
|
|
162
|
+
x.pinned !== y.pinned) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* `scrollRoot: 'auto'` — the nearest ancestor that actually scrolls
|
|
170
|
+
* vertically: `overflow-y` is auto/scroll/overlay AND its content overflows it.
|
|
171
|
+
* An `overflow-auto` wrapper that grows with its content never scrolls (the
|
|
172
|
+
* dash TaskTable case), so it is skipped in favour of the real scroller.
|
|
173
|
+
* Returns `null` for "the window".
|
|
174
|
+
*/
|
|
175
|
+
export function findScrollRoot(node, overflowY) {
|
|
176
|
+
let el = node.parentElement;
|
|
177
|
+
while (el) {
|
|
178
|
+
const tag = el.tagName.toUpperCase();
|
|
179
|
+
if (tag === 'BODY' || tag === 'HTML')
|
|
180
|
+
return null;
|
|
181
|
+
const o = overflowY(el);
|
|
182
|
+
if ((o === 'auto' || o === 'scroll' || o === 'overlay') && el.scrollHeight > el.clientHeight) {
|
|
183
|
+
return el;
|
|
184
|
+
}
|
|
185
|
+
el = el.parentElement;
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
function isWindowLike(x) {
|
|
190
|
+
return !!x && typeof x === 'object' && 'document' in x && 'scrollY' in x;
|
|
191
|
+
}
|
|
192
|
+
/** The ResizeObserver constructor of a document's window, if it has one. */
|
|
193
|
+
function resizeObserverOf(view) {
|
|
194
|
+
return view?.ResizeObserver;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Imperative windowing engine. One per list. Feed it the render list with
|
|
198
|
+
* `setItems`, mount it with `attach(anchor)`, register each rendered element
|
|
199
|
+
* with `measure`, and read `snapshot` (or listen via `onChange`).
|
|
200
|
+
*/
|
|
201
|
+
export class RowWindowEngine {
|
|
202
|
+
opts;
|
|
203
|
+
v;
|
|
204
|
+
keys = [];
|
|
205
|
+
estimates = [];
|
|
206
|
+
keyIndex = new Map();
|
|
207
|
+
getItemKey = (i) => i;
|
|
208
|
+
estimateSize = () => 0;
|
|
209
|
+
pinnedKeys = [];
|
|
210
|
+
pinnedIndices = [];
|
|
211
|
+
pinnedSet = new Set();
|
|
212
|
+
stickyInput = [];
|
|
213
|
+
stickyLevels = [];
|
|
214
|
+
stickySet = new Set();
|
|
215
|
+
rangeExtractor = defaultRangeExtractor;
|
|
216
|
+
scrollMargin = 0;
|
|
217
|
+
anchorEl = null;
|
|
218
|
+
root = null;
|
|
219
|
+
rootUnsub = null;
|
|
220
|
+
anchorRO = null;
|
|
221
|
+
prevOverflowAnchor = '';
|
|
222
|
+
unmountVirtualizer = null;
|
|
223
|
+
nodes = new Map();
|
|
224
|
+
pendingScroll = null;
|
|
225
|
+
sweepQueued = false;
|
|
226
|
+
destroyed = false;
|
|
227
|
+
snapshot = EMPTY_SNAPSHOT;
|
|
228
|
+
constructor(opts) {
|
|
229
|
+
this.opts = { ...opts };
|
|
230
|
+
this.v = new Virtualizer(this.buildOptions(null));
|
|
231
|
+
}
|
|
232
|
+
// ── inputs ──
|
|
233
|
+
/** Replace the render list. `keys[i]` must be unique. */
|
|
234
|
+
setItems(keys, estimates) {
|
|
235
|
+
this.keys = keys;
|
|
236
|
+
this.estimates = estimates;
|
|
237
|
+
const map = new Map();
|
|
238
|
+
for (let i = 0; i < keys.length; i++)
|
|
239
|
+
map.set(keys[i], i);
|
|
240
|
+
this.keyIndex = map;
|
|
241
|
+
// New closures on purpose: virtual-core memoises its measurements on the
|
|
242
|
+
// getItemKey identity, so a reorder with an unchanged count still relayouts.
|
|
243
|
+
this.getItemKey = (i) => keys[i];
|
|
244
|
+
this.estimateSize = (i) => estimates[i] ?? 0;
|
|
245
|
+
// Mounted elements keep their key but may have a new index.
|
|
246
|
+
for (const [key, node] of this.nodes) {
|
|
247
|
+
const idx = map.get(key);
|
|
248
|
+
node.setAttribute('data-index', idx === undefined ? '-1' : String(idx));
|
|
249
|
+
}
|
|
250
|
+
this.resolvePinned();
|
|
251
|
+
this.stickyLevels = normalizeStickyLevels(this.stickyInput, keys.length);
|
|
252
|
+
this.stickySet = new Set(this.stickyLevels.flat());
|
|
253
|
+
this.rebuildRangeExtractor();
|
|
254
|
+
this.sync();
|
|
255
|
+
}
|
|
256
|
+
setPinned(keys) {
|
|
257
|
+
this.pinnedKeys = Array.from(keys);
|
|
258
|
+
this.resolvePinned();
|
|
259
|
+
this.rebuildRangeExtractor();
|
|
260
|
+
this.sync();
|
|
261
|
+
}
|
|
262
|
+
/** Change the render-everything threshold. */
|
|
263
|
+
setThreshold(threshold) {
|
|
264
|
+
if (threshold === this.opts.threshold)
|
|
265
|
+
return;
|
|
266
|
+
this.opts.threshold = threshold;
|
|
267
|
+
this.rebuildRangeExtractor();
|
|
268
|
+
this.sync();
|
|
269
|
+
}
|
|
270
|
+
setSticky(input) {
|
|
271
|
+
this.stickyInput = input;
|
|
272
|
+
this.stickyLevels = normalizeStickyLevels(input, this.keys.length);
|
|
273
|
+
this.stickySet = new Set(this.stickyLevels.flat());
|
|
274
|
+
this.rebuildRangeExtractor();
|
|
275
|
+
this.sync();
|
|
276
|
+
}
|
|
277
|
+
resolvePinned() {
|
|
278
|
+
const idx = [];
|
|
279
|
+
for (const k of this.pinnedKeys) {
|
|
280
|
+
const i = this.keyIndex.get(k);
|
|
281
|
+
if (i !== undefined)
|
|
282
|
+
idx.push(i);
|
|
283
|
+
}
|
|
284
|
+
this.pinnedIndices = idx;
|
|
285
|
+
this.pinnedSet = new Set(idx);
|
|
286
|
+
}
|
|
287
|
+
rebuildRangeExtractor() {
|
|
288
|
+
const policy = {
|
|
289
|
+
threshold: this.opts.threshold,
|
|
290
|
+
pinnedIndices: this.pinnedIndices,
|
|
291
|
+
stickyLevels: this.stickyLevels
|
|
292
|
+
};
|
|
293
|
+
this.rangeExtractor = (range) => extractWindowIndices(range, policy);
|
|
294
|
+
}
|
|
295
|
+
// ── virtual-core wiring ──
|
|
296
|
+
buildOptions(root) {
|
|
297
|
+
const win = isWindowLike(root);
|
|
298
|
+
return {
|
|
299
|
+
count: this.keys.length,
|
|
300
|
+
getScrollElement: () => root,
|
|
301
|
+
estimateSize: this.estimateSize,
|
|
302
|
+
getItemKey: this.getItemKey,
|
|
303
|
+
rangeExtractor: this.rangeExtractor,
|
|
304
|
+
overscan: this.opts.overscan,
|
|
305
|
+
scrollMargin: this.scrollMargin,
|
|
306
|
+
scrollPaddingStart: this.opts.scrollPaddingStart,
|
|
307
|
+
scrollPaddingEnd: this.opts.scrollPaddingEnd,
|
|
308
|
+
initialRect: { width: 0, height: this.opts.initialViewportSize },
|
|
309
|
+
observeElementRect: (win ? observeWindowRect : observeElementRect),
|
|
310
|
+
observeElementOffset: (win ? observeWindowOffset : observeElementOffset),
|
|
311
|
+
scrollToFn: (win ? windowScroll : elementScroll),
|
|
312
|
+
onChange: () => this.emit()
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Push current inputs into virtual-core and recompute the snapshot. */
|
|
316
|
+
sync() {
|
|
317
|
+
if (this.destroyed)
|
|
318
|
+
return;
|
|
319
|
+
this.v.setOptions(this.buildOptions(this.root));
|
|
320
|
+
if (this.anchorEl)
|
|
321
|
+
this.v._willUpdate();
|
|
322
|
+
this.emit();
|
|
323
|
+
}
|
|
324
|
+
emit() {
|
|
325
|
+
if (this.destroyed)
|
|
326
|
+
return;
|
|
327
|
+
const count = this.keys.length;
|
|
328
|
+
const next = layoutWindow(this.v.getVirtualItems(), {
|
|
329
|
+
scrollMargin: this.scrollMargin,
|
|
330
|
+
totalSize: this.v.getTotalSize(),
|
|
331
|
+
count,
|
|
332
|
+
windowed: !(this.opts.threshold > 0 && count < this.opts.threshold),
|
|
333
|
+
stickySet: this.stickySet,
|
|
334
|
+
pinnedSet: this.pinnedSet
|
|
335
|
+
});
|
|
336
|
+
if (sameSnapshot(next, this.snapshot))
|
|
337
|
+
return;
|
|
338
|
+
this.snapshot = next;
|
|
339
|
+
this.opts.onChange?.(next);
|
|
340
|
+
}
|
|
341
|
+
// ── DOM lifecycle ──
|
|
342
|
+
/** Mount on the element whose top is item 0's top (a `<tbody>` or list container). */
|
|
343
|
+
attach(anchor) {
|
|
344
|
+
if (this.destroyed)
|
|
345
|
+
return;
|
|
346
|
+
this.anchorEl = anchor;
|
|
347
|
+
// The engine does its own scroll-anchor correction when a row above the
|
|
348
|
+
// viewport is measured; native CSS scroll anchoring would correct the same
|
|
349
|
+
// shift a second time and jump.
|
|
350
|
+
this.prevOverflowAnchor = anchor.style.overflowAnchor;
|
|
351
|
+
anchor.style.overflowAnchor = 'none';
|
|
352
|
+
this.bindRoot(this.resolveRoot(anchor));
|
|
353
|
+
this.unmountVirtualizer = this.v._didMount();
|
|
354
|
+
this.v._willUpdate();
|
|
355
|
+
for (const node of this.nodes.values()) {
|
|
356
|
+
if (node.isConnected)
|
|
357
|
+
this.v.measureElement(node);
|
|
358
|
+
}
|
|
359
|
+
const RO = resizeObserverOf(anchor.ownerDocument?.defaultView);
|
|
360
|
+
if (RO) {
|
|
361
|
+
this.anchorRO = new RO(() => this.onLayoutShift());
|
|
362
|
+
this.anchorRO.observe(anchor);
|
|
363
|
+
}
|
|
364
|
+
this.emit();
|
|
365
|
+
if (this.pendingScroll) {
|
|
366
|
+
const { index, align } = this.pendingScroll;
|
|
367
|
+
this.pendingScroll = null;
|
|
368
|
+
this.scrollToIndex(index, { align });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
detach() {
|
|
372
|
+
this.rootUnsub?.();
|
|
373
|
+
this.rootUnsub = null;
|
|
374
|
+
this.anchorRO?.disconnect();
|
|
375
|
+
this.anchorRO = null;
|
|
376
|
+
this.unmountVirtualizer?.();
|
|
377
|
+
this.unmountVirtualizer = null;
|
|
378
|
+
if (this.anchorEl)
|
|
379
|
+
this.anchorEl.style.overflowAnchor = this.prevOverflowAnchor;
|
|
380
|
+
this.anchorEl = null;
|
|
381
|
+
this.root = null;
|
|
382
|
+
}
|
|
383
|
+
destroy() {
|
|
384
|
+
this.detach();
|
|
385
|
+
this.nodes.clear();
|
|
386
|
+
this.destroyed = true;
|
|
387
|
+
}
|
|
388
|
+
resolveRoot(anchor) {
|
|
389
|
+
const view = anchor.ownerDocument?.defaultView;
|
|
390
|
+
const configured = this.opts.scrollRoot;
|
|
391
|
+
if (configured !== 'auto')
|
|
392
|
+
return configured;
|
|
393
|
+
const found = findScrollRoot(anchor, (el) => view.getComputedStyle(el).overflowY);
|
|
394
|
+
return found ?? view;
|
|
395
|
+
}
|
|
396
|
+
bindRoot(root) {
|
|
397
|
+
this.rootUnsub?.();
|
|
398
|
+
this.root = root;
|
|
399
|
+
// Registered BEFORE virtual-core's own scroll listener (`_willUpdate` adds
|
|
400
|
+
// it), so the margin is fresh when virtual-core recomputes the range.
|
|
401
|
+
const onScroll = () => this.updateScrollMargin();
|
|
402
|
+
root.addEventListener('scroll', onScroll, { passive: true });
|
|
403
|
+
let rootRO = null;
|
|
404
|
+
const RO = isWindowLike(root) ? undefined : resizeObserverOf(root.ownerDocument?.defaultView);
|
|
405
|
+
if (RO && !isWindowLike(root)) {
|
|
406
|
+
const ro = new RO(() => this.onLayoutShift());
|
|
407
|
+
ro.observe(root);
|
|
408
|
+
rootRO = ro;
|
|
409
|
+
}
|
|
410
|
+
this.rootUnsub = () => {
|
|
411
|
+
root.removeEventListener('scroll', onScroll);
|
|
412
|
+
rootRO?.disconnect();
|
|
413
|
+
};
|
|
414
|
+
this.updateScrollMargin(false);
|
|
415
|
+
this.v.setOptions(this.buildOptions(root));
|
|
416
|
+
}
|
|
417
|
+
/** The anchor or root resized: re-resolve an 'auto' root, refresh the margin. */
|
|
418
|
+
onLayoutShift() {
|
|
419
|
+
if (!this.anchorEl || this.destroyed)
|
|
420
|
+
return;
|
|
421
|
+
if (this.opts.scrollRoot === 'auto') {
|
|
422
|
+
const next = this.resolveRoot(this.anchorEl);
|
|
423
|
+
if (next !== this.root) {
|
|
424
|
+
this.bindRoot(next);
|
|
425
|
+
this.v._willUpdate();
|
|
426
|
+
this.emit();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
this.updateScrollMargin();
|
|
431
|
+
}
|
|
432
|
+
/** Distance from the scroller's content top to the anchor's top. */
|
|
433
|
+
updateScrollMargin(apply = true) {
|
|
434
|
+
const anchor = this.anchorEl;
|
|
435
|
+
const root = this.root;
|
|
436
|
+
if (!anchor || !root)
|
|
437
|
+
return;
|
|
438
|
+
const anchorTop = anchor.getBoundingClientRect().top;
|
|
439
|
+
let margin;
|
|
440
|
+
if (isWindowLike(root)) {
|
|
441
|
+
margin = anchorTop + root.scrollY;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
const el = root;
|
|
445
|
+
margin = anchorTop - el.getBoundingClientRect().top - el.clientTop + el.scrollTop;
|
|
446
|
+
}
|
|
447
|
+
margin = Math.round(margin);
|
|
448
|
+
if (margin === this.scrollMargin)
|
|
449
|
+
return;
|
|
450
|
+
this.scrollMargin = margin;
|
|
451
|
+
if (apply)
|
|
452
|
+
this.sync();
|
|
453
|
+
}
|
|
454
|
+
// ── per-item measurement ──
|
|
455
|
+
/** Register a rendered item element. Re-measured on every resize. */
|
|
456
|
+
measure(node, key) {
|
|
457
|
+
const idx = this.keyIndex.get(key);
|
|
458
|
+
node.setAttribute('data-index', idx === undefined ? '-1' : String(idx));
|
|
459
|
+
const prev = this.nodes.get(key);
|
|
460
|
+
if (prev && prev !== node && !prev.isConnected)
|
|
461
|
+
this.queueSweep();
|
|
462
|
+
this.nodes.set(key, node);
|
|
463
|
+
if (idx !== undefined)
|
|
464
|
+
this.v.measureElement(node);
|
|
465
|
+
}
|
|
466
|
+
/** Forget an item element (it unmounted, or its key changed). */
|
|
467
|
+
unmeasure(node, key) {
|
|
468
|
+
if (this.nodes.get(key) === node)
|
|
469
|
+
this.nodes.delete(key);
|
|
470
|
+
this.queueSweep();
|
|
471
|
+
}
|
|
472
|
+
queueSweep() {
|
|
473
|
+
if (this.sweepQueued)
|
|
474
|
+
return;
|
|
475
|
+
this.sweepQueued = true;
|
|
476
|
+
queueMicrotask(() => {
|
|
477
|
+
this.sweepQueued = false;
|
|
478
|
+
if (!this.destroyed)
|
|
479
|
+
this.v.measureElement(null);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
// ── scrolling ──
|
|
483
|
+
scrollToIndex(index, opts = {}) {
|
|
484
|
+
const align = opts.align ?? 'auto';
|
|
485
|
+
if (index < 0 || index >= this.keys.length)
|
|
486
|
+
return;
|
|
487
|
+
if (!this.anchorEl) {
|
|
488
|
+
this.pendingScroll = { index, align };
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.v.scrollToIndex(index, { align });
|
|
492
|
+
}
|
|
493
|
+
scrollToKey(key, opts = {}) {
|
|
494
|
+
const idx = this.keyIndex.get(key);
|
|
495
|
+
if (idx !== undefined)
|
|
496
|
+
this.scrollToIndex(idx, opts);
|
|
497
|
+
}
|
|
498
|
+
/** Index of `key` in the render list, or -1. */
|
|
499
|
+
indexOf(key) {
|
|
500
|
+
return this.keyIndex.get(key) ?? -1;
|
|
501
|
+
}
|
|
502
|
+
/** Drop every cached measurement (e.g. after a column-width change). */
|
|
503
|
+
remeasure() {
|
|
504
|
+
this.v.measure();
|
|
505
|
+
for (const node of this.nodes.values())
|
|
506
|
+
if (node.isConnected)
|
|
507
|
+
this.v.measureElement(node);
|
|
508
|
+
this.emit();
|
|
509
|
+
}
|
|
510
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joewinke/jatui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared Svelte 5 component library for JAT projects",
|
|
6
6
|
"type": "module",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"svelte": "./dist/components/table/index.js",
|
|
35
35
|
"default": "./dist/components/table/index.js"
|
|
36
36
|
},
|
|
37
|
+
"./virtual": {
|
|
38
|
+
"types": "./dist/virtual/index.d.ts",
|
|
39
|
+
"svelte": "./dist/virtual/index.js",
|
|
40
|
+
"default": "./dist/virtual/index.js"
|
|
41
|
+
},
|
|
37
42
|
"./graph": {
|
|
38
43
|
"types": "./dist/components/graph/index.d.ts",
|
|
39
44
|
"svelte": "./dist/components/graph/index.js",
|
|
@@ -76,7 +81,7 @@
|
|
|
76
81
|
"build": "vite build",
|
|
77
82
|
"build:site": "npm run package && vite build",
|
|
78
83
|
"preview": "vite preview",
|
|
79
|
-
"package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs",
|
|
84
|
+
"package": "svelte-package --input src/lib --output dist && node scripts/compile-rune-modules.mjs && node scripts/verify-dist.mjs",
|
|
80
85
|
"check:demos": "node scripts/check-demo-coverage.mjs",
|
|
81
86
|
"guard": "node scripts/check-demo-coverage.mjs",
|
|
82
87
|
"test:dist": "svelte-kit sync && vitest run --config vitest.dist.config.ts",
|
|
@@ -91,6 +96,7 @@
|
|
|
91
96
|
"@atlaskit/pragmatic-drag-and-drop-live-region": "~2.0.0",
|
|
92
97
|
"@paper-design/shaders": ">=0.0.50",
|
|
93
98
|
"@tanstack/table-core": "^8.0.0",
|
|
99
|
+
"@tanstack/virtual-core": "^3.17.0",
|
|
94
100
|
"@xyflow/svelte": "^1.0.0",
|
|
95
101
|
"cobe": "^2.0.0",
|
|
96
102
|
"d3": "^7.0.0",
|
|
@@ -127,6 +133,9 @@
|
|
|
127
133
|
"@tanstack/table-core": {
|
|
128
134
|
"optional": true
|
|
129
135
|
},
|
|
136
|
+
"@tanstack/virtual-core": {
|
|
137
|
+
"optional": true
|
|
138
|
+
},
|
|
130
139
|
"@xyflow/svelte": {
|
|
131
140
|
"optional": true
|
|
132
141
|
},
|
|
@@ -163,6 +172,7 @@
|
|
|
163
172
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
164
173
|
"@tailwindcss/vite": "^4.0.0",
|
|
165
174
|
"@tanstack/table-core": "^8.21.3",
|
|
175
|
+
"@tanstack/virtual-core": "^3.17.11",
|
|
166
176
|
"@types/d3": "^7.4.3",
|
|
167
177
|
"@vitest/browser": "^3.2.7",
|
|
168
178
|
"@xyflow/svelte": "^1.6.2",
|