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.
- package/AI.md +28 -230
- package/CLAUDE.md +882 -0
- package/build.js +253 -24
- package/package.json +14 -4
- package/scripts/debug/vlist-selection.ts +121 -0
- package/src/components/index.ts +5 -41
- package/src/components/{list → vlist}/config.ts +66 -95
- package/src/components/vlist/constants.ts +23 -0
- package/src/components/vlist/features/api.ts +626 -0
- package/src/components/vlist/features/index.ts +10 -0
- package/src/components/vlist/features/selection.ts +436 -0
- package/src/components/vlist/features/viewport.ts +59 -0
- package/src/components/vlist/index.ts +17 -0
- package/src/components/{list → vlist}/types.ts +242 -32
- package/src/components/vlist/vlist.ts +92 -0
- package/src/core/compose/features/gestures/index.ts +227 -0
- package/src/core/compose/features/gestures/longpress.ts +383 -0
- package/src/core/compose/features/gestures/pan.ts +424 -0
- package/src/core/compose/features/gestures/pinch.ts +475 -0
- package/src/core/compose/features/gestures/rotate.ts +485 -0
- package/src/core/compose/features/gestures/swipe.ts +492 -0
- package/src/core/compose/features/gestures/tap.ts +334 -0
- package/src/core/compose/features/index.ts +2 -38
- package/src/core/compose/index.ts +13 -29
- package/src/core/gestures/index.ts +31 -0
- package/src/core/gestures/longpress.ts +68 -0
- package/src/core/gestures/manager.ts +418 -0
- package/src/core/gestures/pan.ts +48 -0
- package/src/core/gestures/pinch.ts +58 -0
- package/src/core/gestures/rotate.ts +58 -0
- package/src/core/gestures/swipe.ts +66 -0
- package/src/core/gestures/tap.ts +45 -0
- package/src/core/gestures/types.ts +387 -0
- package/src/core/gestures/utils.ts +128 -0
- package/src/core/index.ts +27 -151
- package/src/core/layout/schema.ts +153 -72
- package/src/core/layout/types.ts +5 -2
- package/src/core/viewport/constants.ts +145 -0
- package/src/core/viewport/features/base.ts +73 -0
- package/src/core/viewport/features/collection.ts +1182 -0
- package/src/core/viewport/features/events.ts +130 -0
- package/src/core/viewport/features/index.ts +20 -0
- package/src/core/{list-manager/features/viewport → viewport/features}/item-size.ts +31 -34
- package/src/core/{list-manager/features/viewport → viewport/features}/loading.ts +4 -4
- package/src/core/viewport/features/momentum.ts +269 -0
- package/src/core/viewport/features/placeholders.ts +335 -0
- package/src/core/viewport/features/rendering.ts +962 -0
- package/src/core/viewport/features/scrollbar.ts +434 -0
- package/src/core/viewport/features/scrolling.ts +634 -0
- package/src/core/viewport/features/utils.ts +94 -0
- package/src/core/viewport/features/virtual.ts +525 -0
- package/src/core/viewport/index.ts +31 -0
- package/src/core/viewport/types.ts +133 -0
- package/src/core/viewport/utils/speed-tracker.ts +79 -0
- package/src/core/viewport/viewport.ts +265 -0
- package/src/index.ts +0 -7
- package/src/styles/components/_vlist.scss +352 -0
- package/src/styles/index.scss +1 -1
- package/test/components/vlist-selection.test.ts +240 -0
- package/test/components/vlist.test.ts +63 -0
- package/test/core/collection/adapter.test.ts +161 -0
- package/bun.lock +0 -792
- package/src/components/list/api.ts +0 -314
- package/src/components/list/constants.ts +0 -56
- package/src/components/list/features/api.ts +0 -428
- package/src/components/list/features/index.ts +0 -31
- package/src/components/list/features/list-manager.ts +0 -502
- package/src/components/list/index.ts +0 -39
- package/src/components/list/list.ts +0 -234
- package/src/core/collection/base-collection.ts +0 -100
- package/src/core/collection/collection-composer.ts +0 -178
- package/src/core/collection/collection.ts +0 -745
- package/src/core/collection/constants.ts +0 -172
- package/src/core/collection/events.ts +0 -428
- package/src/core/collection/features/api/loading.ts +0 -279
- package/src/core/collection/features/operations/data-operations.ts +0 -147
- package/src/core/collection/index.ts +0 -104
- package/src/core/collection/state.ts +0 -497
- package/src/core/collection/types.ts +0 -404
- package/src/core/compose/features/collection.ts +0 -119
- package/src/core/compose/features/selection.ts +0 -213
- package/src/core/compose/features/styling.ts +0 -108
- package/src/core/list-manager/api.ts +0 -599
- package/src/core/list-manager/config.ts +0 -593
- package/src/core/list-manager/constants.ts +0 -268
- package/src/core/list-manager/features/api.ts +0 -58
- package/src/core/list-manager/features/collection/collection.ts +0 -705
- package/src/core/list-manager/features/collection/index.ts +0 -17
- package/src/core/list-manager/features/viewport/constants.ts +0 -42
- package/src/core/list-manager/features/viewport/index.ts +0 -16
- package/src/core/list-manager/features/viewport/placeholders.ts +0 -281
- package/src/core/list-manager/features/viewport/rendering.ts +0 -575
- package/src/core/list-manager/features/viewport/scrollbar.ts +0 -495
- package/src/core/list-manager/features/viewport/scrolling.ts +0 -795
- package/src/core/list-manager/features/viewport/template.ts +0 -220
- package/src/core/list-manager/features/viewport/viewport.ts +0 -654
- package/src/core/list-manager/features/viewport/virtual.ts +0 -309
- package/src/core/list-manager/index.ts +0 -279
- package/src/core/list-manager/list-manager.ts +0 -206
- package/src/core/list-manager/types.ts +0 -439
- package/src/core/list-manager/utils/calculations.ts +0 -290
- package/src/core/list-manager/utils/range-calculator.ts +0 -349
- package/src/core/list-manager/utils/speed-tracker.ts +0 -273
- package/src/styles/components/_list.scss +0 -244
- package/src/types/mtrl.d.ts +0 -6
- package/test/components/list.test.ts +0 -256
- package/test/core/collection/failed-ranges.test.ts +0 -270
- package/test/core/compose/features.test.ts +0 -183
- package/test/core/list-manager/features/collection.test.ts +0 -704
- package/test/core/list-manager/features/viewport.test.ts +0 -698
- package/test/core/list-manager/list-manager.test.ts +0 -593
- package/test/core/list-manager/utils/calculations.test.ts +0 -433
- package/test/core/list-manager/utils/range-calculator.test.ts +0 -569
- package/test/core/list-manager/utils/speed-tracker.test.ts +0 -530
- package/tsconfig.build.json +0 -23
- /package/src/components/{list → vlist}/features.ts +0 -0
- /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
|
-
};
|