vlist 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/index.js +1 -28
  2. package/dist/internals.js +1 -60
  3. package/package.json +1 -1
  4. package/dist/constants.js +0 -83
  5. package/dist/core/create.js +0 -740
  6. package/dist/core/dom.js +0 -47
  7. package/dist/core/hooks.js +0 -67
  8. package/dist/core/index.js +0 -13
  9. package/dist/core/pipeline.js +0 -307
  10. package/dist/core/pool.js +0 -42
  11. package/dist/core/scroll.js +0 -137
  12. package/dist/core/sizes.js +0 -6
  13. package/dist/core/state.js +0 -56
  14. package/dist/core/types.js +0 -7
  15. package/dist/core/velocity.js +0 -33
  16. package/dist/events/emitter.js +0 -60
  17. package/dist/events/index.js +0 -6
  18. package/dist/plugins/a11y/index.js +0 -1
  19. package/dist/plugins/a11y/plugin.js +0 -259
  20. package/dist/plugins/async/index.js +0 -12
  21. package/dist/plugins/async/manager.js +0 -568
  22. package/dist/plugins/async/placeholder.js +0 -154
  23. package/dist/plugins/async/plugin.js +0 -311
  24. package/dist/plugins/async/sparse.js +0 -540
  25. package/dist/plugins/autosize/index.js +0 -4
  26. package/dist/plugins/autosize/plugin.js +0 -185
  27. package/dist/plugins/grid/index.js +0 -5
  28. package/dist/plugins/grid/layout.js +0 -275
  29. package/dist/plugins/grid/plugin.js +0 -347
  30. package/dist/plugins/grid/renderer.js +0 -525
  31. package/dist/plugins/grid/types.js +0 -11
  32. package/dist/plugins/groups/async-bridge.js +0 -246
  33. package/dist/plugins/groups/index.js +0 -13
  34. package/dist/plugins/groups/layout.js +0 -294
  35. package/dist/plugins/groups/plugin.js +0 -571
  36. package/dist/plugins/groups/sticky.js +0 -255
  37. package/dist/plugins/groups/types.js +0 -12
  38. package/dist/plugins/masonry/index.js +0 -6
  39. package/dist/plugins/masonry/layout.js +0 -261
  40. package/dist/plugins/masonry/plugin.js +0 -381
  41. package/dist/plugins/masonry/renderer.js +0 -354
  42. package/dist/plugins/masonry/types.js +0 -9
  43. package/dist/plugins/page/index.js +0 -5
  44. package/dist/plugins/page/plugin.js +0 -166
  45. package/dist/plugins/scale/index.js +0 -4
  46. package/dist/plugins/scale/plugin.js +0 -507
  47. package/dist/plugins/scrollbar/controller.js +0 -574
  48. package/dist/plugins/scrollbar/index.js +0 -6
  49. package/dist/plugins/scrollbar/plugin.js +0 -93
  50. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  51. package/dist/plugins/selection/index.js +0 -7
  52. package/dist/plugins/selection/plugin.js +0 -601
  53. package/dist/plugins/selection/state.js +0 -332
  54. package/dist/plugins/snapshots/index.js +0 -5
  55. package/dist/plugins/snapshots/plugin.js +0 -301
  56. package/dist/plugins/sortable/index.js +0 -6
  57. package/dist/plugins/sortable/plugin.js +0 -753
  58. package/dist/plugins/table/header.js +0 -501
  59. package/dist/plugins/table/index.js +0 -12
  60. package/dist/plugins/table/layout.js +0 -211
  61. package/dist/plugins/table/plugin.js +0 -391
  62. package/dist/plugins/table/renderer.js +0 -625
  63. package/dist/plugins/table/types.js +0 -12
  64. package/dist/plugins/transition/index.js +0 -5
  65. package/dist/plugins/transition/plugin.js +0 -405
  66. package/dist/rendering/aria.js +0 -23
  67. package/dist/rendering/index.js +0 -18
  68. package/dist/rendering/measured.js +0 -98
  69. package/dist/rendering/renderer.js +0 -586
  70. package/dist/rendering/scale.js +0 -267
  71. package/dist/rendering/scroll.js +0 -71
  72. package/dist/rendering/sizes.js +0 -193
  73. package/dist/rendering/sort.js +0 -65
  74. package/dist/rendering/viewport.js +0 -268
  75. package/dist/types.js +0 -5
  76. package/dist/utils/padding.js +0 -49
  77. package/dist/utils/stats.js +0 -124
@@ -1,507 +0,0 @@
1
- /**
2
- * vlist v2 — Scale Plugin
3
- *
4
- * Enables support for 1M+ item lists by compressing the scroll space
5
- * when total content size exceeds the browser's ~16.7M pixel limit.
6
- *
7
- * Priority 20 — runs after layout plugins (10), before selection (50).
8
- *
9
- * When compressed:
10
- * - Native scroll is disabled (overflow: hidden)
11
- * - Custom wheel handler with lerp smooth scroll
12
- * - Custom touch handler with momentum
13
- * - Items positioned relative to viewport via onCalculate hook
14
- * - Fallback scrollbar created if no scrollbar plugin present
15
- *
16
- * Uses v1's rendering/scale.ts pure math functions directly.
17
- */
18
- import { getCompressionState, calculateCompressedVisibleRange, calculateCompressedItemPosition, calculateCompressedScrollToIndex, } from "../../rendering/scale";
19
- import { createScrollbar } from "../scrollbar/scrollbar";
20
- import { SCROLL_EASING, SCROLL_DURATION } from "../../constants";
21
- // =============================================================================
22
- // Constants
23
- // =============================================================================
24
- const LERP_FACTOR = 0.65;
25
- const SNAP_THRESHOLD = 0.5;
26
- const SCROLL_SPEED_MULTIPLIER = 1.7;
27
- const TOUCH_DECELERATION = 0.95;
28
- const TOUCH_MIN_VELOCITY = 0.1;
29
- const TOUCH_VELOCITY_SAMPLES = 5;
30
- const TOUCH_VELOCITY_WINDOW = 100;
31
- // =============================================================================
32
- // Factory
33
- // =============================================================================
34
- export function scale(config) {
35
- const force = config?.force ?? false;
36
- let engineState;
37
- let sizeCache;
38
- let viewport;
39
- let horizontal;
40
- let overscan;
41
- let compression = {
42
- isCompressed: false,
43
- actualSize: 0,
44
- virtualSize: 0,
45
- ratio: 1,
46
- };
47
- let compressedActive = false;
48
- let slack = 0;
49
- let fallbackScrollbar = null;
50
- let ownsScrollbar = false;
51
- // Virtual scroll state
52
- let virtualScrollPosition = 0;
53
- let targetScrollPosition = 0;
54
- let smoothScrollId = null;
55
- let easedScrollId = null;
56
- // Touch state
57
- let touchStartPos = 0;
58
- let touchScrollStart = 0;
59
- let momentumId = null;
60
- // Pre-allocated ring buffer for velocity sampling (zero-alloc hot path)
61
- const sampleTimes = new Float64Array(TOUCH_VELOCITY_SAMPLES);
62
- const samplePositions = new Float64Array(TOUCH_VELOCITY_SAMPLES);
63
- let sampleCount = 0;
64
- let sampleHead = 0;
65
- // Reusable range object for compressed visible range calc
66
- const compRange = { start: 0, end: -1 };
67
- // Track item count so onCalculate can detect data mutations
68
- let lastCheckedTotal = -1;
69
- let storedCtx = null;
70
- function getMaxScroll() {
71
- return Math.max(0, compression.virtualSize + slack - engineState.containerSize);
72
- }
73
- function computeSlack() {
74
- if (compression.virtualSize <= 0)
75
- return 0;
76
- return Math.max(0, engineState.containerSize * (1 - compression.ratio));
77
- }
78
- function updateCompression(ctx) {
79
- const totalItems = engineState.totalItems;
80
- compression = getCompressionState(totalItems, sizeCache, force);
81
- if (compression.isCompressed && !compressedActive) {
82
- activateCompression(ctx);
83
- }
84
- else if (!compression.isCompressed && compressedActive) {
85
- deactivateCompression(ctx);
86
- }
87
- if (compression.isCompressed) {
88
- slack = computeSlack();
89
- engineState.isCompressed = true;
90
- engineState.compressionRatio = compression.ratio;
91
- ctx.updateContentSize(compression.virtualSize + slack);
92
- if (fallbackScrollbar) {
93
- fallbackScrollbar.updateBounds(compression.virtualSize + slack, engineState.containerSize);
94
- }
95
- }
96
- }
97
- // Wheel/touch handlers — stored so we can remove them
98
- let wheelHandler = null;
99
- let touchStartHandler = null;
100
- let touchMoveHandler = null;
101
- let touchEndHandler = null;
102
- let nativeScrollReset = null;
103
- function applyVirtualScroll(pos) {
104
- virtualScrollPosition = pos;
105
- targetScrollPosition = pos;
106
- engineState.prevScrollPosition = engineState.scrollPosition;
107
- engineState.scrollPosition = pos;
108
- engineState.scrollDirection = pos > engineState.prevScrollPosition ? 1 : -1;
109
- storedCtx.forceRender();
110
- }
111
- function cancelAnimations() {
112
- if (smoothScrollId !== null) {
113
- cancelAnimationFrame(smoothScrollId);
114
- smoothScrollId = null;
115
- }
116
- if (easedScrollId !== null) {
117
- cancelAnimationFrame(easedScrollId);
118
- easedScrollId = null;
119
- }
120
- }
121
- function setVirtualPosition(pos) {
122
- cancelAnimations();
123
- applyVirtualScroll(Math.max(0, Math.min(pos, getMaxScroll())));
124
- }
125
- function easedScrollTo(target, duration, easing = SCROLL_EASING) {
126
- cancelAnimations();
127
- const clampedTarget = Math.max(0, Math.min(target, getMaxScroll()));
128
- const from = virtualScrollPosition;
129
- if (Math.abs(clampedTarget - from) < SNAP_THRESHOLD) {
130
- applyVirtualScroll(clampedTarget);
131
- return;
132
- }
133
- const start = performance.now();
134
- const tick = (now) => {
135
- const t = Math.min((now - start) / duration, 1);
136
- applyVirtualScroll(from + (clampedTarget - from) * easing(t));
137
- if (t < 1)
138
- easedScrollId = requestAnimationFrame(tick);
139
- else
140
- easedScrollId = null;
141
- };
142
- easedScrollId = requestAnimationFrame(tick);
143
- }
144
- function activateCompression(ctx) {
145
- compressedActive = true;
146
- engineState.isCompressed = true;
147
- const scrollProp = horizontal ? "scrollLeft" : "scrollTop";
148
- const overflowProp = horizontal ? "overflowX" : "overflow";
149
- // Capture native position before disabling
150
- const nativePos = viewport[scrollProp];
151
- viewport.style[overflowProp] = "hidden";
152
- viewport[scrollProp] = 0;
153
- if (nativePos > 0) {
154
- virtualScrollPosition = nativePos;
155
- targetScrollPosition = nativePos;
156
- engineState.scrollPosition = nativePos;
157
- }
158
- slack = computeSlack();
159
- // Lerp smooth scroll tick
160
- const smoothScrollTick = () => {
161
- const diff = targetScrollPosition - virtualScrollPosition;
162
- const maxScroll = getMaxScroll();
163
- if (Math.abs(diff) < SNAP_THRESHOLD) {
164
- virtualScrollPosition = Math.max(0, Math.min(targetScrollPosition, maxScroll));
165
- targetScrollPosition = virtualScrollPosition;
166
- smoothScrollId = null;
167
- }
168
- else {
169
- virtualScrollPosition += diff * LERP_FACTOR;
170
- virtualScrollPosition = Math.max(0, Math.min(virtualScrollPosition, maxScroll));
171
- smoothScrollId = requestAnimationFrame(smoothScrollTick);
172
- }
173
- engineState.prevScrollPosition = engineState.scrollPosition;
174
- engineState.scrollPosition = virtualScrollPosition;
175
- engineState.scrollDirection = virtualScrollPosition > engineState.prevScrollPosition ? 1 : -1;
176
- ctx.forceRender();
177
- };
178
- // Wheel handler
179
- wheelHandler = (e) => {
180
- e.preventDefault();
181
- const maxScroll = getMaxScroll();
182
- targetScrollPosition = Math.max(0, Math.min(targetScrollPosition + e.deltaY * compression.ratio * SCROLL_SPEED_MULTIPLIER, maxScroll));
183
- if (smoothScrollId === null) {
184
- smoothScrollId = requestAnimationFrame(smoothScrollTick);
185
- }
186
- };
187
- viewport.addEventListener("wheel", wheelHandler, { passive: false });
188
- // Touch handlers
189
- const cancelMomentum = () => {
190
- if (momentumId !== null) {
191
- cancelAnimationFrame(momentumId);
192
- momentumId = null;
193
- }
194
- };
195
- touchStartHandler = (e) => {
196
- cancelMomentum();
197
- if (smoothScrollId !== null) {
198
- cancelAnimationFrame(smoothScrollId);
199
- smoothScrollId = null;
200
- }
201
- const touch = e.touches[0];
202
- if (!touch)
203
- return;
204
- const y = horizontal ? touch.clientX : touch.clientY;
205
- touchStartPos = y;
206
- touchScrollStart = virtualScrollPosition;
207
- sampleCount = 1;
208
- sampleHead = 0;
209
- sampleTimes[0] = performance.now();
210
- samplePositions[0] = y;
211
- };
212
- touchMoveHandler = (e) => {
213
- e.preventDefault();
214
- const touch = e.touches[0];
215
- if (!touch)
216
- return;
217
- const y = horizontal ? touch.clientX : touch.clientY;
218
- const now = performance.now();
219
- const slot = sampleCount < TOUCH_VELOCITY_SAMPLES ? sampleCount : sampleHead;
220
- sampleTimes[slot] = now;
221
- samplePositions[slot] = y;
222
- if (sampleCount < TOUCH_VELOCITY_SAMPLES) {
223
- sampleCount++;
224
- }
225
- else {
226
- sampleHead = (sampleHead + 1) % TOUCH_VELOCITY_SAMPLES;
227
- }
228
- const delta = touchStartPos - y;
229
- const maxScroll = getMaxScroll();
230
- const newPos = Math.max(0, Math.min(touchScrollStart + delta * compression.ratio * SCROLL_SPEED_MULTIPLIER, maxScroll));
231
- virtualScrollPosition = newPos;
232
- targetScrollPosition = newPos;
233
- engineState.prevScrollPosition = engineState.scrollPosition;
234
- engineState.scrollPosition = newPos;
235
- engineState.scrollDirection = newPos > engineState.prevScrollPosition ? 1 : -1;
236
- ctx.forceRender();
237
- };
238
- touchEndHandler = () => {
239
- const now = performance.now();
240
- // Scan ring buffer for oldest/newest within velocity window (zero alloc)
241
- let oldestTime = Infinity, oldestPos = 0;
242
- let newestTime = -1, newestPos = 0;
243
- let recentCount = 0;
244
- for (let i = 0; i < sampleCount; i++) {
245
- const idx = (sampleHead + i) % TOUCH_VELOCITY_SAMPLES;
246
- const t = sampleTimes[idx];
247
- if (now - t >= TOUCH_VELOCITY_WINDOW)
248
- continue;
249
- recentCount++;
250
- const p = samplePositions[idx];
251
- if (t < oldestTime) {
252
- oldestTime = t;
253
- oldestPos = p;
254
- }
255
- if (t > newestTime) {
256
- newestTime = t;
257
- newestPos = p;
258
- }
259
- }
260
- let velocity = 0;
261
- if (recentCount >= 2) {
262
- const dt = newestTime - oldestTime;
263
- if (dt > 0)
264
- velocity = (oldestPos - newestPos) / dt;
265
- }
266
- sampleCount = 0;
267
- sampleHead = 0;
268
- if (Math.abs(velocity) < TOUCH_MIN_VELOCITY)
269
- return;
270
- let frameVelocity = velocity * 16;
271
- const momentumTick = () => {
272
- frameVelocity *= TOUCH_DECELERATION;
273
- if (Math.abs(frameVelocity) < 0.5) {
274
- momentumId = null;
275
- return;
276
- }
277
- const maxScroll = getMaxScroll();
278
- let newPos = virtualScrollPosition + frameVelocity;
279
- newPos = Math.max(0, Math.min(newPos, maxScroll));
280
- if ((newPos <= 0 && frameVelocity < 0) || (newPos >= maxScroll && frameVelocity > 0)) {
281
- virtualScrollPosition = newPos;
282
- targetScrollPosition = newPos;
283
- engineState.scrollPosition = newPos;
284
- ctx.forceRender();
285
- momentumId = null;
286
- return;
287
- }
288
- virtualScrollPosition = newPos;
289
- targetScrollPosition = newPos;
290
- engineState.prevScrollPosition = engineState.scrollPosition;
291
- engineState.scrollPosition = newPos;
292
- engineState.scrollDirection = newPos > engineState.prevScrollPosition ? 1 : -1;
293
- ctx.forceRender();
294
- momentumId = requestAnimationFrame(momentumTick);
295
- };
296
- momentumId = requestAnimationFrame(momentumTick);
297
- };
298
- viewport.addEventListener("touchstart", touchStartHandler, { passive: true });
299
- viewport.addEventListener("touchmove", touchMoveHandler, { passive: false });
300
- viewport.addEventListener("touchend", touchEndHandler, { passive: true });
301
- viewport.addEventListener("touchcancel", touchEndHandler, { passive: true });
302
- // Native scroll drift guard
303
- nativeScrollReset = () => {
304
- if (viewport[scrollProp] !== 0) {
305
- viewport[scrollProp] = 0;
306
- }
307
- };
308
- viewport.addEventListener("scroll", nativeScrollReset, { passive: true });
309
- // Scroll callback used by both existing scrollbar plugin and fallback
310
- const scrollbarCallback = (position) => {
311
- virtualScrollPosition = position;
312
- targetScrollPosition = position;
313
- if (smoothScrollId !== null) {
314
- cancelAnimationFrame(smoothScrollId);
315
- smoothScrollId = null;
316
- }
317
- engineState.prevScrollPosition = engineState.scrollPosition;
318
- engineState.scrollPosition = position;
319
- engineState.scrollDirection = position > engineState.prevScrollPosition ? 1 : -1;
320
- ctx.forceRender();
321
- };
322
- // If scrollbar plugin exists, redirect its callback and use its instance.
323
- // Otherwise create a fallback scrollbar.
324
- const setCallback = ctx.getMethod("_scrollbar:setCallback");
325
- const getInstance = ctx.getMethod("_scrollbar:getInstance");
326
- if (setCallback && getInstance) {
327
- setCallback(scrollbarCallback);
328
- const existing = getInstance();
329
- if (existing) {
330
- fallbackScrollbar = existing;
331
- ownsScrollbar = false;
332
- fallbackScrollbar.updateBounds(compression.virtualSize + slack, engineState.containerSize);
333
- }
334
- }
335
- else if (!fallbackScrollbar) {
336
- const classPrefix = ctx.config.classPrefix;
337
- fallbackScrollbar = createScrollbar(viewport, scrollbarCallback, {}, classPrefix, horizontal, ctx.dom.root);
338
- ownsScrollbar = true;
339
- if (!viewport.classList.contains(`${classPrefix}-viewport--custom-scrollbar`)) {
340
- viewport.classList.add(`${classPrefix}-viewport--custom-scrollbar`);
341
- }
342
- fallbackScrollbar.updateBounds(compression.virtualSize + slack, engineState.containerSize);
343
- }
344
- }
345
- function deactivateCompression(ctx) {
346
- compressedActive = false;
347
- engineState.isCompressed = false;
348
- engineState.compressionRatio = 1;
349
- virtualScrollPosition = 0;
350
- targetScrollPosition = 0;
351
- sampleCount = 0;
352
- sampleHead = 0;
353
- cleanup();
354
- const overflowProp = horizontal ? "overflowX" : "overflow";
355
- viewport.style[overflowProp] = "auto";
356
- slack = 0;
357
- ctx.updateContentSize(sizeCache.getTotalSize());
358
- }
359
- function cleanup() {
360
- if (smoothScrollId !== null) {
361
- cancelAnimationFrame(smoothScrollId);
362
- smoothScrollId = null;
363
- }
364
- if (easedScrollId !== null) {
365
- cancelAnimationFrame(easedScrollId);
366
- easedScrollId = null;
367
- }
368
- if (momentumId !== null) {
369
- cancelAnimationFrame(momentumId);
370
- momentumId = null;
371
- }
372
- if (wheelHandler) {
373
- viewport.removeEventListener("wheel", wheelHandler);
374
- wheelHandler = null;
375
- }
376
- if (touchStartHandler) {
377
- viewport.removeEventListener("touchstart", touchStartHandler);
378
- touchStartHandler = null;
379
- }
380
- if (touchMoveHandler) {
381
- viewport.removeEventListener("touchmove", touchMoveHandler);
382
- touchMoveHandler = null;
383
- }
384
- if (touchEndHandler) {
385
- viewport.removeEventListener("touchend", touchEndHandler);
386
- viewport.removeEventListener("touchcancel", touchEndHandler);
387
- touchEndHandler = null;
388
- }
389
- if (nativeScrollReset) {
390
- viewport.removeEventListener("scroll", nativeScrollReset);
391
- nativeScrollReset = null;
392
- }
393
- if (fallbackScrollbar && ownsScrollbar) {
394
- fallbackScrollbar.destroy();
395
- }
396
- fallbackScrollbar = null;
397
- ownsScrollbar = false;
398
- const scrollProp = horizontal ? "scrollLeft" : "scrollTop";
399
- if (viewport && viewport[scrollProp] !== 0) {
400
- viewport[scrollProp] = 0;
401
- }
402
- }
403
- return {
404
- name: "scale",
405
- priority: 20,
406
- setup(ctx) {
407
- engineState = ctx.getState();
408
- sizeCache = ctx.sizeCache;
409
- viewport = ctx.dom.viewport;
410
- horizontal = ctx.config.horizontal;
411
- overscan = ctx.config.overscan;
412
- storedCtx = ctx;
413
- ctx.registerMethod("_updateCompressionMode", () => updateCompression(ctx));
414
- // Register scroll get/set so scrollToIndex routes through us
415
- ctx.setScrollFns(() => virtualScrollPosition, (actualOffset) => {
416
- if (!compression.isCompressed || !compressedActive) {
417
- const prop = horizontal ? "scrollLeft" : "scrollTop";
418
- viewport[prop] = actualOffset;
419
- return;
420
- }
421
- cancelAnimations();
422
- if (momentumId !== null) {
423
- cancelAnimationFrame(momentumId);
424
- momentumId = null;
425
- }
426
- const virtualPos = actualOffset * compression.ratio;
427
- applyVirtualScroll(Math.max(0, Math.min(virtualPos, getMaxScroll())));
428
- });
429
- ctx.setScrollToIndexFn((index, align, behavior, duration, easing) => {
430
- if (!compression.isCompressed || !compressedActive)
431
- return false;
432
- const virtualPos = calculateCompressedScrollToIndex(index, sizeCache, engineState.containerSize, engineState.totalItems, compression, align);
433
- if (behavior === "smooth") {
434
- easedScrollTo(virtualPos, duration ?? SCROLL_DURATION, easing);
435
- }
436
- else {
437
- setVirtualPosition(virtualPos);
438
- }
439
- });
440
- // Initial compression check
441
- updateCompression(ctx);
442
- lastCheckedTotal = engineState.totalItems;
443
- ctx.registerDestroyHandler(cleanup);
444
- },
445
- hooks: {
446
- onCalculate(state) {
447
- // Re-check compression when item count changes (data mutation)
448
- if (state.totalItems !== lastCheckedTotal && storedCtx) {
449
- lastCheckedTotal = state.totalItems;
450
- updateCompression(storedCtx);
451
- }
452
- if (!compression.isCompressed || !compressedActive)
453
- return;
454
- // Override phase1's buffer contents with compressed positioning.
455
- // Phase1 ran with state.scrollPosition = virtualScrollPosition and
456
- // sizeCache in actual space, so its range calc may be off.
457
- // We recalculate using the compression-aware formula.
458
- const totalItems = state.totalItems;
459
- if (totalItems === 0 || state.containerSize <= 0) {
460
- state.visibleCount = 0;
461
- return;
462
- }
463
- // Calculate compressed visible range
464
- calculateCompressedVisibleRange(state.scrollPosition, state.containerSize, sizeCache, totalItems, compression, compRange);
465
- // Apply overscan
466
- const renderStart = Math.max(0, compRange.start - overscan);
467
- const renderEnd = Math.min(totalItems - 1, compRange.end + overscan);
468
- // Fill buffers with compressed item positions
469
- const count = Math.min(renderEnd - renderStart + 1, state.capacity);
470
- state.visibleCount = count;
471
- state.startIndex = renderStart;
472
- for (let i = 0; i < count; i++) {
473
- const idx = renderStart + i;
474
- state.visibleIndices[i] = idx;
475
- state.visibleOffsets[i] = calculateCompressedItemPosition(idx, state.scrollPosition, sizeCache, totalItems, state.containerSize, compression);
476
- state.visibleSizes[i] = sizeCache.getSize(idx);
477
- }
478
- state.prevRangeStart = renderStart;
479
- state.prevRangeEnd = renderEnd;
480
- },
481
- onAfterScroll(scrollPosition) {
482
- if (fallbackScrollbar && compressedActive) {
483
- fallbackScrollbar.updatePosition(scrollPosition);
484
- fallbackScrollbar.show();
485
- }
486
- },
487
- onResize() {
488
- if (compressedActive) {
489
- slack = computeSlack();
490
- if (fallbackScrollbar) {
491
- fallbackScrollbar.updateBounds(compression.virtualSize + slack, engineState.containerSize);
492
- }
493
- // Clamp virtual scroll to new bounds
494
- const maxScroll = getMaxScroll();
495
- if (virtualScrollPosition > maxScroll) {
496
- virtualScrollPosition = maxScroll;
497
- targetScrollPosition = maxScroll;
498
- engineState.scrollPosition = maxScroll;
499
- }
500
- }
501
- },
502
- },
503
- destroy() {
504
- cleanup();
505
- },
506
- };
507
- }