vlist 2.0.0 → 2.0.2

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 (91) hide show
  1. package/README.github.md +2 -2
  2. package/README.md +2 -2
  3. package/dist/core/dom.d.ts +1 -1
  4. package/dist/core/index.d.ts +1 -1
  5. package/dist/core/pipeline.d.ts +2 -2
  6. package/dist/core/scroll.d.ts +1 -1
  7. package/dist/core/types.d.ts +7 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +1 -28
  10. package/dist/internals.js +1 -60
  11. package/dist/plugins/scrollbar/controller.d.ts +3 -3
  12. package/dist/plugins/scrollbar/scrollbar.d.ts +2 -2
  13. package/dist/rendering/renderer.d.ts +2 -2
  14. package/dist/rendering/viewport.d.ts +1 -1
  15. package/dist/size.json +1 -1
  16. package/dist/types.d.ts +1 -1
  17. package/package.json +1 -1
  18. package/dist/constants.js +0 -83
  19. package/dist/core/create.js +0 -740
  20. package/dist/core/dom.js +0 -47
  21. package/dist/core/hooks.js +0 -67
  22. package/dist/core/index.js +0 -13
  23. package/dist/core/pipeline.js +0 -307
  24. package/dist/core/pool.js +0 -42
  25. package/dist/core/scroll.js +0 -137
  26. package/dist/core/sizes.js +0 -6
  27. package/dist/core/state.js +0 -56
  28. package/dist/core/types.js +0 -7
  29. package/dist/core/velocity.js +0 -33
  30. package/dist/events/emitter.js +0 -60
  31. package/dist/events/index.js +0 -6
  32. package/dist/plugins/a11y/index.js +0 -1
  33. package/dist/plugins/a11y/plugin.js +0 -259
  34. package/dist/plugins/async/index.js +0 -12
  35. package/dist/plugins/async/manager.js +0 -568
  36. package/dist/plugins/async/placeholder.js +0 -154
  37. package/dist/plugins/async/plugin.js +0 -311
  38. package/dist/plugins/async/sparse.js +0 -540
  39. package/dist/plugins/autosize/index.js +0 -4
  40. package/dist/plugins/autosize/plugin.js +0 -185
  41. package/dist/plugins/grid/index.js +0 -5
  42. package/dist/plugins/grid/layout.js +0 -275
  43. package/dist/plugins/grid/plugin.js +0 -347
  44. package/dist/plugins/grid/renderer.js +0 -525
  45. package/dist/plugins/grid/types.js +0 -11
  46. package/dist/plugins/groups/async-bridge.js +0 -246
  47. package/dist/plugins/groups/index.js +0 -13
  48. package/dist/plugins/groups/layout.js +0 -294
  49. package/dist/plugins/groups/plugin.js +0 -571
  50. package/dist/plugins/groups/sticky.js +0 -255
  51. package/dist/plugins/groups/types.js +0 -12
  52. package/dist/plugins/masonry/index.js +0 -6
  53. package/dist/plugins/masonry/layout.js +0 -261
  54. package/dist/plugins/masonry/plugin.js +0 -381
  55. package/dist/plugins/masonry/renderer.js +0 -354
  56. package/dist/plugins/masonry/types.js +0 -9
  57. package/dist/plugins/page/index.js +0 -5
  58. package/dist/plugins/page/plugin.js +0 -166
  59. package/dist/plugins/scale/index.js +0 -4
  60. package/dist/plugins/scale/plugin.js +0 -507
  61. package/dist/plugins/scrollbar/controller.js +0 -574
  62. package/dist/plugins/scrollbar/index.js +0 -6
  63. package/dist/plugins/scrollbar/plugin.js +0 -93
  64. package/dist/plugins/scrollbar/scrollbar.js +0 -556
  65. package/dist/plugins/selection/index.js +0 -7
  66. package/dist/plugins/selection/plugin.js +0 -601
  67. package/dist/plugins/selection/state.js +0 -332
  68. package/dist/plugins/snapshots/index.js +0 -5
  69. package/dist/plugins/snapshots/plugin.js +0 -301
  70. package/dist/plugins/sortable/index.js +0 -6
  71. package/dist/plugins/sortable/plugin.js +0 -753
  72. package/dist/plugins/table/header.js +0 -501
  73. package/dist/plugins/table/index.js +0 -12
  74. package/dist/plugins/table/layout.js +0 -211
  75. package/dist/plugins/table/plugin.js +0 -391
  76. package/dist/plugins/table/renderer.js +0 -625
  77. package/dist/plugins/table/types.js +0 -12
  78. package/dist/plugins/transition/index.js +0 -5
  79. package/dist/plugins/transition/plugin.js +0 -405
  80. package/dist/rendering/aria.js +0 -23
  81. package/dist/rendering/index.js +0 -18
  82. package/dist/rendering/measured.js +0 -98
  83. package/dist/rendering/renderer.js +0 -586
  84. package/dist/rendering/scale.js +0 -267
  85. package/dist/rendering/scroll.js +0 -71
  86. package/dist/rendering/sizes.js +0 -193
  87. package/dist/rendering/sort.js +0 -65
  88. package/dist/rendering/viewport.js +0 -268
  89. package/dist/types.js +0 -5
  90. package/dist/utils/padding.js +0 -49
  91. 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
- }