mtrl-addons 0.1.2 → 0.2.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 (115) hide show
  1. package/build.js +139 -86
  2. package/package.json +13 -4
  3. package/scripts/debug/vlist-selection.ts +121 -0
  4. package/src/components/index.ts +5 -41
  5. package/src/components/{list → vlist}/config.ts +66 -95
  6. package/src/components/vlist/constants.ts +23 -0
  7. package/src/components/vlist/features/api.ts +322 -0
  8. package/src/components/vlist/features/index.ts +10 -0
  9. package/src/components/vlist/features/selection.ts +444 -0
  10. package/src/components/vlist/features/viewport.ts +65 -0
  11. package/src/components/vlist/index.ts +16 -0
  12. package/src/components/{list → vlist}/types.ts +104 -26
  13. package/src/components/vlist/vlist.ts +92 -0
  14. package/src/core/compose/features/gestures/index.ts +227 -0
  15. package/src/core/compose/features/gestures/longpress.ts +383 -0
  16. package/src/core/compose/features/gestures/pan.ts +424 -0
  17. package/src/core/compose/features/gestures/pinch.ts +475 -0
  18. package/src/core/compose/features/gestures/rotate.ts +485 -0
  19. package/src/core/compose/features/gestures/swipe.ts +492 -0
  20. package/src/core/compose/features/gestures/tap.ts +334 -0
  21. package/src/core/compose/features/index.ts +2 -38
  22. package/src/core/compose/index.ts +13 -29
  23. package/src/core/gestures/index.ts +31 -0
  24. package/src/core/gestures/longpress.ts +68 -0
  25. package/src/core/gestures/manager.ts +418 -0
  26. package/src/core/gestures/pan.ts +48 -0
  27. package/src/core/gestures/pinch.ts +58 -0
  28. package/src/core/gestures/rotate.ts +58 -0
  29. package/src/core/gestures/swipe.ts +66 -0
  30. package/src/core/gestures/tap.ts +45 -0
  31. package/src/core/gestures/types.ts +387 -0
  32. package/src/core/gestures/utils.ts +128 -0
  33. package/src/core/index.ts +27 -151
  34. package/src/core/layout/schema.ts +73 -35
  35. package/src/core/layout/types.ts +5 -2
  36. package/src/core/viewport/constants.ts +140 -0
  37. package/src/core/viewport/features/base.ts +73 -0
  38. package/src/core/viewport/features/collection.ts +882 -0
  39. package/src/core/viewport/features/events.ts +130 -0
  40. package/src/core/viewport/features/index.ts +20 -0
  41. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +27 -30
  42. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  43. package/src/core/viewport/features/momentum.ts +260 -0
  44. package/src/core/viewport/features/placeholders.ts +335 -0
  45. package/src/core/viewport/features/rendering.ts +568 -0
  46. package/src/core/viewport/features/scrollbar.ts +434 -0
  47. package/src/core/viewport/features/scrolling.ts +618 -0
  48. package/src/core/viewport/features/utils.ts +88 -0
  49. package/src/core/viewport/features/virtual.ts +384 -0
  50. package/src/core/viewport/index.ts +31 -0
  51. package/src/core/viewport/types.ts +133 -0
  52. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  53. package/src/core/viewport/viewport.ts +246 -0
  54. package/src/index.ts +0 -7
  55. package/src/styles/components/_vlist.scss +331 -0
  56. package/src/styles/index.scss +1 -1
  57. package/test/components/vlist-selection.test.ts +240 -0
  58. package/test/components/vlist.test.ts +63 -0
  59. package/test/core/collection/adapter.test.ts +161 -0
  60. package/bun.lock +0 -792
  61. package/src/components/list/api.ts +0 -314
  62. package/src/components/list/constants.ts +0 -56
  63. package/src/components/list/features/api.ts +0 -428
  64. package/src/components/list/features/index.ts +0 -31
  65. package/src/components/list/features/list-manager.ts +0 -502
  66. package/src/components/list/index.ts +0 -39
  67. package/src/components/list/list.ts +0 -234
  68. package/src/core/collection/base-collection.ts +0 -100
  69. package/src/core/collection/collection-composer.ts +0 -178
  70. package/src/core/collection/collection.ts +0 -745
  71. package/src/core/collection/constants.ts +0 -172
  72. package/src/core/collection/events.ts +0 -428
  73. package/src/core/collection/features/api/loading.ts +0 -279
  74. package/src/core/collection/features/operations/data-operations.ts +0 -147
  75. package/src/core/collection/index.ts +0 -104
  76. package/src/core/collection/state.ts +0 -497
  77. package/src/core/collection/types.ts +0 -404
  78. package/src/core/compose/features/collection.ts +0 -119
  79. package/src/core/compose/features/selection.ts +0 -213
  80. package/src/core/compose/features/styling.ts +0 -108
  81. package/src/core/list-manager/api.ts +0 -599
  82. package/src/core/list-manager/config.ts +0 -593
  83. package/src/core/list-manager/constants.ts +0 -268
  84. package/src/core/list-manager/features/api.ts +0 -58
  85. package/src/core/list-manager/features/collection/collection.ts +0 -705
  86. package/src/core/list-manager/features/collection/index.ts +0 -17
  87. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  88. package/src/core/list-manager/features/viewport/index.ts +0 -16
  89. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  90. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  91. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  92. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  93. package/src/core/list-manager/features/viewport/template.ts +0 -220
  94. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  95. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  96. package/src/core/list-manager/index.ts +0 -279
  97. package/src/core/list-manager/list-manager.ts +0 -206
  98. package/src/core/list-manager/types.ts +0 -439
  99. package/src/core/list-manager/utils/calculations.ts +0 -290
  100. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  101. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  102. package/src/styles/components/_list.scss +0 -244
  103. package/src/types/mtrl.d.ts +0 -6
  104. package/test/components/list.test.ts +0 -256
  105. package/test/core/collection/failed-ranges.test.ts +0 -270
  106. package/test/core/compose/features.test.ts +0 -183
  107. package/test/core/list-manager/features/collection.test.ts +0 -704
  108. package/test/core/list-manager/features/viewport.test.ts +0 -698
  109. package/test/core/list-manager/list-manager.test.ts +0 -593
  110. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  111. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  112. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  113. package/tsconfig.build.json +0 -23
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,575 +0,0 @@
1
- /**
2
- * Rendering - Item rendering and positioning for viewport
3
- *
4
- * This viewport module handles all aspects of rendering items in the virtual viewport,
5
- * including DOM element creation, positioning, recycling, and updates.
6
- */
7
-
8
- import type { ListManagerComponent, ItemRange } from "../../types";
9
- import type { ItemSizeManager } from "./item-size";
10
- import type { VirtualManager } from "./virtual";
11
- import type { ScrollingManager } from "./scrolling";
12
- import { getDefaultTemplate } from "./template";
13
- import { VIEWPORT_CONSTANTS } from "./constants";
14
- import { addClass, removeClass, hasClass } from "mtrl";
15
-
16
- export interface RenderingConfig {
17
- orientation: "vertical" | "horizontal";
18
- overscan: number;
19
- loadDataForRange?: (
20
- range: { start: number; end: number },
21
- priority?: "high" | "normal" | "low"
22
- ) => void;
23
- measureItems?: boolean; // Add measureItems flag
24
- }
25
-
26
- export interface RenderingManager {
27
- renderItems(): void;
28
- updateItemPositions(): void;
29
- getRenderedElements(): Map<number, HTMLElement>;
30
- setItemsContainer(container: HTMLElement): void;
31
- clear(): void;
32
- }
33
-
34
- /**
35
- * Creates a rendering manager for virtual viewport item rendering
36
- */
37
- export const createRenderingManager = (
38
- component: ListManagerComponent,
39
- itemSizeManager: ItemSizeManager,
40
- virtualManager: VirtualManager,
41
- scrollingManager: ScrollingManager,
42
- config: RenderingConfig,
43
- getActualTotalItems: () => number
44
- ): RenderingManager => {
45
- const {
46
- orientation,
47
- overscan,
48
- loadDataForRange,
49
- measureItems = false,
50
- } = config;
51
-
52
- // Items container reference
53
- let itemsContainer: HTMLElement | null = null;
54
-
55
- // Rendered elements cache and virtual range tracking
56
- const renderedElements = new Map<number, HTMLElement>();
57
- let currentVisibleRange: ItemRange = { start: 0, end: 0 };
58
-
59
- /**
60
- * Set the items container element
61
- */
62
- const setItemsContainer = (container: HTMLElement): void => {
63
- itemsContainer = container;
64
- };
65
-
66
- /**
67
- * Clear all rendered elements
68
- */
69
- const clear = (): void => {
70
- renderedElements.clear();
71
- if (itemsContainer) {
72
- itemsContainer.innerHTML = "";
73
- }
74
- };
75
-
76
- /**
77
- * Render items in the viewport
78
- */
79
- const renderItems = (): void => {
80
- if (!itemsContainer) return;
81
-
82
- const newVisibleRange = virtualManager.calculateVisibleRange(
83
- scrollingManager.getScrollPosition()
84
- );
85
-
86
- // Validate range
87
- const actualTotalItems = getActualTotalItems();
88
- if (
89
- newVisibleRange.start < 0 ||
90
- newVisibleRange.start >= actualTotalItems ||
91
- newVisibleRange.end < newVisibleRange.start
92
- ) {
93
- return;
94
- }
95
-
96
- // Check if range changed
97
- const rangeChanged =
98
- newVisibleRange.start !== currentVisibleRange.start ||
99
- newVisibleRange.end !== currentVisibleRange.end;
100
-
101
- // Check if component has placeholders API
102
- const hasPlaceholders = !!(component as any).placeholders;
103
- const placeholdersAPI = hasPlaceholders
104
- ? (component as any).placeholders
105
- : null;
106
-
107
- // Check if we need to replace any placeholders with real data
108
- let needsPlaceholderReplacement = false;
109
-
110
- if (!rangeChanged && renderedElements.size > 0 && hasPlaceholders) {
111
- // Check if any rendered elements are placeholders but we now have real data
112
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
113
- const item = i < component.items.length ? component.items[i] : null;
114
- const element = renderedElements.get(i);
115
-
116
- if (item && element) {
117
- const isCurrentItemPlaceholder = placeholdersAPI.isPlaceholder(item);
118
- const elementIsPlaceholder = hasClass(
119
- element,
120
- VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS
121
- );
122
-
123
- if (elementIsPlaceholder && !isCurrentItemPlaceholder) {
124
- needsPlaceholderReplacement = true;
125
- break;
126
- }
127
- }
128
- }
129
- }
130
-
131
- if (
132
- !rangeChanged &&
133
- renderedElements.size > 0 &&
134
- !needsPlaceholderReplacement
135
- ) {
136
- // Only skip rendering if we already have items rendered and no placeholders need replacement
137
- updateItemPositions();
138
- return;
139
- }
140
-
141
- // Check for missing data and show placeholders if needed
142
- if (hasPlaceholders && placeholdersAPI.isEnabled()) {
143
- const missingIndices: number[] = [];
144
-
145
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
146
- if (i >= actualTotalItems) break;
147
-
148
- const itemExists =
149
- i < component.items.length &&
150
- component.items[i] !== null &&
151
- component.items[i] !== undefined &&
152
- !placeholdersAPI.isPlaceholder(component.items[i]);
153
-
154
- if (!itemExists) {
155
- missingIndices.push(i);
156
- }
157
- }
158
-
159
- // Show placeholders for missing items
160
- if (missingIndices.length > 0) {
161
- const placeholderRange = {
162
- start: Math.min(...missingIndices),
163
- end: Math.max(...missingIndices),
164
- };
165
- placeholdersAPI.showPlaceholders(placeholderRange);
166
- }
167
- }
168
-
169
- // Proactively load data for visible range AND upcoming ranges
170
- // console.log(
171
- // `🔍 [VIEWPORT] Checking range ${newVisibleRange.start}-${newVisibleRange.end} for missing data`
172
- // );
173
-
174
- const collection = (component as any).collection;
175
- const hasCollection = !!collection;
176
- const hasLoadMissingRanges =
177
- hasCollection && typeof collection.loadMissingRanges === "function";
178
-
179
- // Calculate extended range for proactive loading
180
- const itemsPerViewport = Math.ceil(
181
- virtualManager.getState().containerSize /
182
- itemSizeManager.getEstimatedItemSize()
183
- );
184
-
185
- // DISABLED PREFETCH: Set to 0 to avoid loading wrong ranges
186
- // TODO: Fix the range calculation before re-enabling prefetch
187
- const prefetchBuffer = 0; // Was: Math.ceil(itemsPerViewport * 2);
188
- const extendedRange = {
189
- start: Math.max(0, newVisibleRange.start - prefetchBuffer),
190
- end: Math.min(actualTotalItems - 1, newVisibleRange.end + prefetchBuffer),
191
- };
192
-
193
- // Check for missing data in visible and extended ranges
194
- if (hasLoadMissingRanges) {
195
- // Count missing items in visible range
196
- let visibleMissingCount = 0;
197
- const visibleMissingIndices: number[] = [];
198
-
199
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
200
- // Skip if index is beyond actual total items
201
- if (i >= actualTotalItems) {
202
- break;
203
- }
204
-
205
- // Check if item is missing (null, undefined, or array doesn't extend to this index)
206
- const itemExists =
207
- i < component.items.length &&
208
- component.items[i] !== null &&
209
- component.items[i] !== undefined;
210
-
211
- // Also check if the item is a placeholder (which means real data is missing)
212
- const isItemPlaceholder =
213
- itemExists &&
214
- hasPlaceholders &&
215
- placeholdersAPI.isPlaceholder(component.items[i]);
216
-
217
- if (!itemExists || isItemPlaceholder) {
218
- visibleMissingCount++;
219
- visibleMissingIndices.push(i);
220
- }
221
- }
222
-
223
- // Count missing items in extended range (excluding visible range)
224
- let extendedMissingCount = 0;
225
- const extendedMissingIndices: number[] = [];
226
-
227
- for (let i = extendedRange.start; i <= extendedRange.end; i++) {
228
- // Skip visible range (already counted)
229
- if (i >= newVisibleRange.start && i <= newVisibleRange.end) {
230
- continue;
231
- }
232
-
233
- // Skip if index is beyond actual total items
234
- if (i >= actualTotalItems) {
235
- break;
236
- }
237
-
238
- // Check if item is missing
239
- const itemExists =
240
- i < component.items.length &&
241
- component.items[i] !== null &&
242
- component.items[i] !== undefined;
243
-
244
- // Also check if the item is a placeholder (which means real data is missing)
245
- const isItemPlaceholder =
246
- itemExists &&
247
- hasPlaceholders &&
248
- placeholdersAPI.isPlaceholder(component.items[i]);
249
-
250
- if (!itemExists || isItemPlaceholder) {
251
- extendedMissingCount++;
252
- extendedMissingIndices.push(i);
253
- }
254
- }
255
-
256
- // Load visible range with high priority
257
- if (visibleMissingCount > 0) {
258
- // Use loading manager for all loads to benefit from request queue
259
- if (loadDataForRange) {
260
- loadDataForRange(newVisibleRange, "high");
261
- } else {
262
- // Fallback to direct collection call only if no loading manager
263
- collection.loadMissingRanges(newVisibleRange).catch((error: any) => {
264
- console.error(
265
- "❌ [VIEWPORT] Failed to load visible ranges:",
266
- error
267
- );
268
- });
269
- }
270
- }
271
-
272
- // Load extended range with lower priority (proactive)
273
- // Skip if prefetch is disabled (prefetchBuffer === 0)
274
- if (extendedMissingCount > 0 && prefetchBuffer > 0) {
275
- // Use loading manager for proactive loads
276
- if (loadDataForRange) {
277
- loadDataForRange(extendedRange, "low");
278
- } else {
279
- // Fallback to direct collection call
280
- collection.loadMissingRanges(extendedRange).catch((error: any) => {
281
- console.error(
282
- "❌ [VIEWPORT] Failed to load extended range:",
283
- error
284
- );
285
- });
286
- }
287
- }
288
- }
289
-
290
- // Recycle out-of-range items
291
- const buffer = overscan; // Reduced from overscan * 2 for fewer DOM elements
292
- const recycleStart = newVisibleRange.start - buffer;
293
- const recycleEnd = newVisibleRange.end + buffer;
294
-
295
- for (const [index, element] of renderedElements) {
296
- if (index < recycleStart || index > recycleEnd) {
297
- // Remove from DOM
298
- element.remove();
299
- renderedElements.delete(index);
300
- }
301
- }
302
-
303
- // Render new items
304
- const newElements: { element: HTMLElement; index: number }[] = [];
305
-
306
- for (let i = newVisibleRange.start; i <= newVisibleRange.end; i++) {
307
- if (i >= component.items.length) {
308
- break;
309
- }
310
-
311
- const item = component.items[i];
312
-
313
- if (!item) {
314
- continue; // Skip empty slots
315
- }
316
-
317
- // Check if already rendered
318
- const existingElement = renderedElements.get(i);
319
- if (existingElement) {
320
- // Check if the rendered element is a placeholder but we now have real data
321
- const isCurrentItemPlaceholder =
322
- hasPlaceholders && placeholdersAPI.isPlaceholder(item);
323
- const elementIsPlaceholder = hasClass(
324
- existingElement,
325
- VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS
326
- );
327
-
328
- if (elementIsPlaceholder && !isCurrentItemPlaceholder) {
329
- // We have real data now, remove the placeholder element and re-render
330
- existingElement.remove();
331
- renderedElements.delete(i);
332
- // Continue to render the real item below
333
- } else if (!elementIsPlaceholder && isCurrentItemPlaceholder) {
334
- // We have a real element but now need a placeholder (shouldn't happen often)
335
- existingElement.remove();
336
- renderedElements.delete(i);
337
- // Continue to render the placeholder below
338
- } else {
339
- continue;
340
- }
341
- }
342
-
343
- // Create element
344
- const element = renderItem(item, i);
345
- if (element) {
346
- itemsContainer.appendChild(element);
347
- renderedElements.set(i, element);
348
- newElements.push({ element, index: i });
349
- }
350
- }
351
-
352
- // Update visible range
353
- currentVisibleRange = newVisibleRange;
354
-
355
- // Emit event
356
- component.emit?.("range:rendered", {
357
- range: newVisibleRange,
358
- renderedCount: renderedElements.size,
359
- });
360
-
361
- // Batch measure new elements for performance
362
- if (newElements.length > 0) {
363
- // Use requestAnimationFrame to measure after browser layout
364
- requestAnimationFrame(() => {
365
- newElements.forEach(({ element, index }) => {
366
- if (measureItems) {
367
- itemSizeManager.measureItem(element, index, orientation);
368
- }
369
- });
370
-
371
- // Update positions after measuring
372
- updateItemPositions();
373
- });
374
- } else {
375
- // Update positions immediately if no new elements
376
- updateItemPositions();
377
- }
378
- };
379
-
380
- /**
381
- * Render a single item element
382
- */
383
- const renderItem = (item: any, index: number): HTMLElement | null => {
384
- let template = component.template;
385
-
386
- // Use default template if none provided
387
- if (!template) {
388
- template = getDefaultTemplate();
389
- }
390
-
391
- try {
392
- let element: HTMLElement;
393
-
394
- // Always create fresh element from template
395
- const result = template(item, index);
396
- if (typeof result === "string") {
397
- const wrapper = document.createElement("div");
398
- wrapper.innerHTML = result;
399
- element = wrapper.firstElementChild as HTMLElement;
400
- } else if (result instanceof HTMLElement) {
401
- element = result;
402
- } else {
403
- return null;
404
- }
405
-
406
- // Check if this is a placeholder and add the CSS class
407
- const hasPlaceholders = !!(component as any).placeholders;
408
- if (hasPlaceholders) {
409
- const placeholdersAPI = (component as any).placeholders;
410
- if (placeholdersAPI.isPlaceholder(item)) {
411
- addClass(element, VIEWPORT_CONSTANTS.PLACEHOLDER.CSS_CLASS);
412
- }
413
- }
414
-
415
- // Apply base styles for virtual positioning
416
- element.style.position = "absolute";
417
- element.style.left = "0";
418
- element.style.right = "0"; // For vertical, full width
419
- element.style.boxSizing = "border-box";
420
- element.style.willChange = "transform";
421
- element.style.visibility = "visible"; // Make visible immediately
422
-
423
- if (orientation === "horizontal") {
424
- element.style.top = "0";
425
- element.style.bottom = "0"; // Full height for horizontal
426
- element.style.width = `${itemSizeManager.getEstimatedItemSize()}px`; // Initial width
427
- } else {
428
- element.style.height = `${itemSizeManager.getEstimatedItemSize()}px`; // Initial height
429
- element.style.width = "100%";
430
- }
431
-
432
- return element;
433
- } catch (error) {
434
- console.error(`Error rendering item at index ${index}:`, error);
435
- return null;
436
- }
437
- };
438
-
439
- /**
440
- * Update positions of rendered items
441
- */
442
- const updateItemPositions = () => {
443
- if (!itemsContainer || renderedElements.size === 0) return;
444
-
445
- const isHorizontal = orientation === "horizontal";
446
- const axis = isHorizontal ? "X" : "Y";
447
-
448
- // Get the current scroll position and visible range
449
- const scrollPosition = scrollingManager.getScrollPosition();
450
- const visibleRange = virtualManager.calculateVisibleRange(scrollPosition);
451
-
452
- // Get basic measurements
453
- const totalItems = getActualTotalItems();
454
- const itemSize = itemSizeManager.getEstimatedItemSize();
455
- const virtualTotalSize = virtualManager.getTotalVirtualSize();
456
- const containerSize = virtualManager.getState().containerSize;
457
-
458
- // Get all indices in sorted order
459
- const sortedIndices = Array.from(renderedElements.keys()).sort(
460
- (a, b) => a - b
461
- );
462
-
463
- if (sortedIndices.length === 0) return;
464
-
465
- // Calculate positioning using unified index-based approach
466
- let currentPosition = 0;
467
- const firstRenderedIndex = sortedIndices[0];
468
- const lastRenderedIndex = sortedIndices[sortedIndices.length - 1];
469
-
470
- // Check if we're using compressed virtual space
471
- const actualTotalSize = totalItems * itemSize;
472
- const isCompressed = actualTotalSize > virtualTotalSize;
473
-
474
- if (isCompressed) {
475
- // When using compressed space, we need special handling near the bottom
476
- const maxScrollPosition = virtualTotalSize - containerSize;
477
- const distanceFromBottom = maxScrollPosition - scrollPosition;
478
- const nearBottomThreshold = containerSize; // Within one viewport height
479
-
480
- if (
481
- distanceFromBottom <= nearBottomThreshold &&
482
- distanceFromBottom >= -1
483
- ) {
484
- // Near or at the bottom - use interpolation for smooth transition
485
- const itemsThatFitCompletely = Math.floor(containerSize / itemSize);
486
- const firstVisibleAtBottom = Math.max(
487
- 0,
488
- totalItems - itemsThatFitCompletely
489
- );
490
-
491
- // Calculate normal scroll position
492
- const scrollRatio = scrollPosition / virtualTotalSize;
493
- const exactScrollIndex = scrollRatio * totalItems;
494
-
495
- // Interpolation factor: 0 when far from bottom, 1 when at bottom
496
- const interpolationFactor = Math.max(
497
- 0,
498
- Math.min(1, 1 - distanceFromBottom / nearBottomThreshold)
499
- );
500
-
501
- // For the first rendered item, interpolate between normal and bottom positions
502
- const bottomPosition =
503
- (firstRenderedIndex - firstVisibleAtBottom) * itemSize;
504
- const normalPosition =
505
- (firstRenderedIndex - exactScrollIndex) * itemSize;
506
-
507
- // Interpolate between the two positions
508
- currentPosition =
509
- normalPosition +
510
- (bottomPosition - normalPosition) * interpolationFactor;
511
- } else {
512
- // For normal scrolling in compressed space
513
- const scrollRatio = scrollPosition / virtualTotalSize;
514
- const exactScrollIndex = scrollRatio * totalItems;
515
-
516
- // Calculate offset from the exact scroll position
517
- const offset = firstRenderedIndex - exactScrollIndex;
518
- currentPosition = offset * itemSize;
519
- }
520
- } else {
521
- // When not compressed (actual size <= 10M), use direct positioning
522
- // This gives us natural 1:1 scrolling
523
- const firstItemPosition = firstRenderedIndex * itemSize;
524
- currentPosition = firstItemPosition - scrollPosition;
525
- }
526
-
527
- // Position each rendered item
528
- sortedIndices.forEach((index) => {
529
- const element = renderedElements.get(index);
530
- if (!element) return;
531
-
532
- // Get measured size for this item
533
- const size =
534
- itemSizeManager.getMeasuredSize(index) ||
535
- itemSizeManager.getEstimatedItemSize();
536
-
537
- // Position item relative to container
538
- element.style.position = "absolute";
539
- element.style.transform = `translate${axis}(${currentPosition}px)`;
540
- element.style.visibility = "visible";
541
-
542
- // Update dimensions
543
- if (isHorizontal) {
544
- element.style.width = `${size}px`;
545
- element.style.height = "100%";
546
- element.style.top = "0";
547
- } else {
548
- element.style.height = `${size}px`;
549
- element.style.width = "100%";
550
- element.style.left = "0";
551
- }
552
-
553
- // Move to next position
554
- currentPosition += size;
555
- });
556
-
557
- // Don't set container size to virtual size - keep it minimal
558
- // The scrollbar will handle the virtual size representation
559
- if (isHorizontal) {
560
- itemsContainer.style.width = "100%";
561
- itemsContainer.style.height = "100%";
562
- } else {
563
- itemsContainer.style.height = "100%";
564
- itemsContainer.style.width = "100%";
565
- }
566
- };
567
-
568
- return {
569
- renderItems,
570
- updateItemPositions,
571
- getRenderedElements: () => new Map(renderedElements),
572
- setItemsContainer,
573
- clear,
574
- };
575
- };