mtrl-addons 0.1.1 → 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 -108
  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 -14
  114. /package/src/components/{list → vlist}/features.ts +0 -0
  115. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,654 +0,0 @@
1
- /**
2
- * Viewport Feature - Complete Virtual Scrolling Enhancer
3
- * Handles orientation, virtual scrolling, custom scrollbar, and item rendering
4
- */
5
-
6
- import type {
7
- ListManagerComponent,
8
- ItemRange,
9
- ViewportInfo,
10
- } from "../../types";
11
- import { LIST_MANAGER_CONSTANTS } from "../../constants";
12
- // Removed calculateViewportInfoUtil import - using virtualManager directly
13
- import { getDefaultTemplate } from "./template";
14
- import { createItemSizeManager, type ItemSizeManager } from "./item-size";
15
- import { createScrollingManager, type ScrollingManager } from "./scrolling";
16
- import { createVirtualManager, type VirtualManager } from "./virtual";
17
- import { createRenderingManager, type RenderingManager } from "./rendering";
18
- import { scrollbar as createScrollbar } from "./scrollbar";
19
-
20
- /**
21
- * Configuration for viewport enhancer
22
- */
23
- export interface ViewportConfig {
24
- orientation?: "vertical" | "horizontal";
25
- estimatedItemSize?: number;
26
- overscan?: number;
27
- enableScrollbar?: boolean;
28
- loadDataForRange?: (
29
- range: { start: number; end: number },
30
- priority?: "high" | "normal" | "low"
31
- ) => void;
32
- measureItems?: boolean; // Add measureItems flag
33
- }
34
-
35
- /**
36
- * Component interface after viewport enhancement
37
- */
38
- export interface ViewportComponent {
39
- viewport: {
40
- // Virtual scrolling
41
- getScrollPosition(): number;
42
- getContainerSize(): number;
43
- getTotalVirtualSize(): number;
44
- getVisibleRange(): ItemRange;
45
- getViewportInfo(): ViewportInfo;
46
-
47
- // Navigation - Visual positioning only
48
- scrollToPosition(position: number): void;
49
- scrollToIndex(index: number, alignment?: "start" | "center" | "end"): void;
50
-
51
- // Item sizing
52
- measureItemSize(element: HTMLElement, index: number): number;
53
- hasMeasuredSize(index: number): boolean;
54
- getMeasuredSize(index: number): number;
55
- getEstimatedItemSize(): number;
56
-
57
- // Rendering
58
- renderItems(): void;
59
- updateItemPositions(): void;
60
- getRenderedElements(): Map<number, HTMLElement>;
61
-
62
- // State
63
- getOrientation(): "vertical" | "horizontal";
64
- isInitialized(): boolean;
65
-
66
- // Manual updates
67
- updateContainerPosition(): void;
68
- updateScrollbar(): void;
69
- updateViewport(): void;
70
- };
71
- }
72
-
73
- /**
74
- * Adds viewport functionality to a List Manager component
75
- *
76
- * @param config - Viewport configuration
77
- * @returns Function that enhances a component with viewport capabilities
78
- */
79
- export const withViewport =
80
- (config: ViewportConfig = {}) =>
81
- <T extends ListManagerComponent>(component: T): T & ViewportComponent => {
82
- // Configuration with defaults
83
- const orientation = config.orientation || "vertical";
84
- const estimatedItemSize = 84;
85
- // const estimatedItemSize =
86
- // config.estimatedItemSize ||
87
- // LIST_MANAGER_CONSTANTS.VIRTUAL_SCROLL.DEFAULT_ITEM_SIZE;
88
- const overscan =
89
- config.overscan || LIST_MANAGER_CONSTANTS.VIRTUAL_SCROLL.OVERSCAN_BUFFER;
90
- const enableScrollbar = config.enableScrollbar !== false;
91
- const measureItems = config.measureItems === true; // Default to false
92
-
93
- // Items container for virtual positioning
94
- let itemsContainer: HTMLElement | null = null;
95
-
96
- // Viewport element reference
97
- let viewportElement: HTMLElement | null = null;
98
-
99
- // State
100
- let isViewportInitialized = false;
101
- let resizeObserver: ResizeObserver | null = null;
102
-
103
- // Store the correct total items value from collection layer
104
- let actualTotalItems = component.totalItems;
105
-
106
- // Scrollbar plugin reference
107
- let scrollbarPlugin: any = null;
108
-
109
- // Item size management
110
- const itemSizeManager = createItemSizeManager({
111
- initialEstimate: estimatedItemSize,
112
- orientation,
113
- onSizeUpdated: (newTotalSize) => {
114
- // DON'T update virtual size from measured items - this breaks height capping!
115
- // The virtual size should only be managed by virtualManager.updateTotalVirtualSize()
116
- // Note: Ignoring onSizeUpdated to prevent virtual size reset from measured items
117
- scrollingManager.updateScrollbar();
118
- // Emit current state, not the reset state
119
- component.emit?.("dimensions:changed", {
120
- containerSize: virtualManager.getState().containerSize,
121
- totalVirtualSize: virtualManager.getState().totalVirtualSize,
122
- estimatedItemSize: itemSizeManager.getEstimatedItemSize(),
123
- });
124
- },
125
- onEstimatedSizeChanged: (newEstimate) => {
126
- component.emit?.("estimated-size:changed", {
127
- previousEstimate: estimatedItemSize,
128
- newEstimate,
129
- });
130
- },
131
- });
132
-
133
- // Virtual scrolling manager
134
- const virtualManager = createVirtualManager(
135
- component,
136
- itemSizeManager,
137
- {
138
- orientation,
139
- overscan,
140
- onDimensionsChanged: (data) => {
141
- scrollingManager.updateState({
142
- totalVirtualSize: data.totalVirtualSize,
143
- containerSize: data.containerSize,
144
- });
145
- scrollingManager.updateScrollbar();
146
- component.emit?.("dimensions:changed", data);
147
- },
148
- },
149
- () => actualTotalItems
150
- ); // Pass the callback to get actual total items
151
-
152
- /**
153
- * Check for missing data in a specific range and trigger loading if needed
154
- */
155
- const checkForMissingData = (targetRange: {
156
- start: number;
157
- end: number;
158
- }): void => {
159
- // Try immediate detection first
160
- const collection = (component as any).collection;
161
- const hasCollection = !!collection;
162
- const hasLoadMissingRanges =
163
- hasCollection && typeof collection.loadMissingRanges === "function";
164
-
165
- if (hasLoadMissingRanges) {
166
- // Improved missing data detection - check for null/undefined items in range
167
- let missingCount = 0;
168
- const missingIndices: number[] = [];
169
-
170
- for (let i = targetRange.start; i <= targetRange.end; i++) {
171
- // Skip if index is beyond actual total items
172
- if (i >= actualTotalItems) {
173
- break;
174
- }
175
-
176
- // Check if item is missing (null, undefined, or array doesn't extend to this index)
177
- const itemExists =
178
- i < component.items.length &&
179
- component.items[i] !== null &&
180
- component.items[i] !== undefined;
181
-
182
- if (!itemExists) {
183
- missingCount++;
184
- missingIndices.push(i);
185
- }
186
- }
187
-
188
- if (missingCount > 0) {
189
- // Always use loading manager via loadDataForRange callback
190
- if (config.loadDataForRange) {
191
- config.loadDataForRange(targetRange, "high");
192
- } else {
193
- // Fallback to direct collection call if no loading manager
194
- collection.loadMissingRanges(targetRange).catch((error: any) => {
195
- console.error("❌ Failed to load missing ranges:", error);
196
- });
197
- }
198
- }
199
- }
200
- };
201
-
202
- /**
203
- * Proactively load data for a specific range (called by scrolling manager)
204
- */
205
- const loadDataForRange = (range: { start: number; end: number }): void => {
206
- // Use the callback from config if provided (proactive approach)
207
- if (config.loadDataForRange) {
208
- config.loadDataForRange(range);
209
- } else {
210
- // Fallback to reactive approach (force re-render)
211
-
212
- // Force a re-render
213
- setTimeout(() => {
214
- renderItems();
215
- }, 0);
216
- }
217
- };
218
-
219
- // Forward declare renderItems for scrollingManager
220
- let renderItems: () => void;
221
-
222
- // Scrolling manager
223
- const scrollingManager = createScrollingManager(
224
- component,
225
- itemSizeManager,
226
- {
227
- orientation,
228
- enableScrollbar,
229
- onScrollPositionChanged: (data) => {
230
- component.emit?.("scroll:position:changed", data);
231
- },
232
- onVirtualRangeChanged: (range) => {
233
- component.emit?.("virtual:range:changed", range);
234
- },
235
- },
236
- () =>
237
- virtualManager.calculateVisibleRange(
238
- scrollingManager.getScrollPosition()
239
- ),
240
- () => {
241
- if (renderingManager) {
242
- renderingManager.renderItems();
243
- }
244
- }, // Wrap in arrow function to avoid hoisting issues
245
- () => actualTotalItems, // Pass the callback to get actual total items
246
- loadDataForRange, // Pass the proactive data loading function
247
- undefined, // Removed height cap info - using index-based scrolling
248
- (index: number) => virtualManager.calculateVirtualPositionForIndex(index) // Pass position calculator
249
- );
250
-
251
- // Rendering manager
252
- const renderingManager = createRenderingManager(
253
- component,
254
- itemSizeManager,
255
- virtualManager,
256
- scrollingManager,
257
- {
258
- orientation,
259
- overscan,
260
- loadDataForRange,
261
- measureItems, // Pass the flag
262
- },
263
- () => actualTotalItems
264
- );
265
-
266
- /**
267
- * Initialize viewport
268
- */
269
- const initialize = (): void => {
270
- if (isViewportInitialized) return;
271
-
272
- setupContainer();
273
- if (enableScrollbar) {
274
- scrollingManager.setupScrollbar();
275
- }
276
- scrollingManager.setupWheelEvents();
277
- setupCollectionEventListeners();
278
- setupResizeObserver();
279
- measureContainer();
280
-
281
- if (itemsContainer) {
282
- // Set items container styles: relative, no transform, flex for orientation
283
- itemsContainer.style.position = "relative";
284
- // Remove flex layout as it conflicts with absolute positioning of items
285
- itemsContainer.style.overflow = "hidden"; // Prevent overflow issues
286
- }
287
-
288
- isViewportInitialized = true;
289
- component.emit?.("viewport:initialized", {
290
- orientation,
291
- containerSize: virtualManager.getState().containerSize,
292
- });
293
- };
294
-
295
- /**
296
- * Setup collection event listeners for data updates
297
- */
298
- const setupCollectionEventListeners = (): void => {
299
- // Listen for collection events to trigger rendering
300
- if (component.on) {
301
- component.on("items:set", (data: any) => {
302
- // Use the total from the event data or get the current component total
303
- const currentTotal = data?.total || component.totalItems;
304
- virtualManager.updateTotalVirtualSize(currentTotal);
305
- // Force recalculation
306
- if (renderingManager) {
307
- renderingManager.renderItems();
308
- }
309
- });
310
-
311
- component.on("range:loaded", (data: any) => {
312
- // Use actualTotalItems which maintains the correct total (1M) instead of
313
- // component.totalItems which may be temporarily reset during data loading
314
- virtualManager.updateTotalVirtualSize(actualTotalItems);
315
- // Always render when data loads - the render function will handle what to show
316
- if (renderingManager) {
317
- renderingManager.renderItems();
318
- }
319
- });
320
-
321
- component.on("total:changed", (data: any) => {
322
- // Use the total from the event data instead of component.totalItems
323
- const newTotal = data?.total || component.totalItems;
324
- actualTotalItems = newTotal; // Store the correct value
325
-
326
- virtualManager.updateTotalVirtualSize(newTotal);
327
-
328
- // Update scrollbar plugin
329
- if (scrollbarPlugin && scrollbarPlugin.setTotalItems) {
330
- scrollbarPlugin.setTotalItems(newTotal);
331
- }
332
-
333
- // Force recalculation
334
- if (renderingManager) {
335
- renderingManager.renderItems();
336
- }
337
- });
338
-
339
- component.on("placeholders:replaced", () => {
340
- if (renderingManager) {
341
- renderingManager.renderItems();
342
- }
343
- });
344
-
345
- component.on("estimated-size:changed", (data: any) => {
346
- // Recalculate virtual size with updated estimated size
347
- virtualManager.updateTotalVirtualSize(actualTotalItems);
348
- scrollingManager.updateScrollbar();
349
-
350
- // Update scrollbar plugin
351
- if (scrollbarPlugin && scrollbarPlugin.setItemHeight) {
352
- scrollbarPlugin.setItemHeight(
353
- itemSizeManager.getEstimatedItemSize()
354
- );
355
- }
356
- if (scrollbarPlugin && scrollbarPlugin.setTotalItems) {
357
- scrollbarPlugin.setTotalItems(actualTotalItems);
358
- }
359
- });
360
- }
361
- };
362
-
363
- /**
364
- * Destroy viewport
365
- */
366
- const destroy = (): void => {
367
- if (!isViewportInitialized) return;
368
-
369
- scrollingManager.removeWheelEvents();
370
- scrollingManager.destroyScrollbar();
371
-
372
- // Destroy scrollbar plugin
373
- if (scrollbarPlugin && scrollbarPlugin.destroy) {
374
- scrollbarPlugin.destroy();
375
- scrollbarPlugin = null;
376
- }
377
-
378
- resizeObserver?.disconnect();
379
-
380
- // Clean up rendered elements
381
- renderingManager.clear();
382
-
383
- isViewportInitialized = false;
384
- component.emit?.("viewport:destroyed", {});
385
- };
386
-
387
- /**
388
- * Render items in the visible range
389
- */
390
- renderItems = (): void => {
391
- if (!isViewportInitialized) {
392
- return;
393
- }
394
- renderingManager.renderItems();
395
- };
396
-
397
- /**
398
- * Measure actual item size and update cache
399
- * (Kept for compatibility - delegates to itemSizeManager)
400
- */
401
- const measureItemSize = (element: HTMLElement, index: number): number => {
402
- return itemSizeManager.measureItem(element, index, orientation);
403
- };
404
-
405
- /**
406
- * Get current viewport information
407
- */
408
- const getViewportInfo = (): ViewportInfo => {
409
- const scrollPosition = scrollingManager.getScrollPosition();
410
- const state = virtualManager.getState();
411
- const visibleRange = virtualManager.calculateVisibleRange(scrollPosition);
412
-
413
- return {
414
- containerSize: state.containerSize,
415
- totalVirtualSize: state.totalVirtualSize,
416
- visibleRange,
417
- virtualScrollPosition: scrollPosition,
418
- };
419
- };
420
-
421
- /**
422
- * Update viewport manually
423
- */
424
- const updateViewport = (): void => {
425
- measureContainer();
426
- virtualManager.updateTotalVirtualSize(component.totalItems);
427
- scrollingManager.updateContainerPosition();
428
- scrollingManager.updateScrollbar();
429
- renderItems();
430
- };
431
-
432
- // Private helper functions
433
-
434
- /**
435
- * Setup scrollbar plugin integration
436
- */
437
- const setupScrollbarPlugin = (): void => {
438
- // Create simple adapter interface for scrollbar plugin
439
- const listManagerAdapter = {
440
- getConfig: () => ({
441
- container: component.element,
442
- }),
443
- subscribe: (callback: (payload: any) => void) => {
444
- // Simple event subscription using component events
445
- component.on?.("scroll:position:changed", (data: any) => {
446
- callback({
447
- event: "viewport:changed",
448
- data: {
449
- ...data,
450
- source: "wheel-scroll-scrollbar-update",
451
- scrollRatio:
452
- data.position &&
453
- virtualManager.getState().totalVirtualSize >
454
- virtualManager.getState().containerSize
455
- ? data.position /
456
- (virtualManager.getState().totalVirtualSize -
457
- virtualManager.getState().containerSize)
458
- : 0,
459
- },
460
- });
461
- });
462
-
463
- component.on?.("dimensions:changed", (data: any) => {
464
- callback({
465
- event: "virtual:range:changed",
466
- data: {
467
- ...data,
468
- action: "update-scrollbar",
469
- source: "virtual-viewport",
470
- totalItems: actualTotalItems,
471
- itemHeight: itemSizeManager.getEstimatedItemSize(),
472
- totalVirtualSize:
473
- data.totalVirtualSize || virtualManager.getTotalVirtualSize(),
474
- },
475
- });
476
- });
477
-
478
- return () => {}; // Unsubscribe function
479
- },
480
- emit: (event: string, data: any) => {
481
- if (
482
- event === "viewport:changed" &&
483
- (data.source === "scrollbar" || data.source === "scrollbar-drag")
484
- ) {
485
- // Handle scrollbar events - scroll to position
486
- const targetPosition = data.scrollTop || 0;
487
- scrollingManager.scrollToPosition(targetPosition, data.source);
488
- }
489
- },
490
- };
491
-
492
- // Initialize scrollbar plugin
493
- const scrollbarPluginInstance = createScrollbar({
494
- enabled: true,
495
- itemHeight: itemSizeManager.getEstimatedItemSize(),
496
- totalItems: actualTotalItems,
497
- totalVirtualSize: virtualManager.getTotalVirtualSize(),
498
- });
499
-
500
- scrollbarPlugin = scrollbarPluginInstance.install(listManagerAdapter, {});
501
-
502
- // Connect scrollbar to scrolling manager
503
- (scrollingManager as any).setScrollbarPlugin(scrollbarPlugin);
504
- };
505
-
506
- /**
507
- * Setup container structure with transform-based virtual scrolling
508
- */
509
- const setupContainer = (): void => {
510
- component.element.style.position = "relative";
511
- component.element.style.overflow = "hidden";
512
-
513
- // Create viewport container (using class expected by scrollbar plugin)
514
- const viewport = document.createElement("div");
515
- viewport.className = `mtrl-list__viewport ${component.getClass(
516
- "list-manager"
517
- )}-viewport`;
518
- viewport.style.cssText = `
519
- position: absolute;
520
- top: 0;
521
- left: 0;
522
- width: 100%;
523
- height: 100%;
524
- overflow: hidden;
525
- `;
526
-
527
- // Store viewport reference
528
- viewportElement = viewport;
529
-
530
- // Create items container for natural item flow
531
- itemsContainer = document.createElement("div");
532
- itemsContainer.className = `${component.getClass(
533
- "list-manager"
534
- )}-viewport-items`;
535
- itemsContainer.style.cssText = `
536
- position: relative;
537
- width: 100%;
538
- will-change: contents;
539
- `;
540
-
541
- viewport.appendChild(itemsContainer);
542
- component.element.appendChild(viewport);
543
-
544
- // Set items container reference in scrolling manager and rendering manager
545
- (scrollingManager as any).setItemsContainer(itemsContainer);
546
- renderingManager.setItemsContainer(itemsContainer);
547
-
548
- // Initialize scrollbar plugin if enabled
549
- if (enableScrollbar) {
550
- setupScrollbarPlugin();
551
- }
552
- };
553
-
554
- /**
555
- * Setup resize observer
556
- */
557
- const setupResizeObserver = (): void => {
558
- resizeObserver = new ResizeObserver(() => {
559
- measureContainer();
560
- updateViewport();
561
- });
562
-
563
- // Observe the viewport element if available, otherwise the component element
564
- const observeElement = viewportElement || component.element;
565
- resizeObserver.observe(observeElement);
566
- };
567
-
568
- /**
569
- * Measure container dimensions
570
- */
571
- const measureContainer = (): void => {
572
- // Measure the actual viewport element, not the component element
573
- const measureElement = viewportElement || component.element;
574
- const newSize =
575
- orientation === "vertical"
576
- ? measureElement.offsetHeight
577
- : measureElement.offsetWidth;
578
-
579
- const currentSize = virtualManager.getState().containerSize;
580
- if (newSize !== currentSize) {
581
- virtualManager.updateState({ containerSize: newSize });
582
- scrollingManager.updateState({ containerSize: newSize });
583
- scrollingManager.updateContainerPosition();
584
- scrollingManager.updateScrollbar();
585
-
586
- component.emit?.("viewport:changed", getViewportInfo());
587
- }
588
- };
589
-
590
- // Initialize viewport when component initializes
591
- const originalInitialize = component.initialize;
592
- component.initialize = () => {
593
- originalInitialize.call(component);
594
- initialize();
595
- };
596
-
597
- // Destroy viewport when component destroys
598
- const originalDestroy = component.destroy;
599
- component.destroy = () => {
600
- destroy();
601
- originalDestroy.call(component);
602
- };
603
-
604
- // Viewport API
605
- const viewport = {
606
- // Virtual scrolling
607
- getScrollPosition: () => scrollingManager.getScrollPosition(),
608
- getContainerSize: () => virtualManager.getState().containerSize,
609
- getTotalVirtualSize: () => virtualManager.getState().totalVirtualSize,
610
- getVisibleRange: () =>
611
- virtualManager.calculateVisibleRange(
612
- scrollingManager.getScrollPosition()
613
- ),
614
- getViewportInfo,
615
-
616
- // Navigation - Visual positioning only
617
- scrollToPosition: scrollingManager.scrollToPosition,
618
- scrollToIndex: scrollingManager.scrollToIndex,
619
-
620
- // Item sizing
621
- measureItemSize,
622
- hasMeasuredSize: (index: number) =>
623
- itemSizeManager.hasMeasuredSize(index),
624
- getMeasuredSize: (index: number) =>
625
- itemSizeManager.getMeasuredSize(index),
626
- getEstimatedItemSize: () => itemSizeManager.getEstimatedItemSize(),
627
-
628
- // Rendering
629
- renderItems,
630
- updateItemPositions: renderingManager.updateItemPositions,
631
- getRenderedElements: renderingManager.getRenderedElements,
632
-
633
- // State
634
- getOrientation: () => orientation,
635
- isInitialized: () => isViewportInitialized,
636
-
637
- // Manual updates
638
- updateContainerPosition: scrollingManager.updateContainerPosition,
639
- updateScrollbar: scrollingManager.updateScrollbar,
640
- updateViewport,
641
-
642
- // Proactive data loading
643
- loadDataForRange,
644
-
645
- // Lifecycle
646
- initialize,
647
- destroy,
648
- };
649
-
650
- return {
651
- ...component,
652
- viewport,
653
- };
654
- };