mtrl-addons 0.2.1 → 0.2.3

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 (128) hide show
  1. package/{src/components/index.ts → dist/components/index.d.ts} +0 -2
  2. package/dist/components/vlist/config.d.ts +86 -0
  3. package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
  4. package/dist/components/vlist/features/api.d.ts +7 -0
  5. package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
  6. package/dist/components/vlist/features/selection.d.ts +6 -0
  7. package/dist/components/vlist/features/viewport.d.ts +9 -0
  8. package/dist/components/vlist/features.d.ts +31 -0
  9. package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -9
  10. package/dist/components/vlist/types.d.ts +596 -0
  11. package/dist/components/vlist/vlist.d.ts +29 -0
  12. package/dist/core/compose/features/gestures/index.d.ts +86 -0
  13. package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
  14. package/dist/core/compose/features/gestures/pan.d.ts +108 -0
  15. package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
  16. package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
  17. package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
  18. package/dist/core/compose/features/gestures/tap.d.ts +79 -0
  19. package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
  20. package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
  21. package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
  22. package/dist/core/gestures/longpress.d.ts +23 -0
  23. package/dist/core/gestures/manager.d.ts +14 -0
  24. package/dist/core/gestures/pan.d.ts +12 -0
  25. package/dist/core/gestures/pinch.d.ts +14 -0
  26. package/dist/core/gestures/rotate.d.ts +14 -0
  27. package/dist/core/gestures/swipe.d.ts +20 -0
  28. package/dist/core/gestures/tap.d.ts +12 -0
  29. package/dist/core/gestures/types.d.ts +320 -0
  30. package/dist/core/gestures/utils.d.ts +57 -0
  31. package/dist/core/index.d.ts +13 -0
  32. package/dist/core/layout/config.d.ts +33 -0
  33. package/dist/core/layout/index.d.ts +51 -0
  34. package/dist/core/layout/jsx.d.ts +65 -0
  35. package/dist/core/layout/schema.d.ts +112 -0
  36. package/dist/core/layout/types.d.ts +69 -0
  37. package/dist/core/viewport/constants.d.ts +105 -0
  38. package/dist/core/viewport/features/base.d.ts +14 -0
  39. package/dist/core/viewport/features/collection.d.ts +41 -0
  40. package/dist/core/viewport/features/events.d.ts +13 -0
  41. package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
  42. package/dist/core/viewport/features/item-size.d.ts +30 -0
  43. package/dist/core/viewport/features/loading.d.ts +34 -0
  44. package/dist/core/viewport/features/momentum.d.ts +17 -0
  45. package/dist/core/viewport/features/performance.d.ts +53 -0
  46. package/dist/core/viewport/features/placeholders.d.ts +38 -0
  47. package/dist/core/viewport/features/rendering.d.ts +16 -0
  48. package/dist/core/viewport/features/scrollbar.d.ts +26 -0
  49. package/dist/core/viewport/features/scrolling.d.ts +16 -0
  50. package/dist/core/viewport/features/utils.d.ts +43 -0
  51. package/dist/core/viewport/features/virtual.d.ts +18 -0
  52. package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
  53. package/dist/core/viewport/types.d.ts +96 -0
  54. package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
  55. package/dist/core/viewport/viewport.d.ts +11 -0
  56. package/{src/index.ts → dist/index.d.ts} +0 -4
  57. package/dist/index.js +5143 -0
  58. package/dist/index.mjs +5111 -0
  59. package/dist/styles.css +254 -0
  60. package/dist/styles.css.map +1 -0
  61. package/package.json +6 -1
  62. package/src/styles/components/_vlist.scss +234 -213
  63. package/.cursorrules +0 -117
  64. package/AI.md +0 -241
  65. package/build.js +0 -201
  66. package/scripts/analyze-orphaned-functions.ts +0 -387
  67. package/scripts/debug/vlist-selection.ts +0 -121
  68. package/src/components/vlist/config.ts +0 -323
  69. package/src/components/vlist/features/api.ts +0 -322
  70. package/src/components/vlist/features/selection.ts +0 -444
  71. package/src/components/vlist/features/viewport.ts +0 -65
  72. package/src/components/vlist/features.ts +0 -112
  73. package/src/components/vlist/types.ts +0 -591
  74. package/src/components/vlist/vlist.ts +0 -92
  75. package/src/core/compose/features/gestures/index.ts +0 -227
  76. package/src/core/compose/features/gestures/longpress.ts +0 -383
  77. package/src/core/compose/features/gestures/pan.ts +0 -424
  78. package/src/core/compose/features/gestures/pinch.ts +0 -475
  79. package/src/core/compose/features/gestures/rotate.ts +0 -485
  80. package/src/core/compose/features/gestures/swipe.ts +0 -492
  81. package/src/core/compose/features/gestures/tap.ts +0 -334
  82. package/src/core/gestures/longpress.ts +0 -68
  83. package/src/core/gestures/manager.ts +0 -418
  84. package/src/core/gestures/pan.ts +0 -48
  85. package/src/core/gestures/pinch.ts +0 -58
  86. package/src/core/gestures/rotate.ts +0 -58
  87. package/src/core/gestures/swipe.ts +0 -66
  88. package/src/core/gestures/tap.ts +0 -45
  89. package/src/core/gestures/types.ts +0 -387
  90. package/src/core/gestures/utils.ts +0 -128
  91. package/src/core/index.ts +0 -43
  92. package/src/core/layout/config.ts +0 -102
  93. package/src/core/layout/index.ts +0 -168
  94. package/src/core/layout/jsx.ts +0 -174
  95. package/src/core/layout/schema.ts +0 -1001
  96. package/src/core/layout/types.ts +0 -95
  97. package/src/core/viewport/constants.ts +0 -140
  98. package/src/core/viewport/features/base.ts +0 -73
  99. package/src/core/viewport/features/collection.ts +0 -882
  100. package/src/core/viewport/features/events.ts +0 -130
  101. package/src/core/viewport/features/item-size.ts +0 -271
  102. package/src/core/viewport/features/loading.ts +0 -263
  103. package/src/core/viewport/features/momentum.ts +0 -260
  104. package/src/core/viewport/features/performance.ts +0 -161
  105. package/src/core/viewport/features/placeholders.ts +0 -335
  106. package/src/core/viewport/features/rendering.ts +0 -568
  107. package/src/core/viewport/features/scrollbar.ts +0 -434
  108. package/src/core/viewport/features/scrolling.ts +0 -618
  109. package/src/core/viewport/features/utils.ts +0 -88
  110. package/src/core/viewport/features/virtual.ts +0 -384
  111. package/src/core/viewport/types.ts +0 -133
  112. package/src/core/viewport/utils/speed-tracker.ts +0 -79
  113. package/src/core/viewport/viewport.ts +0 -246
  114. package/test/benchmarks/layout/advanced.test.ts +0 -656
  115. package/test/benchmarks/layout/comparison.test.ts +0 -519
  116. package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
  117. package/test/benchmarks/layout/real-components.test.ts +0 -733
  118. package/test/benchmarks/layout/simple.test.ts +0 -321
  119. package/test/benchmarks/layout/stress.test.ts +0 -990
  120. package/test/collection/basic.test.ts +0 -304
  121. package/test/components/vlist-selection.test.ts +0 -240
  122. package/test/components/vlist.test.ts +0 -63
  123. package/test/core/collection/adapter.test.ts +0 -161
  124. package/test/core/collection/collection.test.ts +0 -394
  125. package/test/core/layout/layout.test.ts +0 -201
  126. package/test/utils/dom-helpers.ts +0 -275
  127. package/test/utils/performance-helpers.ts +0 -392
  128. package/tsconfig.json +0 -20
@@ -1,618 +0,0 @@
1
- /**
2
- * Scrolling Feature - Virtual scrolling with integrated velocity tracking
3
- * Handles wheel events, touch/mouse events, scroll position management, velocity measurement, and momentum scrolling
4
- */
5
-
6
- import type { ViewportContext, ViewportComponent } from "../types";
7
- import { VIEWPORT_CONSTANTS } from "../constants";
8
- import { wrapInitialize, getViewportState, clamp } from "./utils";
9
-
10
- export interface ScrollingConfig {
11
- orientation?: "vertical" | "horizontal";
12
- sensitivity?: number;
13
- smoothing?: boolean;
14
- idleTimeout?: number;
15
- }
16
-
17
- // Speed tracker interface
18
- interface SpeedTracker {
19
- velocity: number;
20
- lastPosition: number;
21
- lastTime: number;
22
- direction: "forward" | "backward";
23
- samples: Array<{ position: number; time: number }>;
24
- }
25
-
26
- /**
27
- * Creates a new speed tracker
28
- */
29
- const createSpeedTracker = (): SpeedTracker => ({
30
- velocity: 0,
31
- lastPosition: 0,
32
- lastTime: Date.now(),
33
- direction: "forward",
34
- samples: [],
35
- });
36
-
37
- /**
38
- * Updates speed tracker with new position
39
- */
40
- const updateSpeedTracker = (
41
- tracker: SpeedTracker,
42
- newPosition: number,
43
- previousPosition: number
44
- ): SpeedTracker => {
45
- const now = Date.now();
46
- const timeDelta = now - tracker.lastTime;
47
-
48
- if (timeDelta === 0) return tracker;
49
-
50
- const positionDelta = newPosition - previousPosition;
51
- const instantVelocity = Math.abs(positionDelta) / timeDelta;
52
-
53
- // Add new sample
54
- const samples = [...tracker.samples, { position: newPosition, time: now }];
55
-
56
- // Keep only recent samples (last 100ms)
57
- const recentSamples = samples.filter((s) => now - s.time < 100);
58
-
59
- // Calculate average velocity from recent samples
60
- let avgVelocity = instantVelocity;
61
- if (recentSamples.length > 1) {
62
- const oldestSample = recentSamples[0];
63
- const totalDistance = Math.abs(newPosition - oldestSample.position);
64
- const totalTime = now - oldestSample.time;
65
- avgVelocity = totalTime > 0 ? totalDistance / totalTime : instantVelocity;
66
- }
67
-
68
- return {
69
- velocity: avgVelocity,
70
- lastPosition: newPosition,
71
- lastTime: now,
72
- direction: positionDelta >= 0 ? "forward" : "backward",
73
- samples: recentSamples,
74
- };
75
- };
76
-
77
- /**
78
- * Scrolling feature for viewport
79
- * Handles wheel events, velocity tracking, and idle detection
80
- */
81
- export const withScrolling = (config: ScrollingConfig = {}) => {
82
- return <T extends ViewportContext & ViewportComponent>(component: T): T => {
83
- const {
84
- orientation = "vertical",
85
- sensitivity = VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.SCROLL_SENSITIVITY,
86
- smoothing = false,
87
- idleTimeout = 100, // Default idle timeout in ms
88
- } = config;
89
-
90
- // State
91
- let scrollPosition = 0;
92
- let totalVirtualSize = 0;
93
- let containerSize = 0;
94
- let isScrolling = false;
95
- let lastScrollTime = 0;
96
- let speedTracker = createSpeedTracker();
97
- let idleTimeoutId: number | null = null;
98
- let idleCheckFrame: number | null = null;
99
- let lastIdleCheckPosition = 0;
100
- let hasEmittedIdle = false; // Track if we've already emitted idle
101
-
102
- // console.log(`[Scrolling] Initial state - position: ${scrollPosition}`);
103
-
104
- // Get viewport state
105
- let viewportState: any;
106
-
107
- // Use shared initialization wrapper
108
- wrapInitialize(component, () => {
109
- viewportState = getViewportState(component);
110
-
111
- // Initialize state values
112
- if (viewportState) {
113
- totalVirtualSize = viewportState.virtualTotalSize || 0;
114
- containerSize = viewportState.containerSize || 0;
115
- // console.log(
116
- // `[Scrolling] Initialized with totalVirtualSize: ${totalVirtualSize}, containerSize: ${containerSize}`
117
- // );
118
- }
119
-
120
- // Listen for virtual size changes
121
- component.on?.("viewport:virtual-size-changed", (data: any) => {
122
- // console.log("[Scrolling] Virtual size changed:", data);
123
- updateScrollBounds(data.totalVirtualSize, containerSize);
124
- });
125
-
126
- // Listen for container size changes
127
- component.on?.("viewport:container-size-changed", (data: any) => {
128
- if (data.containerSize) {
129
- containerSize = data.containerSize;
130
- updateScrollBounds(totalVirtualSize, containerSize);
131
- }
132
- });
133
-
134
- // Attach wheel event listener to viewport element
135
- const viewportElement =
136
- viewportState?.viewportElement || (component as any).viewportElement;
137
- if (viewportElement) {
138
- // console.log(`[Scrolling] Attaching wheel event to viewport element`);
139
- viewportElement.addEventListener("wheel", handleWheel, {
140
- passive: false,
141
- });
142
-
143
- // Store reference for cleanup
144
- (component as any)._scrollingViewportElement = viewportElement;
145
- } else {
146
- console.warn(`[Scrolling] No viewport element found for wheel events`);
147
- }
148
- });
149
-
150
- // Use clamp from utils
151
-
152
- // Start idle detection
153
- const startIdleDetection = () => {
154
- // console.log("[Scrolling] Starting idle detection");
155
-
156
- // Stop any existing idle detection first
157
- if (idleCheckFrame !== null) {
158
- cancelAnimationFrame(idleCheckFrame);
159
- idleCheckFrame = null;
160
- }
161
-
162
- hasEmittedIdle = false; // Reset idle emission flag
163
- const checkIdle = () => {
164
- if (scrollPosition === lastIdleCheckPosition) {
165
- // Position hasn't changed - we're idle
166
- if (!hasEmittedIdle && (speedTracker.velocity > 0 || isScrolling)) {
167
- // console.log(
168
- // "[Scrolling] Idle detected - position stable, setting velocity to zero"
169
- // );
170
- hasEmittedIdle = true; // Mark that we've emitted idle
171
- setVelocityToZero();
172
- }
173
- } else {
174
- // Position changed, reset the flag
175
- hasEmittedIdle = false;
176
- }
177
- lastIdleCheckPosition = scrollPosition;
178
- idleCheckFrame = requestAnimationFrame(checkIdle);
179
- };
180
- idleCheckFrame = requestAnimationFrame(checkIdle);
181
- };
182
-
183
- // Stop idle detection
184
- const stopIdleDetection = () => {
185
- if (idleCheckFrame) {
186
- // console.log("[Scrolling] Stopping idle detection");
187
- cancelAnimationFrame(idleCheckFrame);
188
- idleCheckFrame = null;
189
- }
190
- };
191
-
192
- // Set velocity to zero and emit idle event
193
- const setVelocityToZero = () => {
194
- // console.log("[Scrolling] Setting velocity to zero and emitting idle");
195
-
196
- // Stop idle detection since we're now idle
197
- stopIdleDetection();
198
-
199
- speedTracker = createSpeedTracker();
200
- isScrolling = false; // Reset scrolling state
201
-
202
- // Emit velocity change
203
- component.emit?.("viewport:velocity-changed", {
204
- velocity: 0,
205
- direction: speedTracker.direction,
206
- });
207
-
208
- // Emit idle state
209
- component.emit?.("viewport:idle", {
210
- position: scrollPosition,
211
- lastScrollTime,
212
- });
213
- };
214
- // Update container position
215
- const updateContainerPosition = () => {
216
- if (!viewportState || !viewportState.itemsContainer) return;
217
-
218
- // Items container doesn't move - items inside it are positioned
219
- // This is handled by the rendering feature
220
- };
221
-
222
- // Handle wheel event
223
- const handleWheel = (event: WheelEvent) => {
224
- event.preventDefault();
225
-
226
- const delta = orientation === "vertical" ? event.deltaY : event.deltaX;
227
- const scrollDelta = delta * sensitivity;
228
-
229
- const previousPosition = scrollPosition;
230
- const maxScroll = Math.max(0, totalVirtualSize - containerSize);
231
-
232
- let newPosition = scrollPosition + scrollDelta;
233
-
234
- // Apply smoothing if enabled
235
- if (smoothing) {
236
- const smoothingFactor = 0.3;
237
- newPosition = scrollPosition + scrollDelta * smoothingFactor;
238
- }
239
-
240
- newPosition = clamp(newPosition, 0, maxScroll);
241
-
242
- // console.log(
243
- // `[Scrolling] Wheel: delta=${delta}, scrollDelta=${scrollDelta}, pos=${scrollPosition} -> ${newPosition}, max=${maxScroll}`
244
- // );
245
-
246
- if (newPosition !== scrollPosition) {
247
- scrollPosition = newPosition;
248
- const now = Date.now();
249
-
250
- // Update scroll state
251
- if (!isScrolling) {
252
- isScrolling = true;
253
- // Stop any existing idle detection before starting new one
254
- stopIdleDetection();
255
- startIdleDetection();
256
- }
257
- lastScrollTime = now;
258
-
259
- // Update speed tracker
260
- speedTracker = updateSpeedTracker(
261
- speedTracker,
262
- scrollPosition,
263
- previousPosition
264
- );
265
-
266
- // Update viewport state
267
- if (viewportState) {
268
- viewportState.scrollPosition = scrollPosition;
269
- viewportState.velocity = speedTracker.velocity;
270
- viewportState.scrollDirection = speedTracker.direction;
271
- }
272
-
273
- // Emit events
274
- component.emit?.("viewport:scroll", {
275
- position: scrollPosition,
276
- direction: speedTracker.direction,
277
- previousPosition,
278
- });
279
-
280
- component.emit?.("viewport:velocity-changed", {
281
- velocity: speedTracker.velocity,
282
- direction: speedTracker.direction,
283
- });
284
-
285
- // Trigger render
286
- component.viewport.renderItems();
287
- }
288
- };
289
-
290
- // Scroll to position
291
- const scrollToPosition = (position: number, source?: string) => {
292
- const maxScroll = Math.max(0, totalVirtualSize - containerSize);
293
- const clampedPosition = clamp(position, 0, maxScroll);
294
-
295
- // console.log(
296
- // `[Scrolling] scrollToPosition: pos=${position} -> ${clampedPosition}, source=${source}, currentPos=${scrollPosition}, velocity=${speedTracker.velocity.toFixed(
297
- // 3
298
- // )}`
299
- // );
300
-
301
- if (clampedPosition !== scrollPosition) {
302
- const previousPosition = scrollPosition;
303
- scrollPosition = clampedPosition;
304
-
305
- // Update speed tracker to calculate velocity
306
- speedTracker = updateSpeedTracker(
307
- speedTracker,
308
- scrollPosition,
309
- previousPosition
310
- );
311
-
312
- // Update viewport state
313
- if (viewportState) {
314
- viewportState.scrollPosition = scrollPosition;
315
- viewportState.velocity = speedTracker.velocity;
316
- viewportState.scrollDirection = speedTracker.direction;
317
- }
318
-
319
- const direction =
320
- clampedPosition > previousPosition ? "forward" : "backward";
321
-
322
- component.emit?.("viewport:scroll", {
323
- position: scrollPosition,
324
- direction,
325
- previousPosition,
326
- source,
327
- });
328
-
329
- // Emit velocity change event so collection can track it
330
- component.emit?.("viewport:velocity-changed", {
331
- velocity: speedTracker.velocity,
332
- direction: speedTracker.direction,
333
- });
334
-
335
- // Update scroll state and idle detection
336
- if (!isScrolling) {
337
- isScrolling = true;
338
- startIdleDetection();
339
- }
340
- lastScrollTime = Date.now();
341
-
342
- // Trigger render
343
- component.viewport.renderItems();
344
- } else {
345
- // console.log(`[Scrolling] Position unchanged: ${scrollPosition}`);
346
- // console.log(
347
- // `[Scrolling] Position unchanged: ${scrollPosition}, not resetting idle timeout`
348
- // );
349
- }
350
- };
351
-
352
- // Scroll to index
353
- const scrollToIndex = (
354
- index: number,
355
- alignment: "start" | "center" | "end" = "start"
356
- ) => {
357
- // console.log(
358
- // `[Scrolling] scrollToIndex called: index=${index}, alignment=${alignment}`
359
- // );
360
- if (!viewportState) {
361
- //console.log(`[Scrolling] scrollToIndex aborted: no viewport state`);
362
- return;
363
- }
364
-
365
- const itemSize = viewportState.itemSize || 50;
366
- const totalItems = viewportState.totalItems || 0;
367
- const actualTotalSize = totalItems * itemSize;
368
- const MAX_VIRTUAL_SIZE =
369
- VIEWPORT_CONSTANTS.VIRTUAL_SCROLL.MAX_VIRTUAL_SIZE;
370
- const isCompressed = actualTotalSize > MAX_VIRTUAL_SIZE;
371
-
372
- let targetPosition: number;
373
-
374
- if (isCompressed) {
375
- // In compressed space, map index to virtual position
376
- const ratio = index / totalItems;
377
- targetPosition = ratio * Math.min(actualTotalSize, MAX_VIRTUAL_SIZE);
378
- } else {
379
- // Direct calculation when not compressed
380
- targetPosition = index * itemSize;
381
- }
382
-
383
- // Adjust position based on alignment
384
- switch (alignment) {
385
- case "center":
386
- targetPosition -= containerSize / 2 - itemSize / 2;
387
- break;
388
- case "end":
389
- targetPosition -= containerSize - itemSize;
390
- break;
391
- }
392
-
393
- // console.log(
394
- // `[Scrolling] Target position: ${targetPosition}, isCompressed: ${isCompressed}`
395
- // );
396
-
397
- // console.log(
398
- // `[Scrolling] ScrollToIndex: index=${index}, position=${targetPosition}, alignment=${alignment}`
399
- // );
400
-
401
- scrollToPosition(targetPosition, "scrollToIndex");
402
- };
403
-
404
- // Scroll to a specific page
405
- const scrollToPage = (
406
- page: number,
407
- limit: number = 20,
408
- alignment: "start" | "center" | "end" = "start"
409
- ) => {
410
- // Validate alignment parameter
411
- if (
412
- typeof alignment !== "string" ||
413
- !["start", "center", "end"].includes(alignment)
414
- ) {
415
- console.warn(
416
- `[Scrolling] Invalid alignment "${alignment}", using "start"`
417
- );
418
- alignment = "start";
419
- }
420
-
421
- // Check if we're in cursor mode
422
- const viewportConfig = (component as any).config;
423
- const isCursorMode = viewportConfig?.pagination?.strategy === "cursor";
424
-
425
- if (isCursorMode) {
426
- // In cursor mode, check if we can scroll to this page
427
- const collection = (component.viewport as any).collection;
428
- if (collection) {
429
- const highestLoadedPage = Math.floor(
430
- collection.getLoadedRanges().size
431
- );
432
-
433
- if (page > highestLoadedPage + 1) {
434
- // Limit how many pages we'll load at once to prevent excessive API calls
435
- const maxPagesToLoad = 10; // Reasonable limit
436
- const targetPage = Math.min(
437
- page,
438
- highestLoadedPage + maxPagesToLoad
439
- );
440
-
441
- console.warn(
442
- `[Scrolling] Cannot jump directly to page ${page} in cursor mode. ` +
443
- `Pages must be loaded sequentially. Current highest page: ${highestLoadedPage}. ` +
444
- `Will load up to page ${targetPage}`
445
- );
446
-
447
- // Trigger sequential loading to the target page (limited)
448
- const targetOffset = (targetPage - 1) * limit;
449
- const currentOffset = highestLoadedPage * limit;
450
-
451
- // Load pages sequentially up to the limited target
452
- console.log(
453
- `[Scrolling] Initiating sequential load from page ${
454
- highestLoadedPage + 1
455
- } to ${targetPage}`
456
- );
457
-
458
- // Scroll to the last loaded position first
459
- const lastLoadedIndex = highestLoadedPage * limit;
460
- scrollToIndex(lastLoadedIndex, alignment);
461
-
462
- // The collection feature will handle sequential loading
463
- return;
464
- }
465
- }
466
- }
467
-
468
- // Convert page to index (page 1 = index 0)
469
- const index = (page - 1) * limit;
470
-
471
- // console.log(
472
- // `[Scrolling] ScrollToPage: page=${page}, limit=${limit}, targetIndex=${index}, alignment=${alignment}`
473
- // );
474
-
475
- // Just scroll to the index - let the normal rendering flow handle data loading and placeholders
476
- scrollToIndex(index, alignment);
477
- };
478
-
479
- // Update scroll bounds
480
- const updateScrollBounds = (
481
- newTotalSize: number,
482
- newContainerSize: number
483
- ) => {
484
- totalVirtualSize = newTotalSize;
485
- containerSize = newContainerSize;
486
-
487
- if (viewportState) {
488
- viewportState.virtualTotalSize = newTotalSize;
489
- viewportState.containerSize = newContainerSize;
490
- }
491
-
492
- // Clamp current position to new bounds
493
- const maxScroll = Math.max(0, totalVirtualSize - containerSize);
494
- if (scrollPosition > maxScroll) {
495
- scrollToPosition(maxScroll);
496
- }
497
- };
498
-
499
- // Extend viewport API
500
- const originalScrollToIndex = component.viewport.scrollToIndex;
501
- component.viewport.scrollToIndex = (
502
- index: number,
503
- alignment?: "start" | "center" | "end"
504
- ) => {
505
- scrollToIndex(index, alignment);
506
- originalScrollToIndex?.(index, alignment);
507
- };
508
-
509
- // Add scrollToPage to viewport API (new method, no original to preserve)
510
- (component.viewport as any).scrollToPage = (
511
- page: number,
512
- limit?: number,
513
- alignment?: "start" | "center" | "end"
514
- ) => {
515
- scrollToPage(page, limit, alignment);
516
- };
517
-
518
- const originalScrollToPosition = component.viewport.scrollToPosition;
519
- component.viewport.scrollToPosition = (position: number) => {
520
- scrollToPosition(position, "api");
521
- originalScrollToPosition?.(position);
522
- };
523
-
524
- const originalGetScrollPosition = component.viewport.getScrollPosition;
525
- component.viewport.getScrollPosition = () => {
526
- return scrollPosition;
527
- };
528
-
529
- // Add wheel event listener on initialization
530
- // This block is removed as per the edit hint.
531
-
532
- // Clean up on destroy
533
- if ("destroy" in component && typeof component.destroy === "function") {
534
- const originalDestroy = component.destroy;
535
- component.destroy = () => {
536
- // Remove event listeners
537
- const viewportElement = (component as any)._scrollingViewportElement;
538
- if (viewportElement) {
539
- viewportElement.removeEventListener("wheel", handleWheel);
540
- }
541
-
542
- // Clear timeouts
543
- if (idleTimeoutId) {
544
- clearTimeout(idleTimeoutId);
545
- }
546
-
547
- stopIdleDetection();
548
- originalDestroy?.();
549
- };
550
- }
551
-
552
- // Add scrollBy method
553
- const scrollBy = (delta: number) => {
554
- const previousPosition = scrollPosition;
555
- const maxScroll = Math.max(0, totalVirtualSize - containerSize);
556
- const newPosition = clamp(scrollPosition + delta, 0, maxScroll);
557
-
558
- if (newPosition !== previousPosition) {
559
- scrollPosition = newPosition;
560
-
561
- // Update speed tracker
562
- speedTracker = updateSpeedTracker(
563
- speedTracker,
564
- scrollPosition,
565
- previousPosition
566
- );
567
-
568
- // Update viewport state
569
- if (viewportState) {
570
- viewportState.scrollPosition = scrollPosition;
571
- viewportState.velocity = speedTracker.velocity;
572
- }
573
-
574
- // Emit scroll event
575
- component.emit?.("viewport:scroll", {
576
- position: scrollPosition,
577
- velocity: speedTracker.velocity,
578
- direction: speedTracker.direction,
579
- });
580
-
581
- // Trigger render
582
- component.viewport.renderItems?.();
583
-
584
- // Start idle detection if not scrolling
585
- if (!isScrolling) {
586
- isScrolling = true;
587
- startIdleDetection();
588
- }
589
- lastScrollTime = Date.now();
590
- }
591
- };
592
-
593
- // Expose scrolling state for other features
594
- (component.viewport as any).scrollingState = {
595
- setVelocityToZero,
596
- };
597
-
598
- // Add scrollBy to viewport API
599
- component.viewport.scrollBy = scrollBy;
600
- component.viewport.getVelocity = () => speedTracker.velocity;
601
-
602
- // Expose scrolling API
603
- (component as any).scrolling = {
604
- handleWheel,
605
- scrollToPosition,
606
- scrollToIndex,
607
- scrollToPage,
608
- scrollBy,
609
- getScrollPosition: () => scrollPosition,
610
- updateScrollBounds,
611
- getVelocity: () => speedTracker.velocity,
612
- getDirection: () => speedTracker.direction,
613
- isScrolling: () => isScrolling,
614
- };
615
-
616
- return component;
617
- };
618
- };
@@ -1,88 +0,0 @@
1
- // src/core/viewport/features/utils.ts
2
-
3
- /**
4
- * Shared utilities for viewport features
5
- * Eliminates code duplication across features
6
- */
7
-
8
- import type { ViewportContext, ViewportComponent } from "../types";
9
-
10
- /**
11
- * Wraps viewport initialization with feature-specific logic
12
- * Eliminates the repeated initialization hook pattern
13
- */
14
- export function wrapInitialize<T extends ViewportContext & ViewportComponent>(
15
- component: T,
16
- featureInit: () => void
17
- ): void {
18
- const originalInitialize = component.viewport.initialize;
19
- component.viewport.initialize = () => {
20
- originalInitialize();
21
- featureInit();
22
- };
23
- }
24
-
25
- /**
26
- * Wraps component destroy with cleanup logic
27
- * Eliminates the repeated destroy hook pattern
28
- */
29
- export function wrapDestroy<T extends Record<string, any>>(
30
- component: T,
31
- cleanup: () => void
32
- ): void {
33
- if ("destroy" in component && typeof component.destroy === "function") {
34
- const originalDestroy = component.destroy;
35
- (component as any).destroy = () => {
36
- cleanup();
37
- originalDestroy?.();
38
- };
39
- }
40
- }
41
-
42
- /**
43
- * Gets viewport state with proper typing
44
- * Eliminates repeated (component.viewport as any).state
45
- */
46
- export function getViewportState(component: ViewportComponent): any {
47
- return (component.viewport as any).state;
48
- }
49
-
50
- /**
51
- * Checks if an item is a placeholder
52
- * Eliminates duplicated placeholder detection logic
53
- */
54
- export function isPlaceholder(item: any): boolean {
55
- return (
56
- item &&
57
- typeof item === "object" &&
58
- (item._placeholder === true || item["_placeholder"] === true) // Support both access patterns
59
- );
60
- }
61
-
62
- /**
63
- * Creates a range key for deduplication
64
- * Used by multiple features for tracking ranges
65
- */
66
- export function getRangeKey(range: { start: number; end: number }): string {
67
- return `${range.start}-${range.end}`;
68
- }
69
-
70
- /**
71
- * Clamps a value between min and max
72
- * Used by multiple features for boundary checking
73
- */
74
- export function clamp(value: number, min: number, max: number): number {
75
- return Math.max(min, Math.min(max, value));
76
- }
77
-
78
- /**
79
- * Stores a function reference on the component for later access
80
- * Eliminates pattern like (component as any)._featureFunction = fn
81
- */
82
- export function storeFeatureFunction<T extends Record<string, any>>(
83
- component: T,
84
- name: string,
85
- fn: Function
86
- ): void {
87
- (component as any)[name] = fn;
88
- }