mtrl-addons 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -9
- 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 +6 -1
- package/src/styles/components/_vlist.scss +234 -213
- package/.cursorrules +0 -117
- package/AI.md +0 -241
- package/build.js +0 -201
- 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 -322
- package/src/components/vlist/features/selection.ts +0 -444
- package/src/components/vlist/features/viewport.ts +0 -65
- package/src/components/vlist/features.ts +0 -112
- package/src/components/vlist/types.ts +0 -591
- 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 -1001
- package/src/core/layout/types.ts +0 -95
- package/src/core/viewport/constants.ts +0 -140
- package/src/core/viewport/features/base.ts +0 -73
- package/src/core/viewport/features/collection.ts +0 -882
- 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 -260
- 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 -568
- package/src/core/viewport/features/scrollbar.ts +0 -434
- package/src/core/viewport/features/scrolling.ts +0 -618
- package/src/core/viewport/features/utils.ts +0 -88
- package/src/core/viewport/features/virtual.ts +0 -384
- 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 -246
- 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,568 +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 };
|
|
56
|
-
|
|
57
|
-
let viewportState: ViewportState | null = null;
|
|
58
|
-
let currentVisibleRange = { start: 0, end: 0 };
|
|
59
|
-
let lastRenderTime = 0;
|
|
60
|
-
|
|
61
|
-
// Element pool management
|
|
62
|
-
const getPooledElement = (): HTMLElement => {
|
|
63
|
-
if (enableRecycling && elementPool.length > 0) {
|
|
64
|
-
poolStats.recycled++;
|
|
65
|
-
return elementPool.pop()!;
|
|
66
|
-
}
|
|
67
|
-
const element = document.createElement("div");
|
|
68
|
-
element.className = "mtrl-viewport-item";
|
|
69
|
-
poolStats.created++;
|
|
70
|
-
return element;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const releaseElement = (element: HTMLElement): void => {
|
|
74
|
-
if (!enableRecycling) {
|
|
75
|
-
element.remove();
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// Clean and clone for reuse
|
|
79
|
-
element.className = "mtrl-viewport-item";
|
|
80
|
-
element.removeAttribute("data-index");
|
|
81
|
-
element.style.cssText = "";
|
|
82
|
-
element.innerHTML = "";
|
|
83
|
-
const cleanElement = element.cloneNode(false) as HTMLElement;
|
|
84
|
-
|
|
85
|
-
if (elementPool.length < maxPoolSize) {
|
|
86
|
-
elementPool.push(cleanElement);
|
|
87
|
-
poolStats.poolSize = elementPool.length;
|
|
88
|
-
}
|
|
89
|
-
element.remove();
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Initialize
|
|
93
|
-
wrapInitialize(component, () => {
|
|
94
|
-
viewportState = getViewportState(component) as ViewportState;
|
|
95
|
-
|
|
96
|
-
// Listen for collection data loaded
|
|
97
|
-
component.on?.("collection:range-loaded", (data: any) => {
|
|
98
|
-
if (!data.items?.length) return;
|
|
99
|
-
|
|
100
|
-
// Analyze data structure on first load
|
|
101
|
-
const placeholders = (component as any).placeholders;
|
|
102
|
-
if (placeholders && !placeholders.hasAnalyzedStructure()) {
|
|
103
|
-
placeholders.analyzeDataStructure(data.items);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Update collection items and replace placeholders
|
|
107
|
-
data.items.forEach((item: any, i: number) => {
|
|
108
|
-
const index = data.offset + i;
|
|
109
|
-
const oldItem = collectionItems[index];
|
|
110
|
-
collectionItems[index] = item;
|
|
111
|
-
|
|
112
|
-
// Replace placeholder in DOM if needed
|
|
113
|
-
if (
|
|
114
|
-
oldItem &&
|
|
115
|
-
isPlaceholder(oldItem) &&
|
|
116
|
-
renderedElements.has(index)
|
|
117
|
-
) {
|
|
118
|
-
const element = renderedElements.get(index);
|
|
119
|
-
if (element) {
|
|
120
|
-
const newElement = renderItem(item, index);
|
|
121
|
-
if (newElement) {
|
|
122
|
-
// Remove placeholder classes
|
|
123
|
-
removeClass(newElement, VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS);
|
|
124
|
-
|
|
125
|
-
// Add replaced class for fade-in animation
|
|
126
|
-
addClass(newElement, "viewport-item--replaced");
|
|
127
|
-
|
|
128
|
-
// Copy position and replace
|
|
129
|
-
Object.assign(newElement.style, {
|
|
130
|
-
position: element.style.position,
|
|
131
|
-
transform: element.style.transform,
|
|
132
|
-
width: element.style.width,
|
|
133
|
-
});
|
|
134
|
-
element.parentNode?.replaceChild(newElement, element);
|
|
135
|
-
renderedElements.set(index, newElement);
|
|
136
|
-
releaseElement(element);
|
|
137
|
-
|
|
138
|
-
// Remove the replaced class after animation completes
|
|
139
|
-
setTimeout(() => {
|
|
140
|
-
removeClass(newElement, "viewport-item--replaced");
|
|
141
|
-
}, 300);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
// Check if we need to render
|
|
148
|
-
const { visibleRange } = viewportState || {};
|
|
149
|
-
if (visibleRange) {
|
|
150
|
-
const renderStart = Math.max(0, visibleRange.start - overscan);
|
|
151
|
-
const renderEnd = Math.min(
|
|
152
|
-
viewportState?.totalItems ?? 0 - 1,
|
|
153
|
-
visibleRange.end + overscan
|
|
154
|
-
);
|
|
155
|
-
const loadedStart = data.offset;
|
|
156
|
-
const loadedEnd = data.offset + data.items.length - 1;
|
|
157
|
-
|
|
158
|
-
// Check for placeholders in range
|
|
159
|
-
const hasPlaceholdersInRange = Array.from(
|
|
160
|
-
{ length: Math.min(loadedEnd, renderEnd) - loadedStart + 1 },
|
|
161
|
-
(_, i) => collectionItems[loadedStart + i]
|
|
162
|
-
).some((item) => item && isPlaceholder(item));
|
|
163
|
-
|
|
164
|
-
const needsRender =
|
|
165
|
-
(loadedStart <= renderEnd &&
|
|
166
|
-
loadedEnd >= renderStart &&
|
|
167
|
-
renderedElements.size < renderEnd - renderStart + 1) ||
|
|
168
|
-
hasPlaceholdersInRange;
|
|
169
|
-
|
|
170
|
-
if (needsRender) renderItems();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Listen for events
|
|
175
|
-
component.on?.("viewport:range-changed", renderItems);
|
|
176
|
-
component.on?.("viewport:scroll", updateItemPositions);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// Template helpers
|
|
180
|
-
const getDefaultTemplate = () => (item: any, index: number) => {
|
|
181
|
-
// Use layout system for default template
|
|
182
|
-
return [
|
|
183
|
-
{
|
|
184
|
-
tag: "div",
|
|
185
|
-
class: "viewport-item",
|
|
186
|
-
text:
|
|
187
|
-
typeof item === "object"
|
|
188
|
-
? item.name || item.label || item.text || `Item ${index}`
|
|
189
|
-
: String(item),
|
|
190
|
-
},
|
|
191
|
-
];
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// Process layout schema with item data substitution
|
|
195
|
-
const processLayoutSchema = (
|
|
196
|
-
schema: any,
|
|
197
|
-
item: any,
|
|
198
|
-
index: number
|
|
199
|
-
): any => {
|
|
200
|
-
if (typeof schema === "string") {
|
|
201
|
-
// Handle variable substitution like {{name}}, {{index}}
|
|
202
|
-
return schema.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
|
203
|
-
if (key === "index") return String(index);
|
|
204
|
-
if (key === "item") return String(item);
|
|
205
|
-
|
|
206
|
-
// Handle nested properties like {{user.name}}
|
|
207
|
-
const value = key.split(".").reduce((obj: any, prop: string) => {
|
|
208
|
-
return obj?.[prop.trim()];
|
|
209
|
-
}, item);
|
|
210
|
-
|
|
211
|
-
return value !== undefined ? String(value) : match;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (Array.isArray(schema)) {
|
|
216
|
-
return schema.map((child) => processLayoutSchema(child, item, index));
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (typeof schema === "object" && schema !== null) {
|
|
220
|
-
const processed: any = {};
|
|
221
|
-
for (const [key, value] of Object.entries(schema)) {
|
|
222
|
-
processed[key] = processLayoutSchema(value, item, index);
|
|
223
|
-
}
|
|
224
|
-
return processed;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return schema;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Position calculation
|
|
231
|
-
const calculateItemPosition = (
|
|
232
|
-
index: number,
|
|
233
|
-
scrollPosition: number,
|
|
234
|
-
totalItems: number,
|
|
235
|
-
itemSize: number,
|
|
236
|
-
virtualTotalSize: number,
|
|
237
|
-
containerSize: number
|
|
238
|
-
): number => {
|
|
239
|
-
const actualTotalSize = totalItems * itemSize;
|
|
240
|
-
const isCompressed =
|
|
241
|
-
actualTotalSize > virtualTotalSize && virtualTotalSize > 0;
|
|
242
|
-
|
|
243
|
-
if (!isCompressed || totalItems === 0) {
|
|
244
|
-
return index * itemSize - scrollPosition;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// Compressed space handling
|
|
248
|
-
const maxScrollPosition = virtualTotalSize - containerSize;
|
|
249
|
-
const distanceFromBottom = maxScrollPosition - scrollPosition;
|
|
250
|
-
const nearBottomThreshold = containerSize;
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
distanceFromBottom <= nearBottomThreshold &&
|
|
254
|
-
distanceFromBottom >= -1
|
|
255
|
-
) {
|
|
256
|
-
// Near bottom interpolation
|
|
257
|
-
const itemsAtBottom = Math.floor(containerSize / itemSize);
|
|
258
|
-
const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
|
|
259
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
260
|
-
const exactScrollIndex = scrollRatio * totalItems;
|
|
261
|
-
const interpolation = Math.max(
|
|
262
|
-
0,
|
|
263
|
-
Math.min(1, 1 - distanceFromBottom / nearBottomThreshold)
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
const bottomPosition = (index - firstVisibleAtBottom) * itemSize;
|
|
267
|
-
const normalPosition = (index - exactScrollIndex) * itemSize;
|
|
268
|
-
return (
|
|
269
|
-
normalPosition + (bottomPosition - normalPosition) * interpolation
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Normal compressed scrolling
|
|
274
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
275
|
-
return (index - scrollRatio * totalItems) * itemSize;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// Render single item
|
|
279
|
-
const renderItem = (item: any, index: number): HTMLElement | null => {
|
|
280
|
-
const itemTemplate = template || getDefaultTemplate();
|
|
281
|
-
|
|
282
|
-
try {
|
|
283
|
-
const result = itemTemplate(item, index);
|
|
284
|
-
let element: HTMLElement;
|
|
285
|
-
|
|
286
|
-
// Check if result is a layout schema (array or object)
|
|
287
|
-
if (
|
|
288
|
-
Array.isArray(result) ||
|
|
289
|
-
(typeof result === "object" &&
|
|
290
|
-
result !== null &&
|
|
291
|
-
!(result instanceof HTMLElement))
|
|
292
|
-
) {
|
|
293
|
-
// Process schema to substitute variables
|
|
294
|
-
const processedSchema = processLayoutSchema(result, item, index);
|
|
295
|
-
|
|
296
|
-
// Use layout system to create element
|
|
297
|
-
const layoutResult = createLayout(processedSchema);
|
|
298
|
-
element = layoutResult.element;
|
|
299
|
-
|
|
300
|
-
// If the layout created a wrapper, use it directly
|
|
301
|
-
if (element && element.nodeType === 1) {
|
|
302
|
-
// Element is already created by layout system
|
|
303
|
-
} else {
|
|
304
|
-
// Fallback if layout didn't create a proper element
|
|
305
|
-
element = getPooledElement();
|
|
306
|
-
if (layoutResult.element) {
|
|
307
|
-
element.appendChild(layoutResult.element);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
} else if (typeof result === "string") {
|
|
311
|
-
element = getPooledElement();
|
|
312
|
-
element.innerHTML = result;
|
|
313
|
-
if (element.children.length === 1) {
|
|
314
|
-
addClass(element.firstElementChild as HTMLElement, "viewport-item");
|
|
315
|
-
}
|
|
316
|
-
} else if (result instanceof HTMLElement) {
|
|
317
|
-
element = getPooledElement();
|
|
318
|
-
element.appendChild(result);
|
|
319
|
-
} else {
|
|
320
|
-
console.warn(`[Rendering] Invalid template result for item ${index}`);
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Add classes
|
|
325
|
-
if (!hasClass(element, "viewport-item"))
|
|
326
|
-
addClass(element, "viewport-item");
|
|
327
|
-
if (isPlaceholder(item)) {
|
|
328
|
-
addClass(element, VIEWPORT_CONSTANTS.PLACEHOLDER.CLASS);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
element.dataset.index = String(index);
|
|
332
|
-
return element;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
console.error(`[Rendering] Error rendering item ${index}:`, error);
|
|
335
|
-
return null;
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// Update positions
|
|
340
|
-
const updateItemPositions = (): void => {
|
|
341
|
-
if (!viewportState || renderedElements.size === 0) return;
|
|
342
|
-
|
|
343
|
-
const {
|
|
344
|
-
scrollPosition,
|
|
345
|
-
itemSize: itemSize,
|
|
346
|
-
totalItems,
|
|
347
|
-
virtualTotalSize,
|
|
348
|
-
containerSize,
|
|
349
|
-
} = viewportState;
|
|
350
|
-
const actualTotalSize = totalItems * itemSize;
|
|
351
|
-
const isCompressed =
|
|
352
|
-
actualTotalSize > virtualTotalSize && virtualTotalSize > 0;
|
|
353
|
-
|
|
354
|
-
const sortedIndices = Array.from(renderedElements.keys()).sort(
|
|
355
|
-
(a, b) => a - b
|
|
356
|
-
);
|
|
357
|
-
if (!sortedIndices.length) return;
|
|
358
|
-
|
|
359
|
-
const firstIndex = sortedIndices[0];
|
|
360
|
-
let currentPosition = 0;
|
|
361
|
-
|
|
362
|
-
if (isCompressed) {
|
|
363
|
-
const maxScroll = virtualTotalSize - containerSize;
|
|
364
|
-
const distanceFromBottom = maxScroll - scrollPosition;
|
|
365
|
-
|
|
366
|
-
if (distanceFromBottom <= containerSize && distanceFromBottom >= -1) {
|
|
367
|
-
// Near bottom interpolation
|
|
368
|
-
const itemsAtBottom = Math.floor(containerSize / itemSize);
|
|
369
|
-
const firstVisibleAtBottom = Math.max(0, totalItems - itemsAtBottom);
|
|
370
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
371
|
-
const exactScrollIndex = scrollRatio * totalItems;
|
|
372
|
-
const interpolation = Math.max(
|
|
373
|
-
0,
|
|
374
|
-
Math.min(1, 1 - distanceFromBottom / containerSize)
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
const bottomPos = (firstIndex - firstVisibleAtBottom) * itemSize;
|
|
378
|
-
const normalPos = (firstIndex - exactScrollIndex) * itemSize;
|
|
379
|
-
currentPosition = normalPos + (bottomPos - normalPos) * interpolation;
|
|
380
|
-
} else {
|
|
381
|
-
const scrollRatio = scrollPosition / virtualTotalSize;
|
|
382
|
-
currentPosition = (firstIndex - scrollRatio * totalItems) * itemSize;
|
|
383
|
-
}
|
|
384
|
-
} else {
|
|
385
|
-
currentPosition = firstIndex * itemSize - scrollPosition;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Position each item
|
|
389
|
-
sortedIndices.forEach((index) => {
|
|
390
|
-
const element = renderedElements.get(index);
|
|
391
|
-
if (element) {
|
|
392
|
-
element.style.transform = `translateY(${Math.round(
|
|
393
|
-
currentPosition
|
|
394
|
-
)}px)`;
|
|
395
|
-
|
|
396
|
-
// Strategic log for last items
|
|
397
|
-
// if (index >= totalItems - 5) {
|
|
398
|
-
// console.log(
|
|
399
|
-
// `[Rendering] Last item positioned: index=${index}, position=${Math.round(
|
|
400
|
-
// currentPosition
|
|
401
|
-
// )}px, itemSize=${itemSize}px, totalItems=${totalItems}`
|
|
402
|
-
// );
|
|
403
|
-
// }
|
|
404
|
-
|
|
405
|
-
currentPosition += itemSize;
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
// Main render function
|
|
411
|
-
const renderItems = () => {
|
|
412
|
-
if (!viewportState?.itemsContainer) return;
|
|
413
|
-
|
|
414
|
-
const {
|
|
415
|
-
visibleRange,
|
|
416
|
-
itemsContainer,
|
|
417
|
-
totalItems,
|
|
418
|
-
itemSize,
|
|
419
|
-
scrollPosition,
|
|
420
|
-
containerSize,
|
|
421
|
-
virtualTotalSize,
|
|
422
|
-
} = viewportState;
|
|
423
|
-
|
|
424
|
-
// Validate range
|
|
425
|
-
if (
|
|
426
|
-
!visibleRange ||
|
|
427
|
-
visibleRange.start < 0 ||
|
|
428
|
-
visibleRange.start >= totalItems ||
|
|
429
|
-
visibleRange.end < visibleRange.start ||
|
|
430
|
-
isNaN(visibleRange.start) ||
|
|
431
|
-
isNaN(visibleRange.end)
|
|
432
|
-
) {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Check if range changed
|
|
437
|
-
if (
|
|
438
|
-
visibleRange.start === currentVisibleRange.start &&
|
|
439
|
-
visibleRange.end === currentVisibleRange.end &&
|
|
440
|
-
renderedElements.size > 0
|
|
441
|
-
) {
|
|
442
|
-
updateItemPositions();
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
lastRenderTime = Date.now();
|
|
447
|
-
const renderStart = Math.max(0, visibleRange.start - overscan);
|
|
448
|
-
const renderEnd = Math.min(totalItems - 1, visibleRange.end + overscan);
|
|
449
|
-
|
|
450
|
-
// Remove items outside range
|
|
451
|
-
Array.from(renderedElements.entries())
|
|
452
|
-
.filter(([index]) => index < renderStart || index > renderEnd)
|
|
453
|
-
.forEach(([index, element]) => {
|
|
454
|
-
if (element.parentNode) releaseElement(element);
|
|
455
|
-
renderedElements.delete(index);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
// Get items source
|
|
459
|
-
const hasCollectionItems = Object.keys(collectionItems).length > 0;
|
|
460
|
-
const items = hasCollectionItems
|
|
461
|
-
? collectionItems
|
|
462
|
-
: component.items || [];
|
|
463
|
-
const missingItems: number[] = [];
|
|
464
|
-
|
|
465
|
-
// Render items in range
|
|
466
|
-
for (let i = renderStart; i <= renderEnd; i++) {
|
|
467
|
-
if (i < 0 || i >= totalItems || renderedElements.has(i)) continue;
|
|
468
|
-
|
|
469
|
-
let item = items[i];
|
|
470
|
-
if (!item) {
|
|
471
|
-
missingItems.push(i);
|
|
472
|
-
// Generate placeholder
|
|
473
|
-
const placeholders = (component as any).placeholders;
|
|
474
|
-
item = placeholders?.generatePlaceholderItem(i) || {
|
|
475
|
-
_placeholder: true,
|
|
476
|
-
index: i,
|
|
477
|
-
id: `placeholder-${i}`,
|
|
478
|
-
name: VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(15),
|
|
479
|
-
text: VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(25),
|
|
480
|
-
description:
|
|
481
|
-
VIEWPORT_CONSTANTS.PLACEHOLDER.MASK_CHARACTER.repeat(40),
|
|
482
|
-
};
|
|
483
|
-
collectionItems[i] = item;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const element = renderItem(item, i);
|
|
487
|
-
if (element) {
|
|
488
|
-
const position = calculateItemPosition(
|
|
489
|
-
i,
|
|
490
|
-
scrollPosition,
|
|
491
|
-
totalItems,
|
|
492
|
-
itemSize,
|
|
493
|
-
virtualTotalSize,
|
|
494
|
-
containerSize
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
Object.assign(element.style, {
|
|
498
|
-
position: "absolute",
|
|
499
|
-
transform: `translateY(${position}px)`,
|
|
500
|
-
width: "100%",
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
itemsContainer.appendChild(element);
|
|
504
|
-
renderedElements.set(i, element);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Request missing items
|
|
509
|
-
if (
|
|
510
|
-
missingItems.length > 0 &&
|
|
511
|
-
component.viewport?.collection?.loadMissingRanges
|
|
512
|
-
) {
|
|
513
|
-
component.viewport.collection.loadMissingRanges(
|
|
514
|
-
{
|
|
515
|
-
start: Math.min(...missingItems),
|
|
516
|
-
end: Math.max(...missingItems),
|
|
517
|
-
},
|
|
518
|
-
"rendering:missing-items"
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
currentVisibleRange = visibleRange;
|
|
523
|
-
|
|
524
|
-
// Emit items rendered event with elements for size calculation
|
|
525
|
-
const renderedElementsArray = Array.from(renderedElements.values());
|
|
526
|
-
component.emit?.("viewport:items-rendered", {
|
|
527
|
-
elements: renderedElementsArray,
|
|
528
|
-
range: visibleRange,
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
component.emit?.("viewport:rendered", {
|
|
532
|
-
range: visibleRange,
|
|
533
|
-
renderedCount: renderedElements.size,
|
|
534
|
-
});
|
|
535
|
-
updateItemPositions();
|
|
536
|
-
|
|
537
|
-
// Load data if no items rendered
|
|
538
|
-
if (
|
|
539
|
-
renderedElements.size === 0 &&
|
|
540
|
-
totalItems > 0 &&
|
|
541
|
-
component.viewport?.collection
|
|
542
|
-
) {
|
|
543
|
-
component.viewport.collection.loadMissingRanges(
|
|
544
|
-
visibleRange,
|
|
545
|
-
"rendering:no-items"
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
|
|
550
|
-
// Extend viewport API
|
|
551
|
-
const originalRenderItems = component.viewport.renderItems;
|
|
552
|
-
component.viewport.renderItems = () => {
|
|
553
|
-
renderItems();
|
|
554
|
-
originalRenderItems?.();
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
// Cleanup
|
|
558
|
-
wrapDestroy(component, () => {
|
|
559
|
-
renderedElements.forEach((element) => {
|
|
560
|
-
if (element.parentNode) releaseElement(element);
|
|
561
|
-
});
|
|
562
|
-
renderedElements.clear();
|
|
563
|
-
elementPool.length = 0;
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
return component;
|
|
567
|
-
};
|
|
568
|
-
};
|