@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 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
+ [![NPM version](https://img.shields.io/npm/v/@humanspeak/svelte-virtual-list.svg)](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
4
+ [![Build Status](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/npm-publish.yml)
5
+ [![License](https://img.shields.io/npm/l/@humanspeak/svelte-virtual-list.svg)](https://github.com/humanspeak/svelte-virtual-list/blob/main/LICENSE)
6
+ [![Downloads](https://img.shields.io/npm/dm/@humanspeak/svelte-virtual-list.svg)](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
7
+ [![CodeQL](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/codeql.yml/badge.svg)](https://github.com/humanspeak/svelte-virtual-list/actions/workflows/codeql.yml)
8
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
9
+ [![Types](https://img.shields.io/npm/types/@humanspeak/svelte-virtual-list.svg)](https://www.npmjs.com/package/@humanspeak/svelte-virtual-list)
10
+ [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](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>
@@ -0,0 +1,4 @@
1
+ import type { Props } from './types.js';
2
+ declare const SvelteVirtualList: import("svelte").Component<Props, {}, "">;
3
+ type SvelteVirtualList = ReturnType<typeof SvelteVirtualList>;
4
+ export default SvelteVirtualList;
@@ -0,0 +1,4 @@
1
+ import SvelteVirtualList from './SvelteVirtualList.svelte';
2
+ import type { DebugInfo, Mode, Props } from './types.js';
3
+ export default SvelteVirtualList;
4
+ export type { DebugInfo, Mode, Props };
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import SvelteVirtualList from './SvelteVirtualList.svelte';
2
+ export default SvelteVirtualList;
@@ -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
+ }