@humanspeak/svelte-virtual-list 0.3.5 → 0.3.6
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 +84 -20
- package/package.json +27 -26
|
@@ -537,6 +537,8 @@
|
|
|
537
537
|
let programmaticScrollInProgress = $state(false) // Prevent bottom-anchoring during programmatic scrolls
|
|
538
538
|
let lastCalculatedHeight = $state(0)
|
|
539
539
|
let lastItemsLength = $state(0)
|
|
540
|
+
// Track last observed total height to compute precise deltas on item count changes
|
|
541
|
+
let lastTotalHeightObserved = $state(0)
|
|
540
542
|
|
|
541
543
|
/**
|
|
542
544
|
* CRITICAL: O(1) Reactive Total Height Calculation
|
|
@@ -661,34 +663,96 @@
|
|
|
661
663
|
const currentCalculatedItemHeight = heightManager.averageHeight
|
|
662
664
|
const currentHeight = height
|
|
663
665
|
const currentTotalHeight = totalHeight()
|
|
664
|
-
const
|
|
666
|
+
const prevTotalHeight =
|
|
667
|
+
lastTotalHeightObserved ||
|
|
668
|
+
currentTotalHeight - itemsAdded * currentCalculatedItemHeight
|
|
669
|
+
const prevMaxScrollTop = Math.max(0, prevTotalHeight - currentHeight)
|
|
670
|
+
const nextMaxScrollTop = Math.max(0, currentTotalHeight - currentHeight)
|
|
671
|
+
const deltaMax = nextMaxScrollTop - prevMaxScrollTop
|
|
672
|
+
log('[SVL] items-length-change:before', {
|
|
673
|
+
instanceId,
|
|
674
|
+
itemsAdded,
|
|
675
|
+
lastItemsLength,
|
|
676
|
+
currentItemsLength,
|
|
677
|
+
currentScrollTop,
|
|
678
|
+
prevTotalHeight,
|
|
679
|
+
currentTotalHeight,
|
|
680
|
+
prevMaxScrollTop,
|
|
681
|
+
nextMaxScrollTop,
|
|
682
|
+
deltaMax,
|
|
683
|
+
averageItemHeight: currentCalculatedItemHeight
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
// Maintain visual position for ALL cases by advancing scrollTop by deltaMax.
|
|
687
|
+
// If near the bottom, this naturally pins to the new max; otherwise it preserves the current content.
|
|
688
|
+
programmaticScrollInProgress = true
|
|
689
|
+
void heightManager.runDynamicUpdate(() => {
|
|
690
|
+
const unclamped = currentScrollTop + deltaMax
|
|
691
|
+
const newScrollTop = Math.max(0, Math.min(nextMaxScrollTop, unclamped))
|
|
692
|
+
heightManager.viewport.scrollTop = newScrollTop
|
|
693
|
+
heightManager.scrollTop = newScrollTop
|
|
694
|
+
log('[SVL] items-length-change:applied', {
|
|
695
|
+
instanceId,
|
|
696
|
+
previousScrollTop: currentScrollTop,
|
|
697
|
+
appliedScrollTop: newScrollTop,
|
|
698
|
+
prevMaxScrollTop,
|
|
699
|
+
nextMaxScrollTop,
|
|
700
|
+
deltaMax
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
// We are explicitly managing position; consider this a programmatic action.
|
|
704
|
+
// Do not flip userHasScrolledAway here; it should reflect user intent only.
|
|
665
705
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
706
|
+
// Reconcile on next frame in case measured heights adjust totals
|
|
707
|
+
requestAnimationFrame(() => {
|
|
708
|
+
const beforeReconcileScrollTop = heightManager.viewport.scrollTop
|
|
709
|
+
const reconciledNextMax = Math.max(0, totalHeight() - height)
|
|
710
|
+
const reconciledDeltaMaxChange = reconciledNextMax - nextMaxScrollTop
|
|
711
|
+
// Desired position is to maintain distance-from-end; equivalently keep (max - scrollTop) constant.
|
|
712
|
+
const desiredScrollTop = Math.max(
|
|
713
|
+
0,
|
|
714
|
+
Math.min(reconciledNextMax, newScrollTop + reconciledDeltaMaxChange)
|
|
715
|
+
)
|
|
716
|
+
// Snap to integer pixels to prevent oscillation due to subpixel rounding
|
|
717
|
+
const desiredRounded = Math.round(desiredScrollTop)
|
|
718
|
+
const diffToDesired = desiredRounded - heightManager.viewport.scrollTop
|
|
719
|
+
if (Math.abs(diffToDesired) >= 1) {
|
|
720
|
+
const adjusted = Math.max(
|
|
671
721
|
0,
|
|
672
|
-
|
|
722
|
+
Math.min(reconciledNextMax, desiredRounded)
|
|
673
723
|
)
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
724
|
+
heightManager.viewport.scrollTop = adjusted
|
|
725
|
+
heightManager.scrollTop = adjusted
|
|
726
|
+
log('[SVL] items-length-change:reconciled', {
|
|
727
|
+
instanceId,
|
|
728
|
+
beforeReconcileScrollTop,
|
|
729
|
+
adjustedScrollTop: adjusted,
|
|
730
|
+
reconciledNextMax,
|
|
731
|
+
reconciledDeltaMaxChange,
|
|
732
|
+
desiredScrollTop,
|
|
733
|
+
desiredRounded,
|
|
734
|
+
diffToDesired
|
|
735
|
+
})
|
|
736
|
+
} else {
|
|
737
|
+
log('[SVL] items-length-change:reconciled-skip', {
|
|
738
|
+
instanceId,
|
|
739
|
+
beforeReconcileScrollTop,
|
|
740
|
+
reconciledNextMax,
|
|
741
|
+
reconciledDeltaMaxChange,
|
|
742
|
+
desiredScrollTop,
|
|
743
|
+
desiredRounded,
|
|
744
|
+
diffToDesired
|
|
745
|
+
})
|
|
746
|
+
}
|
|
747
|
+
programmaticScrollInProgress = false
|
|
686
748
|
})
|
|
687
|
-
}
|
|
749
|
+
})
|
|
688
750
|
}
|
|
689
751
|
}
|
|
690
752
|
|
|
691
753
|
lastItemsLength = currentItemsLength
|
|
754
|
+
// Update last observed total height at the end of the effect
|
|
755
|
+
lastTotalHeightObserved = totalHeight()
|
|
692
756
|
})
|
|
693
757
|
|
|
694
758
|
// Update container height continuously to reflect layout changes that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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": "^1.4.
|
|
63
|
-
"@eslint/js": "^9.
|
|
64
|
-
"@faker-js/faker": "^10.
|
|
65
|
-
"@playwright/test": "^1.56.
|
|
66
|
-
"@sveltejs/adapter-auto": "^
|
|
67
|
-
"@sveltejs/kit": "^2.
|
|
62
|
+
"@eslint/compat": "^1.4.1",
|
|
63
|
+
"@eslint/js": "^9.39.1",
|
|
64
|
+
"@faker-js/faker": "^10.1.0",
|
|
65
|
+
"@playwright/test": "^1.56.1",
|
|
66
|
+
"@sveltejs/adapter-auto": "^7.0.0",
|
|
67
|
+
"@sveltejs/kit": "^2.48.4",
|
|
68
68
|
"@sveltejs/package": "^2.5.4",
|
|
69
69
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
|
70
|
-
"@tailwindcss/vite": "^4.1.
|
|
70
|
+
"@tailwindcss/vite": "^4.1.17",
|
|
71
71
|
"@testing-library/jest-dom": "^6.9.1",
|
|
72
72
|
"@testing-library/svelte": "^5.2.8",
|
|
73
73
|
"@testing-library/user-event": "^14.6.1",
|
|
74
|
-
"@types/node": "^24.
|
|
75
|
-
"@typescript-eslint/eslint-plugin": "^8.46.
|
|
76
|
-
"@typescript-eslint/parser": "^8.46.
|
|
77
|
-
"@vitest/coverage-v8": "^
|
|
74
|
+
"@types/node": "^24.10.1",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
|
76
|
+
"@typescript-eslint/parser": "^8.46.4",
|
|
77
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
78
78
|
"concurrently": "^9.2.1",
|
|
79
|
-
"eslint": "^9.
|
|
79
|
+
"eslint": "^9.39.1",
|
|
80
80
|
"eslint-config-prettier": "^10.1.8",
|
|
81
81
|
"eslint-plugin-import": "^2.32.0",
|
|
82
|
-
"eslint-plugin-svelte": "^3.
|
|
83
|
-
"eslint-plugin-unused-imports": "^4.
|
|
84
|
-
"globals": "^16.
|
|
82
|
+
"eslint-plugin-svelte": "^3.13.0",
|
|
83
|
+
"eslint-plugin-unused-imports": "^4.3.0",
|
|
84
|
+
"globals": "^16.5.0",
|
|
85
85
|
"husky": "^9.1.7",
|
|
86
|
-
"jsdom": "^27.
|
|
86
|
+
"jsdom": "^27.2.0",
|
|
87
87
|
"prettier": "^3.6.2",
|
|
88
88
|
"prettier-plugin-organize-imports": "^4.3.0",
|
|
89
89
|
"prettier-plugin-sort-json": "^4.1.1",
|
|
90
90
|
"prettier-plugin-svelte": "^3.4.0",
|
|
91
|
-
"prettier-plugin-tailwindcss": "^0.
|
|
92
|
-
"publint": "^0.3.
|
|
93
|
-
"svelte": "^5.
|
|
94
|
-
"svelte-check": "^4.3.
|
|
95
|
-
"tailwindcss": "^4.1.
|
|
91
|
+
"prettier-plugin-tailwindcss": "^0.7.1",
|
|
92
|
+
"publint": "^0.3.15",
|
|
93
|
+
"svelte": "^5.43.6",
|
|
94
|
+
"svelte-check": "^4.3.4",
|
|
95
|
+
"tailwindcss": "^4.1.17",
|
|
96
96
|
"tw-animate-css": "^1.4.0",
|
|
97
97
|
"typescript": "^5.9.3",
|
|
98
|
-
"typescript-eslint": "^8.46.
|
|
99
|
-
"vite": "^7.
|
|
100
|
-
"vitest": "^
|
|
98
|
+
"typescript-eslint": "^8.46.4",
|
|
99
|
+
"vite": "^7.2.2",
|
|
100
|
+
"vitest": "^4.0.8"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
103
|
"svelte": "^5.0.0"
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
124
124
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
125
125
|
"dev": "vite dev",
|
|
126
|
-
"dev:all": "concurrently -k -n pkg,docs,sitemap -c green,cyan,magenta \"pnpm -w -r --filter @humanspeak/svelte-virtual-list run dev
|
|
126
|
+
"dev:all": "concurrently -k -n pkg,docs,sitemap -c green,cyan,magenta \"pnpm -w -r --filter @humanspeak/svelte-virtual-list run dev\" \"pnpm --filter docs run dev\" \"pnpm --filter docs run sitemap:watch\"",
|
|
127
127
|
"dev:pkg": "svelte-kit sync && svelte-package --watch",
|
|
128
128
|
"format": "prettier --write .",
|
|
129
129
|
"lint": "prettier --check . && eslint .",
|
|
@@ -138,6 +138,7 @@
|
|
|
138
138
|
"test:e2e:report": "playwright show-report",
|
|
139
139
|
"test:e2e:ui": "playwright test --ui",
|
|
140
140
|
"test:only": "vitest run --",
|
|
141
|
+
"test:unit": "vitest run --coverage",
|
|
141
142
|
"test:watch": "vitest --"
|
|
142
143
|
}
|
|
143
144
|
}
|