@humanspeak/svelte-virtual-list 0.2.6-beta.0 → 0.2.6-beta.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/dist/SvelteVirtualList.svelte +159 -208
- package/dist/utils/heightCalculation.d.ts +2 -1
- package/dist/utils/heightCalculation.js +8 -5
- package/dist/utils/initialization.d.ts +103 -0
- package/dist/utils/initialization.js +114 -0
- package/dist/utils/resizeObserver.d.ts +122 -0
- package/dist/utils/resizeObserver.js +176 -0
- package/dist/utils/scrollCalculation.d.ts +47 -0
- package/dist/utils/scrollCalculation.js +173 -0
- package/dist/utils/virtualList.d.ts +2 -1
- package/dist/utils/virtualList.js +107 -23
- package/package.json +4 -3
|
@@ -35,6 +35,17 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
35
35
|
if (mode === 'bottomToTop') {
|
|
36
36
|
const visibleCount = Math.ceil(viewportHeight / itemHeight) + 1;
|
|
37
37
|
const bottomIndex = totalItems - Math.floor(scrollTop / itemHeight);
|
|
38
|
+
// Safeguard: if bottomIndex is negative, it means scrollTop is too large for current itemHeight
|
|
39
|
+
// This can happen when itemHeight changes but scrollTop hasn't been corrected yet
|
|
40
|
+
if (bottomIndex < 0) {
|
|
41
|
+
// Calculate what scrollTop should be and use that for visible range
|
|
42
|
+
const totalHeight = totalItems * itemHeight;
|
|
43
|
+
const correctedScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
44
|
+
const correctedBottomIndex = totalItems - Math.floor(correctedScrollTop / itemHeight);
|
|
45
|
+
const start = Math.max(0, correctedBottomIndex - visibleCount - bufferSize);
|
|
46
|
+
const end = Math.min(totalItems, correctedBottomIndex + bufferSize);
|
|
47
|
+
return { start, end };
|
|
48
|
+
}
|
|
38
49
|
// Add buffer to both ends
|
|
39
50
|
const start = Math.max(0, bottomIndex - visibleCount - bufferSize);
|
|
40
51
|
const end = Math.min(totalItems, bottomIndex + bufferSize);
|
|
@@ -43,6 +54,23 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
43
54
|
else {
|
|
44
55
|
const start = Math.floor(scrollTop / itemHeight);
|
|
45
56
|
const end = Math.min(totalItems, start + Math.ceil(viewportHeight / itemHeight) + 1);
|
|
57
|
+
// Safeguard for topToBottom: ensure last item is fully visible when at max scroll
|
|
58
|
+
const totalHeight = totalItems * itemHeight;
|
|
59
|
+
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
60
|
+
// Add dynamic tolerance based on item height for browser rendering precision
|
|
61
|
+
const tolerance = Math.max(itemHeight, 10); // At least one full item height or 10px minimum
|
|
62
|
+
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
63
|
+
if (isAtBottom) {
|
|
64
|
+
// When at the bottom, ensure we include all items up to the end
|
|
65
|
+
const adjustedEnd = totalItems;
|
|
66
|
+
const visibleItemCount = Math.ceil(viewportHeight / itemHeight) + bufferSize + 1;
|
|
67
|
+
const adjustedStart = Math.max(0, adjustedEnd - visibleItemCount);
|
|
68
|
+
// TopToBottom safeguard is now active
|
|
69
|
+
return {
|
|
70
|
+
start: adjustedStart,
|
|
71
|
+
end: adjustedEnd
|
|
72
|
+
};
|
|
73
|
+
}
|
|
46
74
|
// Add buffer to both ends
|
|
47
75
|
return {
|
|
48
76
|
start: Math.max(0, start - bufferSize),
|
|
@@ -65,9 +93,15 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
65
93
|
* @returns {number} The calculated transform Y value in pixels
|
|
66
94
|
*/
|
|
67
95
|
export const calculateTransformY = (mode, totalItems, visibleEnd, visibleStart, itemHeight) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
if (mode === 'bottomToTop') {
|
|
97
|
+
// In bottomToTop mode, we need to position the container so that
|
|
98
|
+
// the first visible item (visibleStart) aligns with its correct position
|
|
99
|
+
// from the bottom of the total content
|
|
100
|
+
return (totalItems - visibleEnd) * itemHeight;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
return visibleStart * itemHeight;
|
|
104
|
+
}
|
|
71
105
|
};
|
|
72
106
|
/**
|
|
73
107
|
* Updates the virtual list's height and scroll position when necessary.
|
|
@@ -124,39 +158,89 @@ export const updateHeightAndScroll = (state, setters, immediate = false) => {
|
|
|
124
158
|
* 40
|
|
125
159
|
* )
|
|
126
160
|
*/
|
|
127
|
-
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight) => {
|
|
161
|
+
export const calculateAverageHeight = (itemElements, visibleRange, heightCache, currentItemHeight, dirtyItems) => {
|
|
128
162
|
const validElements = itemElements.filter((el) => el);
|
|
129
163
|
if (validElements.length === 0) {
|
|
130
164
|
return {
|
|
131
165
|
newHeight: currentItemHeight,
|
|
132
166
|
newLastMeasuredIndex: visibleRange.start,
|
|
133
|
-
updatedHeightCache: heightCache
|
|
167
|
+
updatedHeightCache: heightCache,
|
|
168
|
+
clearedDirtyItems: new Set()
|
|
134
169
|
};
|
|
135
170
|
}
|
|
136
171
|
const newHeightCache = { ...heightCache };
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
const clearedDirtyItems = new Set();
|
|
173
|
+
// Initialize running totals for O(1) average calculation
|
|
174
|
+
let totalValidHeight = 0;
|
|
175
|
+
let validHeightCount = 0;
|
|
176
|
+
// Calculate initial totals from existing cache
|
|
177
|
+
for (const height of Object.values(heightCache)) {
|
|
178
|
+
if (Number.isFinite(height) && height > 0) {
|
|
179
|
+
totalValidHeight += height;
|
|
180
|
+
validHeightCount++;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Process only dirty items if they exist, otherwise process all visible items
|
|
184
|
+
if (dirtyItems.size > 0) {
|
|
185
|
+
// Process only dirty items
|
|
186
|
+
dirtyItems.forEach((itemIndex) => {
|
|
187
|
+
const elementIndex = itemIndex - visibleRange.start;
|
|
188
|
+
const element = validElements[elementIndex];
|
|
189
|
+
if (element && elementIndex >= 0 && elementIndex < validElements.length) {
|
|
190
|
+
try {
|
|
191
|
+
const height = element.getBoundingClientRect().height;
|
|
192
|
+
if (Number.isFinite(height) && height > 0) {
|
|
193
|
+
const oldHeight = newHeightCache[itemIndex];
|
|
194
|
+
// Only update if height actually changed (use smaller tolerance for precision)
|
|
195
|
+
if (!oldHeight || Math.abs(oldHeight - height) >= 0.1) {
|
|
196
|
+
// Update running totals
|
|
197
|
+
if (oldHeight && Number.isFinite(oldHeight) && oldHeight > 0) {
|
|
198
|
+
// Replace old height with new height in running total
|
|
199
|
+
totalValidHeight = totalValidHeight - oldHeight + height;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// Add new height to running total
|
|
203
|
+
totalValidHeight += height;
|
|
204
|
+
validHeightCount++;
|
|
205
|
+
}
|
|
206
|
+
newHeightCache[itemIndex] = height;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
clearedDirtyItems.add(itemIndex);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// Skip invalid measurements but still clear from dirty
|
|
213
|
+
clearedDirtyItems.add(itemIndex);
|
|
145
214
|
}
|
|
146
215
|
}
|
|
147
|
-
|
|
148
|
-
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// Original behavior: process all visible items
|
|
220
|
+
validElements.forEach((el, i) => {
|
|
221
|
+
const itemIndex = visibleRange.start + i;
|
|
222
|
+
if (!newHeightCache[itemIndex]) {
|
|
223
|
+
try {
|
|
224
|
+
const height = el.getBoundingClientRect().height;
|
|
225
|
+
if (Number.isFinite(height) && height > 0) {
|
|
226
|
+
// Add new height to running totals
|
|
227
|
+
totalValidHeight += height;
|
|
228
|
+
validHeightCount++;
|
|
229
|
+
newHeightCache[itemIndex] = height;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Skip invalid measurements
|
|
234
|
+
}
|
|
149
235
|
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
//
|
|
153
|
-
const validHeights = Object.values(newHeightCache).filter((h) => Number.isFinite(h) && h > 0);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// O(1) average calculation using running totals!
|
|
154
239
|
return {
|
|
155
|
-
newHeight:
|
|
156
|
-
? validHeights.reduce((sum, h) => sum + h, 0) / validHeights.length
|
|
157
|
-
: currentItemHeight,
|
|
240
|
+
newHeight: validHeightCount > 0 ? totalValidHeight / validHeightCount : currentItemHeight,
|
|
158
241
|
newLastMeasuredIndex: visibleRange.start,
|
|
159
|
-
updatedHeightCache: newHeightCache
|
|
242
|
+
updatedHeightCache: newHeightCache,
|
|
243
|
+
clearedDirtyItems
|
|
160
244
|
};
|
|
161
245
|
};
|
|
162
246
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.2.6-beta.
|
|
3
|
+
"version": "0.2.6-beta.2",
|
|
4
4
|
"description": "A lightweight, high-performance virtual list component for Svelte 5 that renders large datasets with minimal memory usage. Features include dynamic height support, smooth scrolling, TypeScript support, and efficient DOM recycling. Ideal for infinite scrolling lists, data tables, chat interfaces, and any application requiring the rendering of thousands of items without compromising performance. Zero dependencies and fully customizable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"lint": "prettier --check . && eslint .",
|
|
56
56
|
"lint:fix": "npm run format && eslint . --fix",
|
|
57
57
|
"package": "svelte-kit sync && svelte-package && publint",
|
|
58
|
+
"prepare": "husky",
|
|
58
59
|
"prepublishOnly": "npm run package",
|
|
59
60
|
"preview": "vite preview",
|
|
60
61
|
"test": "vitest run --coverage",
|
|
@@ -72,8 +73,7 @@
|
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
75
|
"dependencies": {
|
|
75
|
-
"esm-env": "^1.2.2"
|
|
76
|
-
"runed": "^0.31.1"
|
|
76
|
+
"esm-env": "^1.2.2"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@eslint/compat": "^1.3.1",
|
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
"eslint-plugin-svelte": "^3.11.0",
|
|
98
98
|
"eslint-plugin-unused-imports": "^4.1.4",
|
|
99
99
|
"globals": "^16.3.0",
|
|
100
|
+
"husky": "^9.1.7",
|
|
100
101
|
"jsdom": "^26.1.0",
|
|
101
102
|
"prettier": "^3.6.2",
|
|
102
103
|
"prettier-plugin-organize-imports": "^4.2.0",
|