@humanspeak/svelte-virtual-list 0.4.2 → 0.4.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/dist/SvelteVirtualList.svelte +13 -9
- package/dist/reactive-list-manager/ReactiveListManager.svelte.d.ts +0 -5
- package/dist/reactive-list-manager/ReactiveListManager.svelte.js +2 -20
- package/dist/utils/virtualList.d.ts +0 -27
- package/dist/utils/virtualList.js +2 -44
- package/package.json +24 -23
|
@@ -178,8 +178,11 @@
|
|
|
178
178
|
import { onMount, tick, untrack } from 'svelte'
|
|
179
179
|
|
|
180
180
|
const rafSchedule = createRafScheduler()
|
|
181
|
-
//
|
|
182
|
-
const
|
|
181
|
+
// Timing constants
|
|
182
|
+
const GLOBAL_CORRECTION_COOLDOWN_MS = 16
|
|
183
|
+
const SCROLL_IDLE_DELAY_MS = 250
|
|
184
|
+
const SUPPRESSION_WINDOW_MS = 450
|
|
185
|
+
const HEIGHT_DEBOUNCE_MS = 100
|
|
183
186
|
const lastCorrectionTimestampByViewport = new WeakMap<HTMLElement, number>()
|
|
184
187
|
// Package-specific debug flag - safe for library distribution
|
|
185
188
|
// Enable with: PUBLIC_SVELTE_VIRTUAL_LIST_DEBUG=true (preferred) or SVELTE_VIRTUAL_LIST_DEBUG=true
|
|
@@ -494,7 +497,7 @@
|
|
|
494
497
|
const now = performance.now()
|
|
495
498
|
const viewportEl = heightManager.viewport
|
|
496
499
|
const lastCorrectionMs = lastCorrectionTimestampByViewport.get(viewportEl) ?? 0
|
|
497
|
-
if (now - lastCorrectionMs <
|
|
500
|
+
if (now - lastCorrectionMs < GLOBAL_CORRECTION_COOLDOWN_MS) {
|
|
498
501
|
suppressBottomAnchoringUntilMs = now + 50
|
|
499
502
|
return
|
|
500
503
|
}
|
|
@@ -524,11 +527,12 @@
|
|
|
524
527
|
// Note: `container: 'nearest'` option could replace this once browser support improves
|
|
525
528
|
const currentScrollTop = heightManager.viewport.scrollTop
|
|
526
529
|
const offset = itemRect.bottom - contRect.bottom
|
|
527
|
-
|
|
530
|
+
syncScrollTop(currentScrollTop + offset)
|
|
528
531
|
log('[SVL] b2t-correction-manual', { offset })
|
|
532
|
+
} else {
|
|
533
|
+
// Sync our internal scroll state with actual DOM position
|
|
534
|
+
heightManager.scrollTop = heightManager.viewport.scrollTop
|
|
529
535
|
}
|
|
530
|
-
// Sync our internal scroll state with actual DOM position
|
|
531
|
-
heightManager.scrollTop = heightManager.viewport.scrollTop
|
|
532
536
|
// After peer correction, delay further corrections briefly
|
|
533
537
|
suppressBottomAnchoringUntilMs = performance.now() + 200
|
|
534
538
|
}
|
|
@@ -688,7 +692,7 @@
|
|
|
688
692
|
})
|
|
689
693
|
heightManager.endDynamicUpdate()
|
|
690
694
|
},
|
|
691
|
-
lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 :
|
|
695
|
+
lastMeasuredIndex < 0 || dirtyItems.size > 0 ? 0 : HEIGHT_DEBOUNCE_MS,
|
|
692
696
|
dirtyItems, // Pass dirty items for processing
|
|
693
697
|
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
694
698
|
0, // Don't pass ReactiveListManager state - let each system manage its own totals
|
|
@@ -1124,7 +1128,7 @@
|
|
|
1124
1128
|
if (idleCorrectionsOnly || anchorModeEnabled) {
|
|
1125
1129
|
reconcileToAnchorIfEnabled()
|
|
1126
1130
|
}
|
|
1127
|
-
},
|
|
1131
|
+
}, SCROLL_IDLE_DELAY_MS)
|
|
1128
1132
|
|
|
1129
1133
|
rafSchedule(() => {
|
|
1130
1134
|
const current = heightManager.viewport.scrollTop
|
|
@@ -1132,7 +1136,7 @@
|
|
|
1132
1136
|
const delta = lastScrollTopSnapshot - current
|
|
1133
1137
|
if (delta > 0.5) {
|
|
1134
1138
|
// Widen suppression to avoid fighting peer instance corrections
|
|
1135
|
-
suppressBottomAnchoringUntilMs = performance.now() +
|
|
1139
|
+
suppressBottomAnchoringUntilMs = performance.now() + SUPPRESSION_WINDOW_MS
|
|
1136
1140
|
userHasScrolledAway = true
|
|
1137
1141
|
}
|
|
1138
1142
|
}
|
|
@@ -183,11 +183,6 @@ export declare class ReactiveListManager {
|
|
|
183
183
|
* @returns Array of cumulative block sums
|
|
184
184
|
*/
|
|
185
185
|
getBlockSums(): number[];
|
|
186
|
-
/**
|
|
187
|
-
* Build block prefix sums for efficient offset calculations.
|
|
188
|
-
* Uses the same algorithm as the utility function but leverages internal state.
|
|
189
|
-
*/
|
|
190
|
-
private buildBlockSums;
|
|
191
186
|
/**
|
|
192
187
|
* Create a new ReactiveListManager instance
|
|
193
188
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildBlockSums } from '../utils/virtualList.js';
|
|
1
2
|
import { RecomputeScheduler } from './RecomputeScheduler.js';
|
|
2
3
|
/**
|
|
3
4
|
* ReactiveListManager - A standalone reactive height calculation system
|
|
@@ -378,30 +379,11 @@ export class ReactiveListManager {
|
|
|
378
379
|
*/
|
|
379
380
|
getBlockSums() {
|
|
380
381
|
if (!this._blockSumsValid || this._blockSums.length === 0) {
|
|
381
|
-
this._blockSums = this.
|
|
382
|
+
this._blockSums = buildBlockSums(this._heightCache, this._averageHeight, this._itemLength, this._blockSize);
|
|
382
383
|
this._blockSumsValid = true;
|
|
383
384
|
}
|
|
384
385
|
return this._blockSums;
|
|
385
386
|
}
|
|
386
|
-
/**
|
|
387
|
-
* Build block prefix sums for efficient offset calculations.
|
|
388
|
-
* Uses the same algorithm as the utility function but leverages internal state.
|
|
389
|
-
*/
|
|
390
|
-
buildBlockSums() {
|
|
391
|
-
const blocks = Math.ceil(this._itemLength / this._blockSize);
|
|
392
|
-
const sums = new Array(Math.max(0, blocks - 1));
|
|
393
|
-
let running = 0;
|
|
394
|
-
for (let b = 0; b < blocks - 1; b++) {
|
|
395
|
-
const start = b * this._blockSize;
|
|
396
|
-
const end = start + this._blockSize;
|
|
397
|
-
for (let i = start; i < end; i++) {
|
|
398
|
-
const height = this._heightCache[i];
|
|
399
|
-
running += Number.isFinite(height) && height > 0 ? height : this._averageHeight;
|
|
400
|
-
}
|
|
401
|
-
sums[b] = running;
|
|
402
|
-
}
|
|
403
|
-
return sums;
|
|
404
|
-
}
|
|
405
387
|
/**
|
|
406
388
|
* Create a new ReactiveListManager instance
|
|
407
389
|
*
|
|
@@ -138,33 +138,6 @@ export declare const calculateAverageHeight: (itemElements: HTMLElement[], visib
|
|
|
138
138
|
delta: number;
|
|
139
139
|
}>;
|
|
140
140
|
};
|
|
141
|
-
/**
|
|
142
|
-
* Processes large arrays in chunks to prevent UI blocking.
|
|
143
|
-
*
|
|
144
|
-
* This function implements a progressive processing strategy that:
|
|
145
|
-
* 1. Breaks down large arrays into manageable chunks
|
|
146
|
-
* 2. Processes each chunk asynchronously
|
|
147
|
-
* 3. Reports progress after each chunk
|
|
148
|
-
* 4. Yields to the main thread between chunks
|
|
149
|
-
*
|
|
150
|
-
* @param {any[]} items - Array of items to process
|
|
151
|
-
* @param {number} chunkSize - Number of items to process in each chunk
|
|
152
|
-
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
153
|
-
* @param {() => void} onComplete - Callback when all processing is complete
|
|
154
|
-
*
|
|
155
|
-
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* await processChunked(
|
|
159
|
-
* largeArray,
|
|
160
|
-
* 50,
|
|
161
|
-
* (processed) => console.log(`Processed ${processed} items`),
|
|
162
|
-
* () => console.log('All items processed')
|
|
163
|
-
* )
|
|
164
|
-
*/
|
|
165
|
-
export declare const processChunked: (items: any[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
166
|
-
chunkSize: number, onProgress: (processed: number) => void, // eslint-disable-line no-unused-vars
|
|
167
|
-
onComplete: () => void) => Promise<void>;
|
|
168
141
|
/**
|
|
169
142
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
170
143
|
*
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
export const getValidHeight = (height, fallback) => Number.isFinite(height) && height > 0 ? height : fallback;
|
|
19
|
+
const BOTTOM_TOLERANCE_FACTOR = 0.25;
|
|
19
20
|
/**
|
|
20
21
|
* Clamps a numeric value to be within a specified range.
|
|
21
22
|
*
|
|
@@ -97,7 +98,7 @@ export const calculateVisibleRange = (scrollTop, viewportHeight, itemHeight, tot
|
|
|
97
98
|
const totalHeight = totalContentHeight ?? totalItems * itemHeight;
|
|
98
99
|
const maxScrollTop = Math.max(0, totalHeight - viewportHeight);
|
|
99
100
|
// Use strict tolerance to avoid premature bottom anchoring that leaves a visible gap
|
|
100
|
-
const tolerance = Math.max(1, Math.floor(itemHeight *
|
|
101
|
+
const tolerance = Math.max(1, Math.floor(itemHeight * BOTTOM_TOLERANCE_FACTOR));
|
|
101
102
|
const isAtBottom = Math.abs(scrollTop - maxScrollTop) <= tolerance;
|
|
102
103
|
if (isAtBottom) {
|
|
103
104
|
// Pack from the end using measured heights when available: walk backward until viewport filled
|
|
@@ -335,49 +336,6 @@ export const calculateAverageHeight = (itemElements, visibleRange, heightCache,
|
|
|
335
336
|
heightChanges
|
|
336
337
|
};
|
|
337
338
|
};
|
|
338
|
-
/**
|
|
339
|
-
* Processes large arrays in chunks to prevent UI blocking.
|
|
340
|
-
*
|
|
341
|
-
* This function implements a progressive processing strategy that:
|
|
342
|
-
* 1. Breaks down large arrays into manageable chunks
|
|
343
|
-
* 2. Processes each chunk asynchronously
|
|
344
|
-
* 3. Reports progress after each chunk
|
|
345
|
-
* 4. Yields to the main thread between chunks
|
|
346
|
-
*
|
|
347
|
-
* @param {any[]} items - Array of items to process
|
|
348
|
-
* @param {number} chunkSize - Number of items to process in each chunk
|
|
349
|
-
* @param {(processed: number) => void} onProgress - Callback for progress updates
|
|
350
|
-
* @param {() => void} onComplete - Callback when all processing is complete
|
|
351
|
-
*
|
|
352
|
-
* @returns {Promise<void>} Resolves when all chunks have been processed
|
|
353
|
-
*
|
|
354
|
-
* @example
|
|
355
|
-
* await processChunked(
|
|
356
|
-
* largeArray,
|
|
357
|
-
* 50,
|
|
358
|
-
* (processed) => console.log(`Processed ${processed} items`),
|
|
359
|
-
* () => console.log('All items processed')
|
|
360
|
-
* )
|
|
361
|
-
*/
|
|
362
|
-
export const processChunked = async (items, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
363
|
-
chunkSize, onProgress, // eslint-disable-line no-unused-vars
|
|
364
|
-
onComplete) => {
|
|
365
|
-
if (!items.length) {
|
|
366
|
-
onComplete();
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const processChunk = async (startIdx) => {
|
|
370
|
-
const endIdx = Math.min(startIdx + chunkSize, items.length);
|
|
371
|
-
onProgress(endIdx);
|
|
372
|
-
if (endIdx < items.length) {
|
|
373
|
-
setTimeout(() => processChunk(endIdx), 0);
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
onComplete();
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
await processChunk(0);
|
|
380
|
-
};
|
|
381
339
|
/**
|
|
382
340
|
* Calculates the scroll offset (in pixels) needed to bring a specific item into view in a virtual list.
|
|
383
341
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
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",
|
|
@@ -59,45 +59,45 @@
|
|
|
59
59
|
"esm-env": "^1.2.2"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@eslint/compat": "^2.0.
|
|
62
|
+
"@eslint/compat": "^2.0.3",
|
|
63
63
|
"@eslint/js": "^10.0.1",
|
|
64
|
-
"@faker-js/faker": "^10.
|
|
64
|
+
"@faker-js/faker": "^10.4.0",
|
|
65
65
|
"@playwright/cli": "^0.1.1",
|
|
66
66
|
"@playwright/test": "^1.58.2",
|
|
67
67
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
68
|
-
"@sveltejs/kit": "^2.
|
|
68
|
+
"@sveltejs/kit": "^2.55.0",
|
|
69
69
|
"@sveltejs/package": "^2.5.7",
|
|
70
|
-
"@sveltejs/vite-plugin-svelte": "^
|
|
71
|
-
"@tailwindcss/vite": "^4.2.
|
|
70
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
71
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
72
72
|
"@testing-library/jest-dom": "^6.9.1",
|
|
73
73
|
"@testing-library/svelte": "^5.3.1",
|
|
74
74
|
"@testing-library/user-event": "^14.6.1",
|
|
75
|
-
"@types/node": "^25.
|
|
76
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
77
|
-
"@typescript-eslint/parser": "^8.
|
|
78
|
-
"@vitest/coverage-v8": "^4.
|
|
79
|
-
"eslint": "^10.0
|
|
75
|
+
"@types/node": "^25.5.0",
|
|
76
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
77
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
78
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
79
|
+
"eslint": "^10.1.0",
|
|
80
80
|
"eslint-config-prettier": "^10.1.8",
|
|
81
81
|
"eslint-plugin-import": "^2.32.0",
|
|
82
|
-
"eslint-plugin-svelte": "^3.
|
|
82
|
+
"eslint-plugin-svelte": "^3.16.0",
|
|
83
83
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
84
|
-
"globals": "^17.
|
|
84
|
+
"globals": "^17.4.0",
|
|
85
85
|
"husky": "^9.1.7",
|
|
86
|
-
"jsdom": "^
|
|
87
|
-
"mprocs": "^0.
|
|
86
|
+
"jsdom": "^29.0.1",
|
|
87
|
+
"mprocs": "^0.9.2",
|
|
88
88
|
"prettier": "^3.8.1",
|
|
89
89
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
90
|
-
"prettier-plugin-svelte": "^3.5.
|
|
90
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
91
91
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
|
92
|
-
"publint": "^0.3.
|
|
93
|
-
"svelte": "^5.
|
|
94
|
-
"svelte-check": "^4.4.
|
|
95
|
-
"tailwindcss": "^4.2.
|
|
92
|
+
"publint": "^0.3.18",
|
|
93
|
+
"svelte": "^5.55.0",
|
|
94
|
+
"svelte-check": "^4.4.5",
|
|
95
|
+
"tailwindcss": "^4.2.2",
|
|
96
96
|
"tw-animate-css": "^1.4.0",
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
|
-
"typescript-eslint": "^8.
|
|
99
|
-
"vite": "^
|
|
100
|
-
"vitest": "^4.
|
|
98
|
+
"typescript-eslint": "^8.57.2",
|
|
99
|
+
"vite": "^8.0.3",
|
|
100
|
+
"vitest": "^4.1.2"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"svelte": "^5.0.0"
|
|
@@ -120,6 +120,7 @@
|
|
|
120
120
|
],
|
|
121
121
|
"scripts": {
|
|
122
122
|
"build": "vite build && pnpm run package",
|
|
123
|
+
"cf-typegen": "pnpm --filter docs cf-typegen",
|
|
123
124
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
124
125
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
125
126
|
"dev": "vite dev",
|