mtrl-addons 0.1.2 → 0.2.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 (117) hide show
  1. package/AI.md +28 -230
  2. package/CLAUDE.md +882 -0
  3. package/build.js +253 -24
  4. package/package.json +14 -4
  5. package/scripts/debug/vlist-selection.ts +121 -0
  6. package/src/components/index.ts +5 -41
  7. package/src/components/{list → vlist}/config.ts +66 -95
  8. package/src/components/vlist/constants.ts +23 -0
  9. package/src/components/vlist/features/api.ts +626 -0
  10. package/src/components/vlist/features/index.ts +10 -0
  11. package/src/components/vlist/features/selection.ts +436 -0
  12. package/src/components/vlist/features/viewport.ts +59 -0
  13. package/src/components/vlist/index.ts +17 -0
  14. package/src/components/{list → vlist}/types.ts +242 -32
  15. package/src/components/vlist/vlist.ts +92 -0
  16. package/src/core/compose/features/gestures/index.ts +227 -0
  17. package/src/core/compose/features/gestures/longpress.ts +383 -0
  18. package/src/core/compose/features/gestures/pan.ts +424 -0
  19. package/src/core/compose/features/gestures/pinch.ts +475 -0
  20. package/src/core/compose/features/gestures/rotate.ts +485 -0
  21. package/src/core/compose/features/gestures/swipe.ts +492 -0
  22. package/src/core/compose/features/gestures/tap.ts +334 -0
  23. package/src/core/compose/features/index.ts +2 -38
  24. package/src/core/compose/index.ts +13 -29
  25. package/src/core/gestures/index.ts +31 -0
  26. package/src/core/gestures/longpress.ts +68 -0
  27. package/src/core/gestures/manager.ts +418 -0
  28. package/src/core/gestures/pan.ts +48 -0
  29. package/src/core/gestures/pinch.ts +58 -0
  30. package/src/core/gestures/rotate.ts +58 -0
  31. package/src/core/gestures/swipe.ts +66 -0
  32. package/src/core/gestures/tap.ts +45 -0
  33. package/src/core/gestures/types.ts +387 -0
  34. package/src/core/gestures/utils.ts +128 -0
  35. package/src/core/index.ts +27 -151
  36. package/src/core/layout/schema.ts +153 -72
  37. package/src/core/layout/types.ts +5 -2
  38. package/src/core/viewport/constants.ts +145 -0
  39. package/src/core/viewport/features/base.ts +73 -0
  40. package/src/core/viewport/features/collection.ts +1182 -0
  41. package/src/core/viewport/features/events.ts +130 -0
  42. package/src/core/viewport/features/index.ts +20 -0
  43. package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
  44. package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
  45. package/src/core/viewport/features/momentum.ts +269 -0
  46. package/src/core/viewport/features/placeholders.ts +335 -0
  47. package/src/core/viewport/features/rendering.ts +962 -0
  48. package/src/core/viewport/features/scrollbar.ts +434 -0
  49. package/src/core/viewport/features/scrolling.ts +634 -0
  50. package/src/core/viewport/features/utils.ts +94 -0
  51. package/src/core/viewport/features/virtual.ts +525 -0
  52. package/src/core/viewport/index.ts +31 -0
  53. package/src/core/viewport/types.ts +133 -0
  54. package/src/core/viewport/utils/speed-tracker.ts +79 -0
  55. package/src/core/viewport/viewport.ts +265 -0
  56. package/src/index.ts +0 -7
  57. package/src/styles/components/_vlist.scss +352 -0
  58. package/src/styles/index.scss +1 -1
  59. package/test/components/vlist-selection.test.ts +240 -0
  60. package/test/components/vlist.test.ts +63 -0
  61. package/test/core/collection/adapter.test.ts +161 -0
  62. package/bun.lock +0 -792
  63. package/src/components/list/api.ts +0 -314
  64. package/src/components/list/constants.ts +0 -56
  65. package/src/components/list/features/api.ts +0 -428
  66. package/src/components/list/features/index.ts +0 -31
  67. package/src/components/list/features/list-manager.ts +0 -502
  68. package/src/components/list/index.ts +0 -39
  69. package/src/components/list/list.ts +0 -234
  70. package/src/core/collection/base-collection.ts +0 -100
  71. package/src/core/collection/collection-composer.ts +0 -178
  72. package/src/core/collection/collection.ts +0 -745
  73. package/src/core/collection/constants.ts +0 -172
  74. package/src/core/collection/events.ts +0 -428
  75. package/src/core/collection/features/api/loading.ts +0 -279
  76. package/src/core/collection/features/operations/data-operations.ts +0 -147
  77. package/src/core/collection/index.ts +0 -104
  78. package/src/core/collection/state.ts +0 -497
  79. package/src/core/collection/types.ts +0 -404
  80. package/src/core/compose/features/collection.ts +0 -119
  81. package/src/core/compose/features/selection.ts +0 -213
  82. package/src/core/compose/features/styling.ts +0 -108
  83. package/src/core/list-manager/api.ts +0 -599
  84. package/src/core/list-manager/config.ts +0 -593
  85. package/src/core/list-manager/constants.ts +0 -268
  86. package/src/core/list-manager/features/api.ts +0 -58
  87. package/src/core/list-manager/features/collection/collection.ts +0 -705
  88. package/src/core/list-manager/features/collection/index.ts +0 -17
  89. package/src/core/list-manager/features/viewport/constants.ts +0 -42
  90. package/src/core/list-manager/features/viewport/index.ts +0 -16
  91. package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
  92. package/src/core/list-manager/features/viewport/rendering.ts +0 -575
  93. package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
  94. package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
  95. package/src/core/list-manager/features/viewport/template.ts +0 -220
  96. package/src/core/list-manager/features/viewport/viewport.ts +0 -654
  97. package/src/core/list-manager/features/viewport/virtual.ts +0 -309
  98. package/src/core/list-manager/index.ts +0 -279
  99. package/src/core/list-manager/list-manager.ts +0 -206
  100. package/src/core/list-manager/types.ts +0 -439
  101. package/src/core/list-manager/utils/calculations.ts +0 -290
  102. package/src/core/list-manager/utils/range-calculator.ts +0 -349
  103. package/src/core/list-manager/utils/speed-tracker.ts +0 -273
  104. package/src/styles/components/_list.scss +0 -244
  105. package/src/types/mtrl.d.ts +0 -6
  106. package/test/components/list.test.ts +0 -256
  107. package/test/core/collection/failed-ranges.test.ts +0 -270
  108. package/test/core/compose/features.test.ts +0 -183
  109. package/test/core/list-manager/features/collection.test.ts +0 -704
  110. package/test/core/list-manager/features/viewport.test.ts +0 -698
  111. package/test/core/list-manager/list-manager.test.ts +0 -593
  112. package/test/core/list-manager/utils/calculations.test.ts +0 -433
  113. package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
  114. package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
  115. package/tsconfig.build.json +0 -23
  116. /package/src/components/{list → vlist}/features.ts +0 -0
  117. /package/src/core/{compose → viewport}/features/performance.ts +0 -0
@@ -1,705 +0,0 @@
1
- /**
2
- * Collection Feature - Data Management Enhancer
3
- * Handles speed tracking, range loading, collection integration, and placeholders
4
- */
5
-
6
- import type {
7
- ListManagerComponent,
8
- ItemRange,
9
- SpeedTracker,
10
- } from "../../types";
11
- import { LIST_MANAGER_CONSTANTS, PLACEHOLDER } from "../../constants";
12
-
13
- /**
14
- * Configuration for collection enhancer
15
- */
16
- export interface CollectionConfig {
17
- collection?: any; // Collection adapter
18
- rangeSize?: number;
19
- strategy?: "page" | "offset" | "cursor";
20
- fastThreshold?: number;
21
- slowThreshold?: number;
22
- maskCharacter?: string;
23
- enablePlaceholders?: boolean;
24
- }
25
-
26
- /**
27
- * Calculate optimal range size based on viewport dimensions
28
- * @param containerSize - Viewport container size in pixels
29
- * @param estimatedItemSize - Estimated item size in pixels
30
- * @param overscan - Number of items to render outside viewport
31
- * @returns Optimal range size for data loading
32
- */
33
- function calculateOptimalRangeSize(
34
- containerSize: number,
35
- estimatedItemSize: number,
36
- overscan: number = 5
37
- ): number {
38
- // Calculate how many items fit in the viewport
39
- const itemsInViewport = Math.ceil(containerSize / estimatedItemSize);
40
-
41
- // Add overscan buffer for smooth scrolling
42
- const withOverscan = itemsInViewport + overscan * 2;
43
-
44
- // Add preload buffer for better UX (50% more items)
45
- const withPreload = Math.ceil(withOverscan * 1.5);
46
-
47
- // Ensure minimum of 10 items and maximum of 100 items per range
48
- const optimalRangeSize = Math.max(10, Math.min(100, withPreload));
49
-
50
- return optimalRangeSize;
51
- }
52
-
53
- /**
54
- * Component interface after collection enhancement
55
- */
56
- export interface CollectionComponent {
57
- collection: {
58
- // Data management
59
- setItems: (items: any[]) => void;
60
- getItems: () => any[];
61
- setTotalItems: (total: number) => void;
62
- getTotalItems: () => number;
63
-
64
- // Range management
65
- loadRange: (offset: number, limit: number) => Promise<any[]>;
66
- loadMissingRanges: (range: ItemRange) => Promise<void>;
67
- getLoadedRanges: () => Set<number>;
68
- getPendingRanges: () => Set<number>;
69
- getFailedRanges: () => Map<
70
- number,
71
- { attempts: number; lastError: Error; timestamp: number }
72
- >;
73
- clearFailedRanges: () => void;
74
- retryFailedRange: (rangeId: number) => Promise<any[]>;
75
-
76
- // Pagination strategy
77
- setPaginationStrategy: (strategy: "page" | "offset" | "cursor") => void;
78
- getPaginationStrategy: () => "page" | "offset" | "cursor";
79
-
80
- // Placeholder system
81
- analyzeDataStructure: (items: any[]) => void;
82
- generatePlaceholderItem: (index: number) => any;
83
- showPlaceholders: (count: number) => void;
84
- getPlaceholderStructure: () => Map<
85
- string,
86
- { min: number; max: number }
87
- > | null;
88
-
89
- // State
90
- isInitialized: () => boolean;
91
- updateLoadedData: (items: any[], offset: number) => void;
92
- };
93
- }
94
-
95
- /**
96
- * Adds collection functionality to a List Manager component
97
- *
98
- * @param config - Collection configuration
99
- * @returns Function that enhances a component with collection capabilities
100
- */
101
- export const withCollection =
102
- (config: CollectionConfig = {}) =>
103
- <T extends ListManagerComponent>(component: T): T & CollectionComponent => {
104
- // Configuration with defaults - now uses dynamic range size
105
- const collection = config.collection;
106
-
107
- // Calculate optimal range size based on component dimensions
108
- const containerElement = component.element;
109
- const containerSize = containerElement?.offsetHeight || 600; // Default to 600px
110
- const estimatedItemSize = 84; // Default estimated item size
111
- const overscan = 5; // Default overscan
112
-
113
- // Use provided rangeSize from config, otherwise calculate optimal range size
114
- const rangeSize =
115
- config.rangeSize ||
116
- calculateOptimalRangeSize(containerSize, estimatedItemSize, overscan);
117
-
118
- let paginationStrategy: "page" | "offset" | "cursor" =
119
- config.strategy || "page";
120
- const maskCharacter =
121
- config.maskCharacter || LIST_MANAGER_CONSTANTS.PLACEHOLDER.MASK_CHARACTER;
122
- const enablePlaceholders = config.enablePlaceholders !== false;
123
-
124
- // Range management
125
- const loadedRanges = new Set<number>();
126
- const pendingRanges = new Set<number>();
127
- const failedRanges = new Map<
128
- number,
129
- { attempts: number; lastError: Error; timestamp: number }
130
- >();
131
-
132
- // Track actual pending ranges (not just IDs) for better overlap detection
133
- const pendingRangeDetails = new Map<
134
- number,
135
- { start: number; end: number }
136
- >();
137
-
138
- // Placeholder system
139
- let placeholderStructure: Map<string, { min: number; max: number }> | null =
140
- null;
141
- let placeholderTemplate:
142
- | ((item: any, index: number) => string | HTMLElement)
143
- | null = null;
144
-
145
- // State
146
- let isCollectionInitialized = false;
147
-
148
- // Initialize component items array if not already present
149
- if (!component.items) {
150
- component.items = [];
151
- }
152
-
153
- /**
154
- * Initialize collection
155
- */
156
- const initialize = (): void => {
157
- if (isCollectionInitialized) return;
158
-
159
- // Set template if provided
160
- if (component.template) {
161
- placeholderTemplate = component.template;
162
- }
163
-
164
- isCollectionInitialized = true;
165
- component.emit?.("collection:initialized", {
166
- strategy: paginationStrategy,
167
- rangeSize,
168
- });
169
-
170
- // 🔥 AUTO-LOAD INITIAL DATA
171
- if (collection) {
172
- // Load initial range on next tick to ensure all enhancers are ready
173
- setTimeout(() => {
174
- loadRange(0, rangeSize).catch((error) => {
175
- console.error("❌ [COLLECTION] Initial load failed:", error);
176
- });
177
- }, 0);
178
- }
179
- };
180
-
181
- /**
182
- * Cleanup collection resources
183
- */
184
- const destroy = (): void => {
185
- // Reset state
186
- loadedRanges.clear();
187
- pendingRanges.clear();
188
- pendingRangeDetails.clear();
189
- failedRanges.clear();
190
- placeholderStructure = null;
191
- component.items = []; // Clear items on destroy
192
- component.totalItems = 0; // Reset total items
193
- };
194
-
195
- /**
196
- * Check if a range overlaps with any pending ranges
197
- */
198
- const hasOverlappingPendingRange = (
199
- start: number,
200
- end: number
201
- ): boolean => {
202
- for (const [_, range] of pendingRangeDetails) {
203
- if (start <= range.end && end >= range.start) {
204
- return true;
205
- }
206
- }
207
- return false;
208
- };
209
-
210
- /**
211
- * Load range of items
212
- */
213
- const loadRange = async (offset: number, limit: number): Promise<any[]> => {
214
- if (!collection) {
215
- throw new Error("No collection adapter provided");
216
- }
217
-
218
- const rangeId = Math.floor(offset / rangeSize);
219
- const rangeEnd = offset + limit - 1;
220
-
221
- // Check if we already have this data
222
- if (loadedRanges.has(rangeId)) {
223
- return [];
224
- }
225
-
226
- // Check for overlapping pending ranges
227
- if (hasOverlappingPendingRange(offset, rangeEnd)) {
228
- return [];
229
- }
230
-
231
- pendingRanges.add(rangeId);
232
- pendingRangeDetails.set(rangeId, { start: offset, end: rangeEnd });
233
-
234
- try {
235
- let items: any[];
236
- let responseMeta: any = {};
237
-
238
- // Adapt to different pagination strategies
239
- switch (paginationStrategy) {
240
- case "page":
241
- // Calculate page number (1-based)
242
- const page = Math.floor(offset / limit) + 1;
243
-
244
- // For the last page, ensure we get all remaining items
245
- const isLastPage = offset + limit >= component.totalItems;
246
- const adjustedLimit = isLastPage
247
- ? Math.min(limit, component.totalItems - offset)
248
- : limit;
249
-
250
- // Adapt generic read method to page-based strategy
251
- if (collection.loadPage) {
252
- items = await collection.loadPage({ page, limit: adjustedLimit });
253
- } else if (collection.read) {
254
- const result = await collection.read({
255
- page,
256
- limit: adjustedLimit,
257
- });
258
- // Check if the adapter returned an error
259
- if (result.error) {
260
- throw new Error(result.error.message || "Failed to load data");
261
- }
262
- items = result.items || [];
263
- responseMeta = result.meta || {};
264
- } else {
265
- throw new Error(
266
- "Collection adapter missing loadPage or read method"
267
- );
268
- }
269
- break;
270
-
271
- case "offset":
272
- if (collection.loadRange) {
273
- items = await collection.loadRange({ offset, limit });
274
- } else if (collection.read) {
275
- const result = await collection.read({ offset, limit });
276
- // Check if the adapter returned an error
277
- if (result.error) {
278
- throw new Error(result.error.message || "Failed to load data");
279
- }
280
- items = result.items || [];
281
- responseMeta = result.meta || {};
282
- } else {
283
- throw new Error(
284
- "Collection adapter missing loadRange or read method"
285
- );
286
- }
287
- break;
288
-
289
- case "cursor":
290
- // For cursor-based, we need to track the cursor
291
- if (collection.loadWithCursor) {
292
- items = await collection.loadWithCursor({
293
- limit,
294
- cursor: getCursorForOffset(offset),
295
- });
296
- } else if (collection.read) {
297
- const result = await collection.read({
298
- cursor: getCursorForOffset(offset),
299
- limit,
300
- });
301
- // Check if the adapter returned an error
302
- if (result.error) {
303
- throw new Error(result.error.message || "Failed to load data");
304
- }
305
- items = result.items || [];
306
- responseMeta = result.meta || {};
307
- } else {
308
- throw new Error(
309
- "Collection adapter missing loadWithCursor or read method"
310
- );
311
- }
312
- break;
313
-
314
- default:
315
- throw new Error(
316
- `Unsupported pagination strategy: ${paginationStrategy}`
317
- );
318
- }
319
-
320
- // Set total items from API metadata if available (for massive lists)
321
- if (responseMeta.total && responseMeta.total > component.totalItems) {
322
- const newTotal = responseMeta.total;
323
- component.totalItems = newTotal;
324
- console.log(
325
- `📊 [COLLECTION] Updated total items from API metadata: ${newTotal.toLocaleString()}`
326
- );
327
- component.emit?.("total:changed", { total: newTotal });
328
-
329
- // Trigger viewport to recalculate total virtual size for massive lists
330
- if ((component as any).viewport?.updateViewport) {
331
- (component as any).viewport.updateViewport();
332
- }
333
- }
334
-
335
- // Analyze structure on first load
336
- if (enablePlaceholders && !placeholderStructure && items.length > 0) {
337
- analyzeDataStructure(items);
338
- }
339
-
340
- // Update loaded data
341
- updateLoadedData(items, offset);
342
-
343
- // Mark range as loaded
344
- loadedRanges.add(rangeId);
345
- pendingRanges.delete(rangeId);
346
- pendingRangeDetails.delete(rangeId);
347
-
348
- // Emit event
349
- component.emit?.("range:loaded", {
350
- range: { start: offset, end: offset + limit - 1 },
351
- items,
352
- });
353
-
354
- return items;
355
- } catch (error) {
356
- const errorMessage =
357
- error instanceof Error ? error.message : "Unknown error";
358
- console.warn(
359
- `⚠️ [COLLECTION] Failed to load range ${rangeId} (offset: ${offset}):`,
360
- errorMessage
361
- );
362
-
363
- // Remove from loaded ranges so it can be retried later
364
- loadedRanges.delete(rangeId);
365
-
366
- // Track failed range with retry information
367
- const failedInfo = failedRanges.get(rangeId) || {
368
- attempts: 0,
369
- lastError: error as Error,
370
- timestamp: Date.now(),
371
- };
372
- failedInfo.attempts++;
373
- failedInfo.lastError = error as Error;
374
- failedInfo.timestamp = Date.now();
375
- failedRanges.set(rangeId, failedInfo);
376
-
377
- component.emit?.("range:failed", {
378
- rangeId,
379
- offset,
380
- limit,
381
- error: error as Error,
382
- attempts: failedInfo.attempts,
383
- strategy: paginationStrategy,
384
- });
385
-
386
- // Return empty array instead of throwing
387
- // This prevents error propagation but allows the system to continue
388
- return [];
389
- } finally {
390
- pendingRanges.delete(rangeId);
391
- pendingRangeDetails.delete(rangeId);
392
- }
393
- };
394
-
395
- /**
396
- * Load missing ranges based on visible range
397
- */
398
- const loadMissingRanges = async (
399
- visibleRange: ItemRange
400
- ): Promise<void> => {
401
- if (!collection) return;
402
-
403
- // Check if we already have too many pending requests
404
- if (
405
- pendingRanges.size >=
406
- LIST_MANAGER_CONSTANTS.RANGE_LOADING.MAX_CONCURRENT_REQUESTS
407
- ) {
408
- return;
409
- }
410
-
411
- const startRange = Math.floor(visibleRange.start / rangeSize);
412
- const endRange = Math.floor(visibleRange.end / rangeSize);
413
-
414
- const loadPromises: Promise<any[]>[] = [];
415
- const rangesToLoad: number[] = [];
416
-
417
- // During fast scrolling, only load the most critical ranges
418
- const maxRangesToLoad = 3;
419
- let rangesQueued = 0;
420
-
421
- for (
422
- let range = startRange;
423
- range <= endRange && rangesQueued < maxRangesToLoad;
424
- range++
425
- ) {
426
- if (!loadedRanges.has(range) && !pendingRanges.has(range)) {
427
- // Check if this is a failed range and if we should retry
428
- const failedInfo = failedRanges.get(range);
429
- if (failedInfo) {
430
- const timeSinceLastAttempt = Date.now() - failedInfo.timestamp;
431
- const backoffTime = Math.min(
432
- 1000 * Math.pow(2, failedInfo.attempts - 1),
433
- 30000
434
- ); // Exponential backoff, max 30s
435
-
436
- if (timeSinceLastAttempt < backoffTime) {
437
- // Skip this range, too soon to retry
438
- continue;
439
- }
440
- }
441
-
442
- const offset = range * rangeSize;
443
- rangesToLoad.push(range);
444
- loadPromises.push(loadRange(offset, rangeSize));
445
- rangesQueued++;
446
- }
447
- }
448
-
449
- // Load ranges concurrently but respect max concurrent requests
450
- const maxConcurrent =
451
- LIST_MANAGER_CONSTANTS.RANGE_LOADING.MAX_CONCURRENT_REQUESTS;
452
- for (let i = 0; i < loadPromises.length; i += maxConcurrent) {
453
- const batch = loadPromises.slice(i, i + maxConcurrent);
454
- await Promise.allSettled(batch);
455
- }
456
-
457
- component.emit?.("loading:triggered", {
458
- range: visibleRange,
459
- strategy: paginationStrategy,
460
- rangesLoaded: rangesToLoad.length,
461
- });
462
- };
463
-
464
- /**
465
- * Analyze data structure for placeholder generation
466
- */
467
- const analyzeDataStructure = (items: any[]): void => {
468
- if (!items.length) return;
469
-
470
- const structure = new Map<string, { min: number; max: number }>();
471
-
472
- // Analyze sample of items to find length patterns
473
- const sampleSize = Math.min(
474
- items.length,
475
- PLACEHOLDER.PATTERN_ANALYSIS.SAMPLE_SIZE
476
- );
477
-
478
- for (let i = 0; i < sampleSize; i++) {
479
- const item = items[i];
480
- if (!item || typeof item !== "object") continue;
481
-
482
- Object.keys(item).forEach((key) => {
483
- const value = String(item[key] || "");
484
- const length = value.length;
485
-
486
- if (!structure.has(key)) {
487
- structure.set(key, { min: length, max: length });
488
- } else {
489
- const current = structure.get(key)!;
490
- structure.set(key, {
491
- min: Math.min(current.min, length),
492
- max: Math.max(current.max, length),
493
- });
494
- }
495
- });
496
- }
497
-
498
- placeholderStructure = structure;
499
- component.emit?.("structure:analyzed", { structure });
500
- };
501
-
502
- /**
503
- * Generate placeholder item based on analyzed structure
504
- */
505
- const generatePlaceholderItem = (index: number): any => {
506
- if (!placeholderStructure) {
507
- // Fallback placeholder
508
- return {
509
- id: `placeholder-${index}`,
510
- [PLACEHOLDER.PLACEHOLDER_FLAG]: true,
511
- };
512
- }
513
-
514
- const placeholder: Record<string, any> = {
515
- id: `placeholder-${index}`,
516
- [PLACEHOLDER.PLACEHOLDER_FLAG]: true,
517
- };
518
-
519
- // Generate masked values with random lengths within detected ranges
520
- placeholderStructure.forEach((range, key) => {
521
- if (LIST_MANAGER_CONSTANTS.PLACEHOLDER.RANDOM_LENGTH_VARIANCE) {
522
- const length =
523
- Math.floor(Math.random() * (range.max - range.min + 1)) + range.min;
524
- placeholder[key] = maskCharacter.repeat(length);
525
- } else {
526
- // Use average length
527
- const length = Math.round((range.min + range.max) / 2);
528
- placeholder[key] = maskCharacter.repeat(length);
529
- }
530
- });
531
-
532
- return placeholder;
533
- };
534
-
535
- /**
536
- * Show placeholders for a count of items
537
- */
538
- const showPlaceholders = (count: number): void => {
539
- if (!enablePlaceholders) return;
540
-
541
- const placeholderItems: any[] = [];
542
-
543
- for (let i = 0; i < count; i++) {
544
- const placeholderItem = generatePlaceholderItem(i);
545
- placeholderItems.push(placeholderItem);
546
-
547
- // Add to component items array if not already present
548
- if (!component.items[i]) {
549
- component.items[i] = placeholderItem;
550
- }
551
- }
552
-
553
- component.emit?.("placeholders:shown", {
554
- count,
555
- items: placeholderItems,
556
- });
557
- };
558
-
559
- /**
560
- * Update loaded data in component
561
- */
562
- const updateLoadedData = (items: any[], offset: number): void => {
563
- // Ensure items array is large enough
564
- while (component.items.length < offset + items.length) {
565
- component.items.push(null);
566
- }
567
-
568
- // Replace placeholders with real data
569
- items.forEach((item, index) => {
570
- const targetIndex = offset + index;
571
- const existingItem = component.items[targetIndex];
572
-
573
- // Check if replacing a placeholder
574
- const wasPlaceholder =
575
- existingItem && existingItem[PLACEHOLDER.PLACEHOLDER_FLAG];
576
-
577
- component.items[targetIndex] = item;
578
-
579
- if (wasPlaceholder) {
580
- component.emit?.("placeholders:replaced", {
581
- index: targetIndex,
582
- item,
583
- previousPlaceholder: existingItem,
584
- });
585
- }
586
- });
587
-
588
- // Only update total items if we don't have a large total from API metadata
589
- // This prevents overriding the massive list total (1,000,000) with loaded items count (20)
590
- const currentTotal = component.totalItems;
591
- const loadedTotal = offset + items.length;
592
-
593
- if (currentTotal < 1000 && loadedTotal > currentTotal) {
594
- // Only update for small lists where we're building the total incrementally
595
- component.totalItems = loadedTotal;
596
- component.emit?.("total:changed", { total: loadedTotal });
597
- }
598
- };
599
-
600
- /**
601
- * Get cursor for offset (cursor pagination strategy)
602
- */
603
- const getCursorForOffset = (offset: number): string | undefined => {
604
- // This would need to be implemented based on the specific cursor strategy
605
- // For now, return undefined to load from beginning
606
- return undefined;
607
- };
608
-
609
- /**
610
- * Set items directly
611
- */
612
- const setItems = (items: any[]): void => {
613
- component.items = [...items];
614
- component.totalItems = items.length;
615
-
616
- // Analyze structure if enabled and not already done
617
- if (enablePlaceholders && !placeholderStructure && items.length > 0) {
618
- analyzeDataStructure(items);
619
- }
620
-
621
- // Mark all items as loaded
622
- loadedRanges.clear();
623
- for (let i = 0; i < items.length; i += rangeSize) {
624
- const rangeId = Math.floor(i / rangeSize);
625
- loadedRanges.add(rangeId);
626
- }
627
-
628
- component.emit?.("items:set", { items, total: items.length });
629
- };
630
-
631
- /**
632
- * Set total items count
633
- */
634
- const setTotalItems = (total: number): void => {
635
- component.totalItems = total;
636
- component.emit?.("total:changed", { total });
637
- };
638
-
639
- // Initialize collection when component initializes
640
- const originalInitialize = component.initialize;
641
- component.initialize = () => {
642
- originalInitialize.call(component);
643
- initialize();
644
- };
645
-
646
- // Destroy collection when component destroys
647
- const originalDestroy = component.destroy;
648
- component.destroy = () => {
649
- destroy();
650
- originalDestroy.call(component);
651
- };
652
-
653
- // Collection API
654
- const collectionManager = {
655
- // Data management
656
- setItems,
657
- getItems: () => [...component.items],
658
- setTotalItems,
659
- getTotalItems: () => component.totalItems,
660
-
661
- // Range management
662
- loadRange,
663
- loadMissingRanges,
664
- getLoadedRanges: () => new Set(loadedRanges),
665
- getPendingRanges: () => new Set(pendingRanges),
666
- getFailedRanges: () => new Map(failedRanges),
667
- clearFailedRanges: () => {
668
- failedRanges.clear();
669
- },
670
- retryFailedRange: (rangeId: number) => {
671
- if (failedRanges.has(rangeId)) {
672
- failedRanges.delete(rangeId);
673
- const offset = rangeId * rangeSize;
674
- return loadRange(offset, rangeSize);
675
- }
676
- return Promise.resolve([]);
677
- },
678
-
679
- // Pagination strategy
680
- setPaginationStrategy: (strategy: "page" | "offset" | "cursor") => {
681
- paginationStrategy = strategy;
682
- // Reset loaded ranges when strategy changes
683
- loadedRanges.clear();
684
- pendingRanges.clear();
685
- component.emit?.("strategy:changed", { strategy });
686
- },
687
- getPaginationStrategy: () => paginationStrategy,
688
-
689
- // Placeholder system
690
- analyzeDataStructure,
691
- generatePlaceholderItem,
692
- showPlaceholders,
693
- getPlaceholderStructure: () =>
694
- placeholderStructure ? new Map(placeholderStructure) : null,
695
-
696
- // State
697
- isInitialized: () => isCollectionInitialized,
698
- updateLoadedData,
699
- };
700
-
701
- return {
702
- ...component,
703
- collection: collectionManager,
704
- };
705
- };