@humanspeak/svelte-virtual-list 0.0.0
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/LICENSE.md +20 -0
- package/README.md +107 -0
- package/dist/SvelteVirtualList.svelte +173 -0
- package/dist/SvelteVirtualList.svelte.d.ts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.js +1 -0
- package/package.json +117 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2024 Humanspeak, Inc.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Svelte Virtual List
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
|
|
4
|
+
[](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/npm-publish.yml)
|
|
5
|
+
[](https://github.com/humanspeak/svelte-virtual-list/blob/main/LICENSE)
|
|
6
|
+
[](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
|
|
7
|
+
[](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/codeql.yml)
|
|
8
|
+
[](http://www.typescriptlang.org/)
|
|
9
|
+
[](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
|
|
10
|
+
[](https://github.com/humanspeak/svelte-virtual-list/graphs/commit-activity)
|
|
11
|
+
|
|
12
|
+
A virtual list component for Svelte applications. Built for Svelte 5 with TypeScript support.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @humanspeak/svelte-virtual-list
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```svelte
|
|
23
|
+
<script lang="ts">
|
|
24
|
+
import SvelteVirtualList from '@humanspeak/svelte-virtual-list'
|
|
25
|
+
|
|
26
|
+
type Item = {
|
|
27
|
+
id: number
|
|
28
|
+
text: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const items: Item[] = Array.from({ length: 10000 }, (_, i) => ({
|
|
32
|
+
id: i,
|
|
33
|
+
text: `Item ${i}`
|
|
34
|
+
}))
|
|
35
|
+
|
|
36
|
+
let measureRef: HTMLElement
|
|
37
|
+
let itemHeight = 20 // default height
|
|
38
|
+
|
|
39
|
+
onMount(() => {
|
|
40
|
+
if (measureRef) {
|
|
41
|
+
itemHeight = measureRef.getBoundingClientRect().height
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<SvelteVirtualList {items} height={400} {itemHeight}>
|
|
47
|
+
{#snippet renderItem(item: Item, index: number)}
|
|
48
|
+
<div class="list-item" bind:this={measureRef}>
|
|
49
|
+
{item.text}
|
|
50
|
+
</div>
|
|
51
|
+
{/snippet}
|
|
52
|
+
</SvelteVirtualList>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Props
|
|
56
|
+
|
|
57
|
+
The VirtualList component accepts the following props:
|
|
58
|
+
|
|
59
|
+
- `items` - Array of items to render
|
|
60
|
+
- `height` - Height of the viewport in pixels
|
|
61
|
+
- `itemHeight` - Height of each item in pixels
|
|
62
|
+
- `debug` - Enable debug mode (optional)
|
|
63
|
+
- `containerClass` - Custom class for container element (optional)
|
|
64
|
+
- `viewportClass` - Custom class for viewport element (optional)
|
|
65
|
+
- `contentClass` - Custom class for content element (optional)
|
|
66
|
+
- `itemsClass` - Custom class for items wrapper (optional)
|
|
67
|
+
- `renderItem` - Snippet function to render each item
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- Efficient rendering of large lists
|
|
72
|
+
- TypeScript support
|
|
73
|
+
- Customizable styling
|
|
74
|
+
- Debug mode for development
|
|
75
|
+
- Smooth scrolling with buffer zones
|
|
76
|
+
- SSR compatible
|
|
77
|
+
- Svelte 5 runes support
|
|
78
|
+
|
|
79
|
+
## Development
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm install
|
|
83
|
+
npm run dev
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Testing
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npm run test
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Building
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npm run build
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
[MIT](LICENSE)
|
|
101
|
+
|
|
102
|
+
## Related
|
|
103
|
+
|
|
104
|
+
- [Svelte](https://svelte.dev) - JavaScript front-end framework
|
|
105
|
+
- [Original Component](https://github.com/pablo-abc/svelte-virtual-list) - Original inspiration
|
|
106
|
+
|
|
107
|
+
Made with ♥ by [Humanspeak](https://humanspeak.com)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import type { DebugInfo, Props } from './types.js'
|
|
4
|
+
|
|
5
|
+
const {
|
|
6
|
+
items = [],
|
|
7
|
+
itemHeight = 40,
|
|
8
|
+
debug = false,
|
|
9
|
+
renderItem,
|
|
10
|
+
containerClass,
|
|
11
|
+
viewportClass,
|
|
12
|
+
contentClass,
|
|
13
|
+
itemsClass,
|
|
14
|
+
debugFunction,
|
|
15
|
+
mode = 'topToBottom',
|
|
16
|
+
bufferSize = 20
|
|
17
|
+
}: Props = $props()
|
|
18
|
+
|
|
19
|
+
let containerElement: HTMLElement
|
|
20
|
+
let viewportElement: HTMLElement
|
|
21
|
+
let scrollTop = $state(0)
|
|
22
|
+
let initialized = $state(false)
|
|
23
|
+
let height = $state(0)
|
|
24
|
+
|
|
25
|
+
// Initialize height
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (containerElement) {
|
|
28
|
+
height = containerElement.getBoundingClientRect().height
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Separate effect for scroll initialization
|
|
33
|
+
$effect(() => {
|
|
34
|
+
if (
|
|
35
|
+
mode === 'bottomToTop' &&
|
|
36
|
+
viewportElement &&
|
|
37
|
+
height > 0 &&
|
|
38
|
+
items.length &&
|
|
39
|
+
!initialized
|
|
40
|
+
) {
|
|
41
|
+
// Calculate total content height and max scroll position
|
|
42
|
+
const totalHeight = items.length * itemHeight
|
|
43
|
+
const maxScroll = totalHeight - height + itemHeight / 2 // Add half item height to ensure full scroll
|
|
44
|
+
|
|
45
|
+
// Force scroll to bottom
|
|
46
|
+
requestAnimationFrame(() => {
|
|
47
|
+
viewportElement.scrollTop = maxScroll
|
|
48
|
+
scrollTop = maxScroll
|
|
49
|
+
initialized = true
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
let visibleItems = $derived(() => {
|
|
55
|
+
if (!items.length) return { start: 0, end: 0 }
|
|
56
|
+
|
|
57
|
+
if (mode === 'bottomToTop' && !initialized) {
|
|
58
|
+
// Show the absolute last items while initializing
|
|
59
|
+
return {
|
|
60
|
+
start: Math.max(0, items.length - Math.ceil(height / itemHeight)),
|
|
61
|
+
end: items.length
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const visibleStart = Math.floor(scrollTop / itemHeight)
|
|
66
|
+
const visibleEnd = Math.min(items.length, Math.ceil((scrollTop + height) / itemHeight))
|
|
67
|
+
|
|
68
|
+
if (mode === 'bottomToTop') {
|
|
69
|
+
return {
|
|
70
|
+
start: Math.max(0, items.length - visibleEnd - bufferSize),
|
|
71
|
+
end: Math.min(items.length, items.length - visibleStart + bufferSize)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
start: Math.max(0, visibleStart - bufferSize),
|
|
77
|
+
end: Math.min(items.length, visibleEnd + bufferSize)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Handle scroll updates
|
|
82
|
+
const handleScroll = () => {
|
|
83
|
+
if (!viewportElement) return
|
|
84
|
+
scrollTop = viewportElement.scrollTop
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMount(() => {
|
|
88
|
+
if (containerElement) {
|
|
89
|
+
height = containerElement.getBoundingClientRect().height
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<div
|
|
95
|
+
id="virtual-list-container"
|
|
96
|
+
class={containerClass ?? 'virtual-list-container'}
|
|
97
|
+
bind:this={containerElement}
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
id="virtual-list-viewport"
|
|
101
|
+
class={viewportClass ?? 'virtual-list-viewport'}
|
|
102
|
+
bind:this={viewportElement}
|
|
103
|
+
onscroll={handleScroll}
|
|
104
|
+
>
|
|
105
|
+
<div
|
|
106
|
+
id="virtual-list-content"
|
|
107
|
+
class={contentClass ?? 'virtual-list-content'}
|
|
108
|
+
style:height="{Math.max(height, items.length * itemHeight)}px"
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
id="virtual-list-items"
|
|
112
|
+
class={itemsClass ?? 'virtual-list-items'}
|
|
113
|
+
style:transform="translateY({mode === 'bottomToTop'
|
|
114
|
+
? (items.length - visibleItems().end) * itemHeight
|
|
115
|
+
: visibleItems().start * itemHeight}px)"
|
|
116
|
+
>
|
|
117
|
+
{#each mode === 'bottomToTop' ? items
|
|
118
|
+
.slice(visibleItems().start, visibleItems().end)
|
|
119
|
+
.reverse() : items.slice(visibleItems().start, visibleItems().end) as currentItem, i (currentItem?.id ?? i)}
|
|
120
|
+
{#if debug && i === 0}
|
|
121
|
+
{@const debugInfo: DebugInfo = {
|
|
122
|
+
visibleItemsCount: visibleItems().end - visibleItems().start,
|
|
123
|
+
startIndex: visibleItems().start,
|
|
124
|
+
endIndex: visibleItems().end,
|
|
125
|
+
totalItems: items.length
|
|
126
|
+
}}
|
|
127
|
+
{debugFunction
|
|
128
|
+
? debugFunction(debugInfo)
|
|
129
|
+
: console.log('Virtual List Debug:', debugInfo)}
|
|
130
|
+
{/if}
|
|
131
|
+
{@render renderItem(
|
|
132
|
+
currentItem,
|
|
133
|
+
mode === 'bottomToTop'
|
|
134
|
+
? items.length - (visibleItems().start + i) - 1
|
|
135
|
+
: visibleItems().start + i
|
|
136
|
+
)}
|
|
137
|
+
{/each}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<style>
|
|
144
|
+
.virtual-list-container {
|
|
145
|
+
position: relative;
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 100%;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.virtual-list-viewport {
|
|
152
|
+
position: absolute;
|
|
153
|
+
top: 0;
|
|
154
|
+
left: 0;
|
|
155
|
+
right: 0;
|
|
156
|
+
bottom: 0;
|
|
157
|
+
overflow-y: scroll;
|
|
158
|
+
-webkit-overflow-scrolling: touch;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.virtual-list-content {
|
|
162
|
+
position: relative;
|
|
163
|
+
width: 100%;
|
|
164
|
+
min-height: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.virtual-list-items {
|
|
168
|
+
position: absolute;
|
|
169
|
+
width: 100%;
|
|
170
|
+
left: 0;
|
|
171
|
+
top: 0;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
export type Mode = 'topToBottom' | 'bottomToTop';
|
|
3
|
+
export type Props = {
|
|
4
|
+
items: any[];
|
|
5
|
+
itemHeight: number;
|
|
6
|
+
debug?: boolean;
|
|
7
|
+
debugFunction?: (_info: DebugInfo) => void;
|
|
8
|
+
containerClass?: string;
|
|
9
|
+
viewportClass?: string;
|
|
10
|
+
contentClass?: string;
|
|
11
|
+
itemsClass?: string;
|
|
12
|
+
mode?: Mode;
|
|
13
|
+
bufferSize?: number;
|
|
14
|
+
renderItem: Snippet<[item: any, index: number]>;
|
|
15
|
+
};
|
|
16
|
+
export type DebugInfo = {
|
|
17
|
+
startIndex: number;
|
|
18
|
+
endIndex: number;
|
|
19
|
+
visibleItemsCount: number;
|
|
20
|
+
totalItems: number;
|
|
21
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@humanspeak/svelte-virtual-list",
|
|
3
|
+
"description": "A high-performance virtual list component for Svelte 5 that efficiently renders large datasets through DOM recycling. Perfect for handling infinite scrolling and rendering thousands of items with minimal memory footprint.",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vite dev",
|
|
7
|
+
"build": "vite build && npm run package",
|
|
8
|
+
"preview": "vite preview",
|
|
9
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
10
|
+
"prepublishOnly": "npm run package",
|
|
11
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
12
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
13
|
+
"test": "vitest run --coverage",
|
|
14
|
+
"test:only": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"lint": "prettier --check . && eslint .",
|
|
17
|
+
"lint:fix": "npm run format && eslint . --fix",
|
|
18
|
+
"format": "prettier --write ."
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/humanspeak/svelte-virtual-list.git"
|
|
23
|
+
},
|
|
24
|
+
"author": "Humanspeak, Inc.",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/humanspeak/svelte-virtual-list/issues"
|
|
28
|
+
},
|
|
29
|
+
"tags": [
|
|
30
|
+
"svelte",
|
|
31
|
+
"virtual-list",
|
|
32
|
+
"virtual-scroll",
|
|
33
|
+
"virtual-scroller",
|
|
34
|
+
"infinite-scroll",
|
|
35
|
+
"performance",
|
|
36
|
+
"ui-component",
|
|
37
|
+
"svelte5"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"svelte",
|
|
41
|
+
"virtual-list",
|
|
42
|
+
"virtual-scroll",
|
|
43
|
+
"infinite-scroll",
|
|
44
|
+
"performance",
|
|
45
|
+
"ui-component",
|
|
46
|
+
"svelte5",
|
|
47
|
+
"dom-recycling",
|
|
48
|
+
"large-lists",
|
|
49
|
+
"scroll-optimization"
|
|
50
|
+
],
|
|
51
|
+
"homepage": "https://virtuallist.svelte.page",
|
|
52
|
+
"files": [
|
|
53
|
+
"dist",
|
|
54
|
+
"!dist/**/*.test.*",
|
|
55
|
+
"!dist/**/*.spec.*"
|
|
56
|
+
],
|
|
57
|
+
"sideEffects": [
|
|
58
|
+
"**/*.css"
|
|
59
|
+
],
|
|
60
|
+
"svelte": "./dist/index.js",
|
|
61
|
+
"types": "./dist/index.d.ts",
|
|
62
|
+
"type": "module",
|
|
63
|
+
"exports": {
|
|
64
|
+
".": {
|
|
65
|
+
"types": "./dist/index.d.ts",
|
|
66
|
+
"svelte": "./dist/index.js"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"svelte": "^5.0.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
74
|
+
"@eslint/js": "^9.17.0",
|
|
75
|
+
"@sveltejs/adapter-auto": "^3.3.1",
|
|
76
|
+
"@sveltejs/kit": "^2.15.1",
|
|
77
|
+
"@sveltejs/package": "^2.3.7",
|
|
78
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
|
79
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
80
|
+
"@testing-library/svelte": "^5.2.6",
|
|
81
|
+
"@testing-library/user-event": "^14.5.2",
|
|
82
|
+
"@types/node": "^22.10.5",
|
|
83
|
+
"@typescript-eslint/eslint-plugin": "^8.19.0",
|
|
84
|
+
"@typescript-eslint/parser": "^8.19.0",
|
|
85
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
86
|
+
"eslint": "^9.17.0",
|
|
87
|
+
"eslint-config-prettier": "^9.1.0",
|
|
88
|
+
"eslint-plugin-svelte": "^2.46.1",
|
|
89
|
+
"globals": "^15.14.0",
|
|
90
|
+
"jsdom": "^25.0.1",
|
|
91
|
+
"prettier": "^3.4.2",
|
|
92
|
+
"prettier-plugin-organize-imports": "^4.1.0",
|
|
93
|
+
"prettier-plugin-svelte": "^3.3.2",
|
|
94
|
+
"prettier-plugin-tailwindcss": "^0.6.9",
|
|
95
|
+
"publint": "^0.2.12",
|
|
96
|
+
"svelte": "^5.16.1",
|
|
97
|
+
"svelte-check": "^4.1.1",
|
|
98
|
+
"typescript": "^5.7.2",
|
|
99
|
+
"vite": "^6.0.7",
|
|
100
|
+
"vitest": "^2.1.8"
|
|
101
|
+
},
|
|
102
|
+
"overrides": {
|
|
103
|
+
"@sveltejs/kit": {
|
|
104
|
+
"cookie": "^0.7.0"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"volta": {
|
|
108
|
+
"node": "22.12.0"
|
|
109
|
+
},
|
|
110
|
+
"funding": {
|
|
111
|
+
"type": "github",
|
|
112
|
+
"url": "https://github.com/sponsors/humanspeak"
|
|
113
|
+
},
|
|
114
|
+
"publishConfig": {
|
|
115
|
+
"access": "public"
|
|
116
|
+
}
|
|
117
|
+
}
|