mtrl-addons 0.2.2 → 0.2.4
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/{src/components/index.ts → dist/components/index.d.ts} +0 -2
- package/dist/components/vlist/config.d.ts +86 -0
- package/{src/components/vlist/constants.ts → dist/components/vlist/constants.d.ts} +10 -11
- package/dist/components/vlist/features/api.d.ts +7 -0
- package/{src/components/vlist/features/index.ts → dist/components/vlist/features/index.d.ts} +0 -2
- package/dist/components/vlist/features/selection.d.ts +6 -0
- package/dist/components/vlist/features/viewport.d.ts +9 -0
- package/dist/components/vlist/features.d.ts +31 -0
- package/{src/components/vlist/index.ts → dist/components/vlist/index.d.ts} +1 -10
- package/dist/components/vlist/types.d.ts +596 -0
- package/dist/components/vlist/vlist.d.ts +29 -0
- package/dist/core/compose/features/gestures/index.d.ts +86 -0
- package/dist/core/compose/features/gestures/longpress.d.ts +85 -0
- package/dist/core/compose/features/gestures/pan.d.ts +108 -0
- package/dist/core/compose/features/gestures/pinch.d.ts +111 -0
- package/dist/core/compose/features/gestures/rotate.d.ts +111 -0
- package/dist/core/compose/features/gestures/swipe.d.ts +149 -0
- package/dist/core/compose/features/gestures/tap.d.ts +79 -0
- package/{src/core/compose/features/index.ts → dist/core/compose/features/index.d.ts} +1 -2
- package/{src/core/compose/index.ts → dist/core/compose/index.d.ts} +2 -11
- package/{src/core/gestures/index.ts → dist/core/gestures/index.d.ts} +1 -20
- package/dist/core/gestures/longpress.d.ts +23 -0
- package/dist/core/gestures/manager.d.ts +14 -0
- package/dist/core/gestures/pan.d.ts +12 -0
- package/dist/core/gestures/pinch.d.ts +14 -0
- package/dist/core/gestures/rotate.d.ts +14 -0
- package/dist/core/gestures/swipe.d.ts +20 -0
- package/dist/core/gestures/tap.d.ts +12 -0
- package/dist/core/gestures/types.d.ts +320 -0
- package/dist/core/gestures/utils.d.ts +57 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/layout/config.d.ts +33 -0
- package/dist/core/layout/index.d.ts +51 -0
- package/dist/core/layout/jsx.d.ts +65 -0
- package/dist/core/layout/schema.d.ts +112 -0
- package/dist/core/layout/types.d.ts +69 -0
- package/dist/core/viewport/constants.d.ts +105 -0
- package/dist/core/viewport/features/base.d.ts +14 -0
- package/dist/core/viewport/features/collection.d.ts +41 -0
- package/dist/core/viewport/features/events.d.ts +13 -0
- package/{src/core/viewport/features/index.ts → dist/core/viewport/features/index.d.ts} +0 -7
- package/dist/core/viewport/features/item-size.d.ts +30 -0
- package/dist/core/viewport/features/loading.d.ts +34 -0
- package/dist/core/viewport/features/momentum.d.ts +17 -0
- package/dist/core/viewport/features/performance.d.ts +53 -0
- package/dist/core/viewport/features/placeholders.d.ts +38 -0
- package/dist/core/viewport/features/rendering.d.ts +16 -0
- package/dist/core/viewport/features/scrollbar.d.ts +26 -0
- package/dist/core/viewport/features/scrolling.d.ts +16 -0
- package/dist/core/viewport/features/utils.d.ts +43 -0
- package/dist/core/viewport/features/virtual.d.ts +18 -0
- package/{src/core/viewport/index.ts → dist/core/viewport/index.d.ts} +1 -17
- package/dist/core/viewport/types.d.ts +96 -0
- package/dist/core/viewport/utils/speed-tracker.d.ts +22 -0
- package/dist/core/viewport/viewport.d.ts +11 -0
- package/{src/index.ts → dist/index.d.ts} +0 -4
- package/dist/index.js +5143 -0
- package/dist/index.mjs +5111 -0
- package/dist/styles.css +254 -0
- package/dist/styles.css.map +1 -0
- package/package.json +16 -2
- package/.cursorrules +0 -117
- package/AI.md +0 -39
- package/CLAUDE.md +0 -882
- package/build.js +0 -377
- package/index.ts +0 -7
- package/scripts/analyze-orphaned-functions.ts +0 -387
- package/scripts/debug/vlist-selection.ts +0 -121
- package/src/components/vlist/config.ts +0 -323
- package/src/components/vlist/features/api.ts +0 -626
- package/src/components/vlist/features/selection.ts +0 -436
- package/src/components/vlist/features/viewport.ts +0 -59
- package/src/components/vlist/features.ts +0 -112
- package/src/components/vlist/types.ts +0 -723
- package/src/components/vlist/vlist.ts +0 -92
- package/src/core/compose/features/gestures/index.ts +0 -227
- package/src/core/compose/features/gestures/longpress.ts +0 -383
- package/src/core/compose/features/gestures/pan.ts +0 -424
- package/src/core/compose/features/gestures/pinch.ts +0 -475
- package/src/core/compose/features/gestures/rotate.ts +0 -485
- package/src/core/compose/features/gestures/swipe.ts +0 -492
- package/src/core/compose/features/gestures/tap.ts +0 -334
- package/src/core/gestures/longpress.ts +0 -68
- package/src/core/gestures/manager.ts +0 -418
- package/src/core/gestures/pan.ts +0 -48
- package/src/core/gestures/pinch.ts +0 -58
- package/src/core/gestures/rotate.ts +0 -58
- package/src/core/gestures/swipe.ts +0 -66
- package/src/core/gestures/tap.ts +0 -45
- package/src/core/gestures/types.ts +0 -387
- package/src/core/gestures/utils.ts +0 -128
- package/src/core/index.ts +0 -43
- package/src/core/layout/config.ts +0 -102
- package/src/core/layout/index.ts +0 -168
- package/src/core/layout/jsx.ts +0 -174
- package/src/core/layout/schema.ts +0 -1044
- package/src/core/layout/types.ts +0 -95
- package/src/core/viewport/constants.ts +0 -145
- package/src/core/viewport/features/base.ts +0 -73
- package/src/core/viewport/features/collection.ts +0 -1182
- package/src/core/viewport/features/events.ts +0 -130
- package/src/core/viewport/features/item-size.ts +0 -271
- package/src/core/viewport/features/loading.ts +0 -263
- package/src/core/viewport/features/momentum.ts +0 -269
- package/src/core/viewport/features/performance.ts +0 -161
- package/src/core/viewport/features/placeholders.ts +0 -335
- package/src/core/viewport/features/rendering.ts +0 -962
- package/src/core/viewport/features/scrollbar.ts +0 -434
- package/src/core/viewport/features/scrolling.ts +0 -634
- package/src/core/viewport/features/utils.ts +0 -94
- package/src/core/viewport/features/virtual.ts +0 -525
- package/src/core/viewport/types.ts +0 -133
- package/src/core/viewport/utils/speed-tracker.ts +0 -79
- package/src/core/viewport/viewport.ts +0 -265
- package/test/benchmarks/layout/advanced.test.ts +0 -656
- package/test/benchmarks/layout/comparison.test.ts +0 -519
- package/test/benchmarks/layout/performance-comparison.test.ts +0 -274
- package/test/benchmarks/layout/real-components.test.ts +0 -733
- package/test/benchmarks/layout/simple.test.ts +0 -321
- package/test/benchmarks/layout/stress.test.ts +0 -990
- package/test/collection/basic.test.ts +0 -304
- package/test/components/vlist-selection.test.ts +0 -240
- package/test/components/vlist.test.ts +0 -63
- package/test/core/collection/adapter.test.ts +0 -161
- package/test/core/collection/collection.test.ts +0 -394
- package/test/core/layout/layout.test.ts +0 -201
- package/test/utils/dom-helpers.ts +0 -275
- package/test/utils/performance-helpers.ts +0 -392
- package/tsconfig.json +0 -20
|
@@ -1,962 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rendering Feature - Item rendering and positioning for viewport
|
|
3
|
-
* Handles DOM element creation, positioning, recycling, and updates
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { addClass, removeClass, hasClass } from "mtrl";
|
|
7
|
-
import type { ViewportContext, ViewportComponent } from "../types";
|
|
8
|
-
import { VIEWPORT_CONSTANTS } from "../constants";
|
|
9
|
-
import {
|
|
10
|
-
isPlaceholder,
|
|
11
|
-
getViewportState,
|
|
12
|
-
wrapInitialize,
|
|
13
|
-
wrapDestroy,
|
|
14
|
-
} from "./utils";
|
|
15
|
-
import { createLayout } from "../../layout";
|
|
16
|
-
|
|
17
|
-
export interface RenderingConfig {
|
|
18
|
-
template?: (
|
|
19
|
-
item: any,
|
|
20
|
-
index: number,
|
|
21
|
-
) => string | HTMLElement | any[] | Record<string, any>;
|
|
22
|
-
overscan?: number;
|
|
23
|
-
measureItems?: boolean;
|
|
24
|
-
enableRecycling?: boolean;
|
|
25
|
-
maxPoolSize?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface ViewportState {
|
|
29
|
-
scrollPosition: number;
|
|
30
|
-
totalItems: number;
|
|
31
|
-
itemSize: number;
|
|
32
|
-
containerSize: number;
|
|
33
|
-
virtualTotalSize: number;
|
|
34
|
-
visibleRange: { start: number; end: number };
|
|
35
|
-
itemsContainer: HTMLElement | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Rendering feature for viewport
|
|
40
|
-
*/
|
|
41
|
-
export const withRendering = (config: RenderingConfig = {}) => {
|
|
42
|
-
return <T extends ViewportContext & ViewportComponent>(component: T): T => {
|
|
43
|
-
const {
|
|
44
|
-
template,
|
|
45
|
-
overscan = 5,
|
|
46
|
-
measureItems = false,
|
|
47
|
-
enableRecycling = true,
|
|
48
|
-
maxPoolSize = VIEWPORT_CONSTANTS.RENDERING.DEFAULT_MAX_POOL_SIZE,
|
|
49
|
-
} = config;
|
|
50
|
-
|
|
51
|
-
// State
|
|
52
|
-
const renderedElements = new Map<number, HTMLElement>();
|
|
53
|
-
const collectionItems: Record<number, any> = {};
|
|
54
|
-
const elementPool: HTMLElement[] = [];
|
|
55
|
-
const poolStats = { created: 0, recycled: 0, poolSize: 0, released: 0 };
|
|
56
|
-
|
|
57
|
-
// Store layout results for proper cleanup (prevents memory leak)
|
|
58
|
-
const layoutResults = new WeakMap<HTMLElement, { destroy: () => void }>();
|
|
59
|
-
|
|
60
|
-
// Reusable template element for HTML string parsing (more efficient than div)
|
|
61
|
-
const templateParser = document.createElement("template");
|
|
62
|
-
|
|
63
|
-
let viewportState: ViewportState | null = null;
|
|
64
|
-
let currentVisibleRange = { start: 0, end: 0 };
|
|
65
|
-
let lastRenderTime = 0;
|
|
66
|
-
let isRemovingItem = false;
|
|
67
|
-
|
|
68
|
-
// Element pool management
|
|
69
|
-
const getPooledElement = (): HTMLElement => {
|
|
70
|
-
if (enableRecycling && elementPool.length > 0) {
|
|
71
|
-
poolStats.recycled++;
|
|
72
|
-
return elementPool.pop()!;
|
|
73
|
-
}
|
|
74
|
-
const element = document.createElement("div");
|
|
75
|
-
element.className = "mtrl-viewport-item";
|
|
76
|
-
poolStats.created++;
|
|
77
|
-
return element;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const releaseElement = (element: HTMLElement): void => {
|
|
81
|
-
poolStats.released++;
|
|
82
|
-
|
|
83
|
-
// CRITICAL: Call destroy on layoutResult to clean up components and event listeners
|
|
84
|
-
// This fixes the memory leak when using layout templates
|
|
85
|
-
const layoutResult = layoutResults.get(element);
|
|
86
|
-
if (layoutResult && typeof layoutResult.destroy === "function") {
|
|
87
|
-
try {
|
|
88
|
-
layoutResult.destroy();
|
|
89
|
-
} catch (e) {
|
|
90
|
-
// Ignore destroy errors - element may already be cleaned up
|
|
91
|
-
}
|
|
92
|
-
layoutResults.delete(element);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (!enableRecycling) {
|
|
96
|
-
element.remove();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Clean element for reuse (no cloning - reuse the actual element)
|
|
100
|
-
element.className = "mtrl-viewport-item";
|
|
101
|
-
element.removeAttribute("data-index");
|
|
102
|
-
element.style.cssText = "";
|
|
103
|
-
element.innerHTML = "";
|
|
104
|
-
|
|
105
|
-
// Remove from DOM first
|
|
106
|
-
if (element.parentNode) {
|
|
107
|
-
element.parentNode.removeChild(element);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Add to pool if not full
|
|
111
|
-
if (elementPool.length < maxPoolSize) {
|
|
112
|
-
elementPool.push(element);
|
|
113
|
-
poolStats.poolSize = elementPool.length;
|
|
114
|
-
}
|
|
115
|
-
// If pool is full, element is just dereferenced and GC'd
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Initialize
|
|
119
|
-
wrapInitialize(component, () => {
|
|
120
|
-
viewportState = getViewportState(component) as ViewportState;
|
|
121
|
-
|
|
122
|
-
// Set initial items container height from current virtual size
|
|
123
|
-
if (viewportState?.itemsContainer && viewportState.virtualTotalSize > 0) {
|
|
124
|
-
viewportState.itemsContainer.style.height = `${viewportState.virtualTotalSize}px`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Listen for item update requests from API
|
|
128
|
-
component.on?.("item:update-request", (data: any) => {
|
|
129
|
-
const { index, item, previousItem } = data;
|
|
130
|
-
|
|
131
|
-
// Update the item in collectionItems
|
|
132
|
-
collectionItems[index] = item;
|
|
133
|
-
|
|
134
|
-
// Check if this item is currently rendered
|
|
135
|
-
const existingElement = renderedElements.get(index);
|
|
136
|
-
const wasVisible = !!existingElement;
|
|
137
|
-
|
|
138
|
-
// Check if item was selected before update
|
|
139
|
-
const wasSelected = existingElement
|
|
140
|
-
? hasClass(
|
|
141
|
-
existingElement,
|
|
142
|
-
VIEWPORT_CONSTANTS.SELECTION.SELECTED_CLASS,
|
|
143
|
-
) || hasClass(existingElement, "mtrl-viewport-item--selected")
|
|
144
|
-
: false;
|
|
145
|
-
|
|
146
|
-
if (existingElement && existingElement.parentNode) {
|
|
147
|
-
// Re-render the item
|
|
148
|
-
const newElement = renderItem(item, index);
|
|
149
|
-
|
|
150
|
-
if (newElement) {
|
|
151
|
-
// Copy position styles from existing element
|
|
152
|
-
Object.assign(newElement.style, {
|
|
153
|
-
position: existingElement.style.position,
|
|
154
|
-
transform: existingElement.style.transform,
|
|
155
|
-
width: existingElement.style.width,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Preserve selected state
|
|
159
|
-
if (wasSelected) {
|
|
160
|
-
addClass(newElement, VIEWPORT_CONSTANTS.SELECTION.SELECTED_CLASS);
|
|
161
|
-
addClass(newElement, "mtrl-viewport-item--selected");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Add update animation class to inner content for visibility
|
|
165
|
-
addClass(newElement, "viewport-item--updated");
|
|
166
|
-
// Also try to add to first child if it exists (the actual item content)
|
|
167
|
-
const innerItem = newElement.firstElementChild as HTMLElement;
|
|
168
|
-
if (innerItem) {
|
|
169
|
-
addClass(innerItem, "item--updated");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Replace in DOM
|
|
173
|
-
existingElement.parentNode.replaceChild(
|
|
174
|
-
newElement,
|
|
175
|
-
existingElement,
|
|
176
|
-
);
|
|
177
|
-
renderedElements.set(index, newElement);
|
|
178
|
-
releaseElement(existingElement);
|
|
179
|
-
|
|
180
|
-
// Remove the animation class after transition
|
|
181
|
-
setTimeout(() => {
|
|
182
|
-
removeClass(newElement, "viewport-item--updated");
|
|
183
|
-
if (innerItem) {
|
|
184
|
-
removeClass(innerItem, "item--updated");
|
|
185
|
-
}
|
|
186
|
-
}, 500);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Emit completion event
|
|
191
|
-
component.emit?.("item:updated", {
|
|
192
|
-
item,
|
|
193
|
-
index,
|
|
194
|
-
previousItem,
|
|
195
|
-
wasVisible,
|
|
196
|
-
wasSelected,
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Listen for item remove requests from API
|
|
201
|
-
component.on?.("item:remove-request", (data: any) => {
|
|
202
|
-
const { index, item } = data;
|
|
203
|
-
isRemovingItem = true;
|
|
204
|
-
|
|
205
|
-
// console.log(
|
|
206
|
-
// `[RENDER-FIX] ========== ITEM REMOVE START (v4) ==========`,
|
|
207
|
-
// );
|
|
208
|
-
// console.log(`[RENDER-FIX] Removing index: ${index}`);
|
|
209
|
-
|
|
210
|
-
// Get the source of truth - collection.items has already been spliced by api.ts
|
|
211
|
-
const collectionItemsSource =
|
|
212
|
-
(component as any).collection?.items ||
|
|
213
|
-
(component as any).items ||
|
|
214
|
-
[];
|
|
215
|
-
|
|
216
|
-
// console.log(
|
|
217
|
-
// `[RENDER-FIX] collectionItemsSource.length: ${collectionItemsSource.length}`,
|
|
218
|
-
// );
|
|
219
|
-
// console.log(
|
|
220
|
-
// `[RENDER-FIX] collectionItems cache size BEFORE: ${Object.keys(collectionItems).length}`,
|
|
221
|
-
// );
|
|
222
|
-
|
|
223
|
-
// Strategy: Shift collectionItems cache indices down, but use actual data
|
|
224
|
-
// from collection.items (which has been spliced) for the loaded range.
|
|
225
|
-
// For indices beyond the loaded range, shift the cached data.
|
|
226
|
-
|
|
227
|
-
const keys = Object.keys(collectionItems)
|
|
228
|
-
.map(Number)
|
|
229
|
-
.filter((k) => !isNaN(k))
|
|
230
|
-
.sort((a, b) => a - b);
|
|
231
|
-
|
|
232
|
-
// Delete the removed index
|
|
233
|
-
delete collectionItems[index];
|
|
234
|
-
|
|
235
|
-
// Shift all indices after the removed one down by 1
|
|
236
|
-
// For indices within the loaded range, use data from collection.items
|
|
237
|
-
// For indices beyond, shift the existing cached data
|
|
238
|
-
const loadedLength = collectionItemsSource.length;
|
|
239
|
-
|
|
240
|
-
for (const key of keys) {
|
|
241
|
-
if (key > index) {
|
|
242
|
-
const newIndex = key - 1;
|
|
243
|
-
// If newIndex is within loaded data, use source; otherwise shift cache
|
|
244
|
-
if (newIndex < loadedLength && collectionItemsSource[newIndex]) {
|
|
245
|
-
collectionItems[newIndex] = collectionItemsSource[newIndex];
|
|
246
|
-
} else {
|
|
247
|
-
// Shift cached data for indices beyond loaded range
|
|
248
|
-
collectionItems[newIndex] = collectionItems[key];
|
|
249
|
-
}
|
|
250
|
-
delete collectionItems[key];
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const finalKeys = Object.keys(collectionItems)
|
|
255
|
-
.map(Number)
|
|
256
|
-
.filter((k) => !isNaN(k))
|
|
257
|
-
.sort((a, b) => a - b);
|
|
258
|
-
// console.log(
|
|
259
|
-
// `[RENDER-FIX] collectionItems cache size AFTER: ${finalKeys.length}`,
|
|
260
|
-
// );
|
|
261
|
-
// console.log(
|
|
262
|
-
// `[RENDER-FIX] collectionItems keys: [${finalKeys.slice(0, 10).join(", ")}${finalKeys.length > 10 ? "..." : ""}]`,
|
|
263
|
-
// );
|
|
264
|
-
// console.log(
|
|
265
|
-
// `[RENDER-FIX] viewportState.totalItems: ${viewportState?.totalItems}`,
|
|
266
|
-
// );
|
|
267
|
-
// console.log(
|
|
268
|
-
// `[RENDER-FIX] viewportState.visibleRange: ${JSON.stringify(viewportState?.visibleRange)}`,
|
|
269
|
-
// );
|
|
270
|
-
|
|
271
|
-
// Check for any undefined/null values in first 30 items
|
|
272
|
-
const badIndices: number[] = [];
|
|
273
|
-
for (let i = 0; i < Math.min(30, finalKeys.length); i++) {
|
|
274
|
-
const k = finalKeys[i];
|
|
275
|
-
if (!collectionItems[k] || collectionItems[k]._placeholder) {
|
|
276
|
-
badIndices.push(k);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Remove the rendered element at this index
|
|
281
|
-
const existingElement = renderedElements.get(index);
|
|
282
|
-
if (existingElement && existingElement.parentNode) {
|
|
283
|
-
releaseElement(existingElement);
|
|
284
|
-
}
|
|
285
|
-
renderedElements.delete(index);
|
|
286
|
-
|
|
287
|
-
// Shift rendered elements indices down
|
|
288
|
-
const renderedKeys = Array.from(renderedElements.keys()).sort(
|
|
289
|
-
(a, b) => a - b,
|
|
290
|
-
);
|
|
291
|
-
const newRenderedElements = new Map<number, HTMLElement>();
|
|
292
|
-
for (const key of renderedKeys) {
|
|
293
|
-
if (key > index) {
|
|
294
|
-
const element = renderedElements.get(key);
|
|
295
|
-
if (element) {
|
|
296
|
-
// Update data-index attribute
|
|
297
|
-
element.dataset.index = String(key - 1);
|
|
298
|
-
newRenderedElements.set(key - 1, element);
|
|
299
|
-
}
|
|
300
|
-
} else if (key < index) {
|
|
301
|
-
const element = renderedElements.get(key);
|
|
302
|
-
if (element) {
|
|
303
|
-
newRenderedElements.set(key, element);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
renderedElements.clear();
|
|
308
|
-
for (const [key, element] of newRenderedElements) {
|
|
309
|
-
renderedElements.set(key, element);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Decrement totalItems NOW so renderItems() uses the correct count
|
|
313
|
-
// setTotalItems() will be called later by the API, which will emit
|
|
314
|
-
// viewport:total-items-changed for virtual size recalculation
|
|
315
|
-
if (viewportState && viewportState.totalItems > 0) {
|
|
316
|
-
viewportState.totalItems--;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Reset visible range to force re-render
|
|
320
|
-
currentVisibleRange = { start: -1, end: -1 };
|
|
321
|
-
|
|
322
|
-
// Emit completion event
|
|
323
|
-
component.emit?.("item:removed", {
|
|
324
|
-
item,
|
|
325
|
-
index,
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
// After item removal, clear ALL loadedRanges and collectionItems cache
|
|
329
|
-
// This forces a complete reload which is more reliable than trying to
|
|
330
|
-
// track shifted indices. The items array has been spliced and all indices
|
|
331
|
-
// are now different from what loadedRanges thinks they are.
|
|
332
|
-
const collection = (component.viewport as any)?.collection;
|
|
333
|
-
if (collection?.getLoadedRanges) {
|
|
334
|
-
const loadedRanges = collection.getLoadedRanges();
|
|
335
|
-
loadedRanges.clear();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Clear the entire collectionItems cache - it's all stale after removal
|
|
339
|
-
// Keep only the items we actually have in the source array
|
|
340
|
-
const loadedItemsCount = collectionItemsSource.length;
|
|
341
|
-
const cacheKeys = Object.keys(collectionItems)
|
|
342
|
-
.map(Number)
|
|
343
|
-
.filter((k) => !isNaN(k));
|
|
344
|
-
cacheKeys.forEach((key) => {
|
|
345
|
-
if (key >= loadedItemsCount) {
|
|
346
|
-
delete collectionItems[key];
|
|
347
|
-
}
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
// Trigger reload of visible range
|
|
351
|
-
const visibleRange = component.viewport?.getVisibleRange?.();
|
|
352
|
-
if (visibleRange && collection?.loadMissingRanges) {
|
|
353
|
-
collection.loadMissingRanges(
|
|
354
|
-
{ start: 0, end: Math.max(visibleRange.end, loadedItemsCount) },
|
|
355
|
-
"item-removal",
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Trigger re-render
|
|
360
|
-
renderItems();
|
|
361
|
-
isRemovingItem = false;
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
// Listen for collection items evicted - clean up our cache too
|
|
365
|
-
component.on?.("collection:items-evicted", (data: any) => {
|
|
366
|
-
const { keepStart, keepEnd, evictedCount } = data;
|
|
367
|
-
let cleanedCount = 0;
|
|
368
|
-
|
|
369
|
-
// Remove evicted items from our cache
|
|
370
|
-
for (const key in collectionItems) {
|
|
371
|
-
const index = parseInt(key, 10);
|
|
372
|
-
if (index < keepStart || index > keepEnd) {
|
|
373
|
-
delete collectionItems[index];
|
|
374
|
-
cleanedCount++;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
// Listen for collection data loaded
|
|
380
|
-
component.on?.("collection:range-loaded", (data: any) => {
|
|
381
|
-
// console.log(
|
|
382
|
-
// `[RENDER-FIX] collection:range-loaded - offset: ${data.offset}, items: ${data.items?.length}`,
|
|
383
|
-
// );
|
|
384
|
-
if (!data.items?.length) return;
|
|
385
|
-
|
|
386
|
-
// Analyze data structure on first load
|
|
387
|
-
const placeholders = (component as any).placeholders;
|
|
388
|
-
if (placeholders && !placeholders.hasAnalyzedStructure()) {
|
|
389
|
-
placeholders.analyzeDataStructure(data.items);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Update collection items and replace placeholders
|
|
393
|
-
// console.log(
|
|
394
|
-
// `[RENDER-FIX] Updating collectionItems for indices ${data.offset} to ${data.offset + data.items.length - 1}`,
|
|
395
|
-
// );
|
|
396
|
-
let replacedCount = 0;
|
|
397
|
-
let skippedCount = 0;
|
|
398
|
-
data.items.forEach((item: any, i: number) => {
|
|
399
|
-
const index = data.offset + i;
|
|
400
|
-
const oldItem = collectionItems[index];
|
|
401
|
-
collectionItems[index] = item;
|
|
402
|
-
|
|
403
|
-
// Replace placeholder in DOM if needed
|
|
404
|
-
const wasPlaceholder = oldItem && isPlaceholder(oldItem);
|
|
405
|
-
const hasElement = renderedElements.has(index);
|
|
406
|
-
if (wasPlaceholder && hasElement) {
|
|
407
|
-
replacedCount++;
|
|
408
|
-
const element = renderedElements.get(index);
|
|
409
|
-
if (element) {
|
|
410
|
-
const newElement = renderItem(item, index);
|
|
411
|
-
if (newElement) {
|
|
412
|
-
// Remove placeholder classes from wrapper
|
|
413
|
-
removeClass(newElement, VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS);
|
|
414
|
-
// Also remove from inner element (for string templates)
|
|
415
|
-
if (newElement.firstElementChild) {
|
|
416
|
-
removeClass(
|
|
417
|
-
newElement.firstElementChild as HTMLElement,
|
|
418
|
-
VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS,
|
|
419
|
-
);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Add replaced class for fade-in animation
|
|
423
|
-
addClass(newElement, "viewport-item--replaced");
|
|
424
|
-
|
|
425
|
-
// Copy position and replace
|
|
426
|
-
Object.assign(newElement.style, {
|
|
427
|
-
position: element.style.position,
|
|
428
|
-
transform: element.style.transform,
|
|
429
|
-
width: element.style.width,
|
|
430
|
-
});
|
|
431
|
-
element.parentNode?.replaceChild(newElement, element);
|
|
432
|
-
renderedElements.set(index, newElement);
|
|
433
|
-
releaseElement(element);
|
|
434
|
-
|
|
435
|
-
// Remove the replaced class after animation completes
|
|
436
|
-
setTimeout(() => {
|
|
437
|
-
removeClass(newElement, "viewport-item--replaced");
|
|
438
|
-
}, 300);
|
|
439
|
-
} else {
|
|
440
|
-
// renderItem returned null - still release the old element
|
|
441
|
-
releaseElement(element);
|
|
442
|
-
renderedElements.delete(index);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
} else if (wasPlaceholder && !hasElement) {
|
|
446
|
-
skippedCount++;
|
|
447
|
-
}
|
|
448
|
-
});
|
|
449
|
-
// if (replacedCount > 0 || skippedCount > 0) {
|
|
450
|
-
// console.log(
|
|
451
|
-
// `[RENDER-FIX] Placeholder replacement: ${replacedCount} replaced, ${skippedCount} skipped (not rendered)`,
|
|
452
|
-
// );
|
|
453
|
-
// }
|
|
454
|
-
|
|
455
|
-
// Check if we need to render
|
|
456
|
-
const { visibleRange } = viewportState || {};
|
|
457
|
-
if (visibleRange) {
|
|
458
|
-
const renderStart = Math.max(0, visibleRange.start - overscan);
|
|
459
|
-
const renderEnd = Math.min(
|
|
460
|
-
viewportState?.totalItems ?? 0 - 1,
|
|
461
|
-
visibleRange.end + overscan,
|
|
462
|
-
);
|
|
463
|
-
const loadedStart = data.offset;
|
|
464
|
-
const loadedEnd = data.offset + data.items.length - 1;
|
|
465
|
-
|
|
466
|
-
// Check for placeholders in range
|
|
467
|
-
const hasPlaceholdersInRange = Array.from(
|
|
468
|
-
{ length: Math.min(loadedEnd, renderEnd) - loadedStart + 1 },
|
|
469
|
-
(_, i) => collectionItems[loadedStart + i],
|
|
470
|
-
).some((item) => item && isPlaceholder(item));
|
|
471
|
-
|
|
472
|
-
const needsRender =
|
|
473
|
-
(loadedStart <= renderEnd &&
|
|
474
|
-
loadedEnd >= renderStart &&
|
|
475
|
-
renderedElements.size < renderEnd - renderStart + 1) ||
|
|
476
|
-
hasPlaceholdersInRange;
|
|
477
|
-
|
|
478
|
-
if (needsRender) renderItems();
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Listen for events
|
|
483
|
-
component.on?.("viewport:range-changed", renderItems);
|
|
484
|
-
component.on?.("viewport:scroll", updateItemPositions);
|
|
485
|
-
|
|
486
|
-
// Update items container height when virtual size changes
|
|
487
|
-
component.on?.("viewport:virtual-size-changed", (data: any) => {
|
|
488
|
-
if (
|
|
489
|
-
viewportState?.itemsContainer &&
|
|
490
|
-
data.totalVirtualSize !== undefined
|
|
491
|
-
) {
|
|
492
|
-
viewportState.itemsContainer.style.height = `${data.totalVirtualSize}px`;
|
|
493
|
-
}
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
// Template helpers
|
|
498
|
-
const getDefaultTemplate = () => (item: any, index: number) => {
|
|
499
|
-
// Use layout system for default template
|
|
500
|
-
return [
|
|
501
|
-
{
|
|
502
|
-
tag: "div",
|
|
503
|
-
class: "viewport-item",
|
|
504
|
-
text:
|
|
505
|
-
typeof item === "object"
|
|
506
|
-
? item.name || item.label || item.text || `Item ${index}`
|
|
507
|
-
: String(item),
|
|
508
|
-
},
|
|
509
|
-
];
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
// Process layout schema with item data substitution
|
|
513
|
-
const processLayoutSchema = (
|
|
514
|
-
schema: any,
|
|
515
|
-
item: any,
|
|
516
|
-
index: number,
|
|
517
|
-
): any => {
|
|
518
|
-
if (typeof schema === "string") {
|
|
519
|
-
// Handle variable substitution like {{name}}, {{index}}
|
|
520
|
-
return schema.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
521
|
-
if (key === "index") return String(index);
|
|
522
|
-
if (key === "item") return String(item);
|
|
523
|
-
|
|
524
|
-
// Handle nested properties like {{user.name}}
|
|
525
|
-
const value = key.split(".").reduce((obj: any, prop: string) => {
|
|
526
|
-
return obj?.[prop.trim()];
|
|
527
|
-
}, item);
|
|
528
|
-
|
|
529
|
-
return value !== undefined ? String(value) : match;
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (Array.isArray(schema)) {
|
|
534
|
-
return schema.map((child) => processLayoutSchema(child, item, index));
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
if (typeof schema === "object" && schema !== null) {
|
|
538
|
-
const processed: any = {};
|
|
539
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
540
|
-
processed[key] = processLayoutSchema(value, item, index);
|
|
541
|
-
}
|
|
542
|
-
return processed;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return schema;
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
// Position calculation
|
|
549
|
-
const calculateItemPosition = (
|
|
550
|
-
index: number,
|
|
551
|
-
scrollPosition: number,
|
|
552
|
-
totalItems: number,
|
|
553
|
-
itemSize: number,
|
|
554
|
-
virtualTotalSize: number,
|
|
555
|
-
containerSize: number,
|
|
556
|
-
targetScrollIndex?: number,
|
|
557
|
-
): number => {
|
|
558
|
-
const actualTotalSize = totalItems * itemSize;
|
|
559
|
-
const isCompressed =
|
|
560
|
-
actualTotalSize > virtualTotalSize && virtualTotalSize > 0;
|
|
561
|
-
|
|
562
|
-
// If we have a targetScrollIndex (from initialScrollIndex), use it directly
|
|
563
|
-
// This ensures items are positioned correctly even with compression
|
|
564
|
-
if (targetScrollIndex !== undefined && targetScrollIndex > 0) {
|
|
565
|
-
// Position items relative to the target index
|
|
566
|
-
// The target index should appear at the top of the viewport
|
|
567
|
-
return (index - targetScrollIndex) * itemSize;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (!isCompressed || totalItems === 0) {
|
|
571
|
-
return index * itemSize - scrollPosition;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Compressed space handling
|
|
575
|
-
const maxScrollPosition = virtualTotalSize - containerSize;
|
|
576
|
-
const distanceFromBottom = maxScrollPosition - scrollPosition;
|
|
577
|
-
const nearBottomThreshold = containerSize;
|
|
578
|
-
|
|
579
|
-
if (
|
|
580
|
-
distanceFromBottom <= nearBottomThreshold &&
|
|
581
|
-
distanceFromBottom >= -1
|
|
582
|
-
) {
|
|
583
|
-
// Near bottom interpolation
|
|
584
|
-
const itemsAtBottom = Math.floor(containerSize / itemSize);
|
|
585
|
-
const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
|
|
586
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
587
|
-
const exactScrollIndex = scrollRatio * totalItems;
|
|
588
|
-
const interpolation = Math.max(
|
|
589
|
-
0,
|
|
590
|
-
Math.min(1, 1 - distanceFromBottom / nearBottomThreshold),
|
|
591
|
-
);
|
|
592
|
-
|
|
593
|
-
const bottomPosition = (index - firstVisibleAtBottom) * itemSize;
|
|
594
|
-
const normalPosition = (index - exactScrollIndex) * itemSize;
|
|
595
|
-
return (
|
|
596
|
-
normalPosition + (bottomPosition - normalPosition) * interpolation
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Normal compressed scrolling
|
|
601
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
602
|
-
return (index - scrollRatio * totalItems) * itemSize;
|
|
603
|
-
};
|
|
604
|
-
|
|
605
|
-
// Render single item
|
|
606
|
-
const renderItem = (item: any, index: number): HTMLElement | null => {
|
|
607
|
-
const itemTemplate = template || getDefaultTemplate();
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
const result = itemTemplate(item, index);
|
|
611
|
-
let element: HTMLElement;
|
|
612
|
-
|
|
613
|
-
// Check if result is a layout schema (array or object)
|
|
614
|
-
if (
|
|
615
|
-
Array.isArray(result) ||
|
|
616
|
-
(typeof result === "object" &&
|
|
617
|
-
result !== null &&
|
|
618
|
-
!(result instanceof HTMLElement))
|
|
619
|
-
) {
|
|
620
|
-
// Process schema to substitute variables
|
|
621
|
-
const processedSchema = processLayoutSchema(result, item, index);
|
|
622
|
-
|
|
623
|
-
// Use layout system to create element
|
|
624
|
-
const layoutResult = createLayout(processedSchema);
|
|
625
|
-
element = layoutResult.element;
|
|
626
|
-
|
|
627
|
-
// If the layout created a wrapper, use it directly
|
|
628
|
-
if (element && element.nodeType === 1) {
|
|
629
|
-
// Element is already created by layout system
|
|
630
|
-
// Store layoutResult for cleanup when element is released (prevents memory leak)
|
|
631
|
-
layoutResults.set(element, layoutResult);
|
|
632
|
-
} else {
|
|
633
|
-
// Fallback if layout didn't create a proper element
|
|
634
|
-
element = getPooledElement();
|
|
635
|
-
if (layoutResult.element) {
|
|
636
|
-
element.appendChild(layoutResult.element);
|
|
637
|
-
// Store layoutResult on wrapper element for cleanup
|
|
638
|
-
layoutResults.set(element, layoutResult);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
} else if (typeof result === "string") {
|
|
642
|
-
// Parse HTML using template element - optimized path
|
|
643
|
-
// Move nodes directly instead of cloning - much faster, no memory overhead
|
|
644
|
-
templateParser.innerHTML = result;
|
|
645
|
-
const content = templateParser.content;
|
|
646
|
-
|
|
647
|
-
if (content.children.length === 1) {
|
|
648
|
-
// Single root element - move directly (faster than cloneNode)
|
|
649
|
-
element = content.firstElementChild as HTMLElement;
|
|
650
|
-
content.removeChild(element);
|
|
651
|
-
addClass(element, "mtrl-viewport-item");
|
|
652
|
-
if (isPlaceholder(item)) {
|
|
653
|
-
addClass(element, VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS);
|
|
654
|
-
}
|
|
655
|
-
} else {
|
|
656
|
-
// Multiple children - move all into pooled element
|
|
657
|
-
element = getPooledElement();
|
|
658
|
-
while (content.firstChild) {
|
|
659
|
-
element.appendChild(content.firstChild);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
} else if (result instanceof HTMLElement) {
|
|
663
|
-
element = getPooledElement();
|
|
664
|
-
element.appendChild(result);
|
|
665
|
-
} else {
|
|
666
|
-
console.warn(`[Rendering] Invalid template result for item ${index}`);
|
|
667
|
-
return null;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// Add classes
|
|
671
|
-
if (!hasClass(element, "viewport-item"))
|
|
672
|
-
addClass(element, "viewport-item");
|
|
673
|
-
if (isPlaceholder(item)) {
|
|
674
|
-
addClass(element, VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
element.dataset.index = String(index);
|
|
678
|
-
return element;
|
|
679
|
-
} catch (error) {
|
|
680
|
-
console.error(`[Rendering] Error rendering item ${index}:`, error);
|
|
681
|
-
return null;
|
|
682
|
-
}
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
// Update positions
|
|
686
|
-
const updateItemPositions = (): void => {
|
|
687
|
-
if (!viewportState || renderedElements.size === 0) return;
|
|
688
|
-
|
|
689
|
-
const {
|
|
690
|
-
scrollPosition,
|
|
691
|
-
itemSize: itemSize,
|
|
692
|
-
totalItems,
|
|
693
|
-
virtualTotalSize,
|
|
694
|
-
containerSize,
|
|
695
|
-
} = viewportState;
|
|
696
|
-
const actualTotalSize = totalItems * itemSize;
|
|
697
|
-
const isCompressed =
|
|
698
|
-
actualTotalSize > virtualTotalSize && virtualTotalSize > 0;
|
|
699
|
-
|
|
700
|
-
const sortedIndices = Array.from(renderedElements.keys()).sort(
|
|
701
|
-
(a, b) => a - b,
|
|
702
|
-
);
|
|
703
|
-
if (!sortedIndices.length) return;
|
|
704
|
-
|
|
705
|
-
const firstIndex = sortedIndices[0];
|
|
706
|
-
let currentPosition = 0;
|
|
707
|
-
|
|
708
|
-
if (isCompressed) {
|
|
709
|
-
const maxScroll = virtualTotalSize - containerSize;
|
|
710
|
-
const distanceFromBottom = maxScroll - scrollPosition;
|
|
711
|
-
|
|
712
|
-
if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
|
|
713
|
-
// Near bottom interpolation
|
|
714
|
-
const itemsAtBottom = Math.floor(containerSize / itemSize);
|
|
715
|
-
const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
|
|
716
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
717
|
-
const exactScrollIndex = scrollRatio * totalItems;
|
|
718
|
-
const interpolation = Math.max(
|
|
719
|
-
0,
|
|
720
|
-
Math.min(1, 1 - distanceFromBottom / containerSize),
|
|
721
|
-
);
|
|
722
|
-
|
|
723
|
-
const bottomPos = (firstIndex - firstVisibleAtBottom) * itemSize;
|
|
724
|
-
const normalPos = (firstIndex - exactScrollIndex) * itemSize;
|
|
725
|
-
currentPosition = normalPos + (bottomPos - normalPos) * interpolation;
|
|
726
|
-
} else {
|
|
727
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
728
|
-
currentPosition = (firstIndex - scrollRatio * totalItems) * itemSize;
|
|
729
|
-
}
|
|
730
|
-
} else {
|
|
731
|
-
currentPosition = firstIndex * itemSize - scrollPosition;
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Position each item
|
|
735
|
-
sortedIndices.forEach((index) => {
|
|
736
|
-
const element = renderedElements.get(index);
|
|
737
|
-
if (element) {
|
|
738
|
-
element.style.transform = `translateY(${Math.round(
|
|
739
|
-
currentPosition,
|
|
740
|
-
)}px)`;
|
|
741
|
-
|
|
742
|
-
// Strategic log for last items
|
|
743
|
-
// if (index >= totalItems - 5) {
|
|
744
|
-
// console.log(
|
|
745
|
-
// `[Rendering] Last item positioned: index=${index}, position=${Math.round(
|
|
746
|
-
// currentPosition
|
|
747
|
-
// )}px, itemSize=${itemSize}px, totalItems=${totalItems}`
|
|
748
|
-
// );
|
|
749
|
-
// }
|
|
750
|
-
|
|
751
|
-
currentPosition += itemSize;
|
|
752
|
-
}
|
|
753
|
-
});
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
// Main render function
|
|
757
|
-
const renderItems = () => {
|
|
758
|
-
if (!viewportState?.itemsContainer) {
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
const {
|
|
763
|
-
visibleRange,
|
|
764
|
-
itemsContainer,
|
|
765
|
-
totalItems,
|
|
766
|
-
itemSize,
|
|
767
|
-
scrollPosition,
|
|
768
|
-
containerSize,
|
|
769
|
-
virtualTotalSize,
|
|
770
|
-
} = viewportState;
|
|
771
|
-
|
|
772
|
-
// Validate range - skip rendering if no items or range is invalid
|
|
773
|
-
if (
|
|
774
|
-
!visibleRange ||
|
|
775
|
-
totalItems <= 0 ||
|
|
776
|
-
visibleRange.start < 0 ||
|
|
777
|
-
visibleRange.start >= totalItems ||
|
|
778
|
-
visibleRange.end < visibleRange.start ||
|
|
779
|
-
isNaN(visibleRange.start) ||
|
|
780
|
-
isNaN(visibleRange.end)
|
|
781
|
-
) {
|
|
782
|
-
// If totalItems is 0, clear all rendered elements to remove stale placeholders
|
|
783
|
-
if (totalItems <= 0 && renderedElements.size > 0) {
|
|
784
|
-
Array.from(renderedElements.entries()).forEach(([index, element]) => {
|
|
785
|
-
if (element.parentNode) releaseElement(element);
|
|
786
|
-
renderedElements.delete(index);
|
|
787
|
-
});
|
|
788
|
-
currentVisibleRange = { start: -1, end: -1 };
|
|
789
|
-
}
|
|
790
|
-
return;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// Check if range changed
|
|
794
|
-
if (
|
|
795
|
-
visibleRange.start === currentVisibleRange.start &&
|
|
796
|
-
visibleRange.end === currentVisibleRange.end &&
|
|
797
|
-
renderedElements.size > 0
|
|
798
|
-
) {
|
|
799
|
-
updateItemPositions();
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
lastRenderTime = Date.now();
|
|
804
|
-
const renderStart = Math.max(0, visibleRange.start - overscan);
|
|
805
|
-
const renderEnd = Math.min(totalItems - 1, visibleRange.end + overscan);
|
|
806
|
-
|
|
807
|
-
// if (isRemovingItem) {
|
|
808
|
-
// console.log(
|
|
809
|
-
// `[RENDER-FIX] renderItems calc: visibleRange=${JSON.stringify(visibleRange)}, overscan=${overscan}, totalItems=${totalItems}`,
|
|
810
|
-
// );
|
|
811
|
-
// console.log(
|
|
812
|
-
// `[RENDER-FIX] renderItems calc: renderStart=${renderStart}, renderEnd=${renderEnd}`,
|
|
813
|
-
// );
|
|
814
|
-
// }
|
|
815
|
-
|
|
816
|
-
// Remove items outside range
|
|
817
|
-
Array.from(renderedElements.entries())
|
|
818
|
-
.filter(([index]) => index < renderStart || index > renderEnd)
|
|
819
|
-
.forEach(([index, element]) => {
|
|
820
|
-
if (element.parentNode) releaseElement(element);
|
|
821
|
-
renderedElements.delete(index);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
// Get items source
|
|
825
|
-
const hasCollectionItems = Object.keys(collectionItems).length > 0;
|
|
826
|
-
const items = hasCollectionItems
|
|
827
|
-
? collectionItems
|
|
828
|
-
: component.items || [];
|
|
829
|
-
const missingItems: number[] = [];
|
|
830
|
-
|
|
831
|
-
// Render items in range
|
|
832
|
-
for (let i = renderStart; i <= renderEnd; i++) {
|
|
833
|
-
if (i < 0 || i >= totalItems || renderedElements.has(i)) continue;
|
|
834
|
-
|
|
835
|
-
let item = items[i];
|
|
836
|
-
if (!item) {
|
|
837
|
-
// Only log during removal to avoid spam during scroll
|
|
838
|
-
|
|
839
|
-
missingItems.push(i);
|
|
840
|
-
// Generate placeholder
|
|
841
|
-
const placeholders = (component as any).placeholders;
|
|
842
|
-
item = placeholders?.generatePlaceholderItem(i) || {
|
|
843
|
-
_placeholder: true,
|
|
844
|
-
index: i,
|
|
845
|
-
id: `placeholder-${i}`,
|
|
846
|
-
name: VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(15),
|
|
847
|
-
text: VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(25),
|
|
848
|
-
description:
|
|
849
|
-
VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(40),
|
|
850
|
-
};
|
|
851
|
-
collectionItems[i] = item;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
const element = renderItem(item, i);
|
|
855
|
-
if (element) {
|
|
856
|
-
// Get targetScrollIndex from viewportState if available
|
|
857
|
-
const targetScrollIndex = (viewportState as any)?.targetScrollIndex;
|
|
858
|
-
const position = calculateItemPosition(
|
|
859
|
-
i,
|
|
860
|
-
scrollPosition,
|
|
861
|
-
totalItems,
|
|
862
|
-
itemSize,
|
|
863
|
-
virtualTotalSize,
|
|
864
|
-
containerSize,
|
|
865
|
-
targetScrollIndex,
|
|
866
|
-
);
|
|
867
|
-
|
|
868
|
-
Object.assign(element.style, {
|
|
869
|
-
position: "absolute",
|
|
870
|
-
transform: `translateY(${position}px)`,
|
|
871
|
-
width: "100%",
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
itemsContainer.appendChild(element);
|
|
875
|
-
renderedElements.set(i, element);
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
// Request missing items
|
|
880
|
-
if (
|
|
881
|
-
missingItems.length > 0 &&
|
|
882
|
-
component.viewport?.collection?.loadMissingRanges
|
|
883
|
-
) {
|
|
884
|
-
component.viewport.collection.loadMissingRanges(
|
|
885
|
-
{
|
|
886
|
-
start: Math.min(...missingItems),
|
|
887
|
-
end: Math.max(...missingItems),
|
|
888
|
-
},
|
|
889
|
-
"rendering:missing-items",
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
currentVisibleRange = visibleRange;
|
|
894
|
-
|
|
895
|
-
// Log DOM stats periodically (every 10 renders)
|
|
896
|
-
if (poolStats.created % 50 === 0 && poolStats.created > 0) {
|
|
897
|
-
logDOMStats(`render:${poolStats.created}`);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Emit items rendered event with elements for size calculation
|
|
901
|
-
const renderedElementsArray = Array.from(renderedElements.values());
|
|
902
|
-
component.emit?.("viewport:items-rendered", {
|
|
903
|
-
elements: renderedElementsArray,
|
|
904
|
-
range: visibleRange,
|
|
905
|
-
});
|
|
906
|
-
|
|
907
|
-
component.emit?.("viewport:rendered", {
|
|
908
|
-
range: visibleRange,
|
|
909
|
-
renderedCount: renderedElements.size,
|
|
910
|
-
});
|
|
911
|
-
updateItemPositions();
|
|
912
|
-
|
|
913
|
-
// Load data if no items rendered
|
|
914
|
-
if (
|
|
915
|
-
renderedElements.size === 0 &&
|
|
916
|
-
totalItems > 0 &&
|
|
917
|
-
component.viewport?.collection
|
|
918
|
-
) {
|
|
919
|
-
component.viewport.collection.loadMissingRanges(
|
|
920
|
-
visibleRange,
|
|
921
|
-
"rendering:no-items",
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
// Extend viewport API
|
|
927
|
-
const originalRenderItems = component.viewport.renderItems;
|
|
928
|
-
component.viewport.renderItems = () => {
|
|
929
|
-
renderItems();
|
|
930
|
-
originalRenderItems?.();
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
// Cleanup
|
|
934
|
-
wrapDestroy(component, () => {
|
|
935
|
-
// Release all rendered elements - use releaseElement to properly destroy layoutResults
|
|
936
|
-
renderedElements.forEach((element) => {
|
|
937
|
-
releaseElement(element);
|
|
938
|
-
});
|
|
939
|
-
renderedElements.clear();
|
|
940
|
-
|
|
941
|
-
// Clear element pool
|
|
942
|
-
elementPool.length = 0;
|
|
943
|
-
|
|
944
|
-
// Clear collection items cache - this is critical for memory!
|
|
945
|
-
for (const key in collectionItems) {
|
|
946
|
-
delete collectionItems[key];
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
// Reset state
|
|
950
|
-
currentVisibleRange = { start: -1, end: -1 };
|
|
951
|
-
viewportState = null;
|
|
952
|
-
lastRenderTime = 0;
|
|
953
|
-
|
|
954
|
-
// Reset pool stats
|
|
955
|
-
poolStats.created = 0;
|
|
956
|
-
poolStats.recycled = 0;
|
|
957
|
-
poolStats.poolSize = 0;
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
return component;
|
|
961
|
-
};
|
|
962
|
-
};
|