@floor/vlist 0.5.7 → 0.6.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/README.md +793 -592
- package/dist/async/index.js +1 -0
- package/dist/builder/context.d.ts +3 -3
- package/dist/builder/context.d.ts.map +1 -1
- package/dist/builder/core.d.ts +1 -1
- package/dist/builder/core.d.ts.map +1 -1
- package/dist/builder/index.js +1 -250
- package/dist/builder/types.d.ts +3 -3
- package/dist/builder/types.d.ts.map +1 -1
- package/dist/compression/index.js +1 -104
- package/dist/core/core.js +1 -0
- package/dist/core/full.d.ts +22 -0
- package/dist/core/full.d.ts.map +1 -0
- package/dist/core/index.js +1 -133
- package/dist/core/lite.d.ts +129 -0
- package/dist/core/lite.d.ts.map +1 -0
- package/dist/core/minimal.d.ts +104 -0
- package/dist/core/minimal.d.ts.map +1 -0
- package/dist/core-light.js +1 -68
- package/dist/data/index.js +1 -233
- package/dist/features/async/index.d.ts +9 -0
- package/dist/features/async/index.d.ts.map +1 -0
- package/dist/features/async/manager.d.ts +103 -0
- package/dist/features/async/manager.d.ts.map +1 -0
- package/dist/features/async/placeholder.d.ts +62 -0
- package/dist/features/async/placeholder.d.ts.map +1 -0
- package/dist/features/async/plugin.d.ts +60 -0
- package/dist/features/async/plugin.d.ts.map +1 -0
- package/dist/features/async/sparse.d.ts +91 -0
- package/dist/features/async/sparse.d.ts.map +1 -0
- package/dist/features/grid/index.d.ts +9 -0
- package/dist/features/grid/index.d.ts.map +1 -0
- package/dist/features/grid/layout.d.ts +29 -0
- package/dist/features/grid/layout.d.ts.map +1 -0
- package/dist/features/grid/plugin.d.ts +48 -0
- package/dist/features/grid/plugin.d.ts.map +1 -0
- package/dist/features/grid/renderer.d.ts +55 -0
- package/dist/features/grid/renderer.d.ts.map +1 -0
- package/dist/features/grid/types.d.ts +71 -0
- package/dist/features/grid/types.d.ts.map +1 -0
- package/dist/features/page/index.d.ts +8 -0
- package/dist/features/page/index.d.ts.map +1 -0
- package/dist/features/page/plugin.d.ts +53 -0
- package/dist/features/page/plugin.d.ts.map +1 -0
- package/dist/features/scale/index.d.ts +10 -0
- package/dist/features/scale/index.d.ts.map +1 -0
- package/dist/features/scale/plugin.d.ts +42 -0
- package/dist/features/scale/plugin.d.ts.map +1 -0
- package/dist/features/scrollbar/controller.d.ts +121 -0
- package/dist/features/scrollbar/controller.d.ts.map +1 -0
- package/dist/features/scrollbar/index.d.ts +8 -0
- package/dist/features/scrollbar/index.d.ts.map +1 -0
- package/dist/features/scrollbar/plugin.d.ts +60 -0
- package/dist/features/scrollbar/plugin.d.ts.map +1 -0
- package/dist/features/scrollbar/scrollbar.d.ts +73 -0
- package/dist/features/scrollbar/scrollbar.d.ts.map +1 -0
- package/dist/features/sections/index.d.ts +10 -0
- package/dist/features/sections/index.d.ts.map +1 -0
- package/dist/features/sections/layout.d.ts +46 -0
- package/dist/features/sections/layout.d.ts.map +1 -0
- package/dist/features/sections/plugin.d.ts +64 -0
- package/dist/features/sections/plugin.d.ts.map +1 -0
- package/dist/features/sections/sticky.d.ts +33 -0
- package/dist/features/sections/sticky.d.ts.map +1 -0
- package/dist/features/sections/types.d.ts +86 -0
- package/dist/features/sections/types.d.ts.map +1 -0
- package/dist/features/selection/index.d.ts +7 -0
- package/dist/features/selection/index.d.ts.map +1 -0
- package/dist/features/selection/plugin.d.ts +44 -0
- package/dist/features/selection/plugin.d.ts.map +1 -0
- package/dist/features/selection/state.d.ts +102 -0
- package/dist/features/selection/state.d.ts.map +1 -0
- package/dist/features/snapshots/index.d.ts +8 -0
- package/dist/features/snapshots/index.d.ts.map +1 -0
- package/dist/features/snapshots/plugin.d.ts +44 -0
- package/dist/features/snapshots/plugin.d.ts.map +1 -0
- package/dist/grid/index.js +1 -198
- package/dist/groups/index.js +1 -204
- package/dist/index.d.ts +17 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1129
- package/dist/page/index.js +1 -0
- package/dist/plugins/groups/plugin.d.ts +3 -2
- package/dist/plugins/groups/plugin.d.ts.map +1 -1
- package/dist/react/index.js +1 -1024
- package/dist/react/react.js +1 -0
- package/dist/rendering/heights.d.ts +63 -0
- package/dist/rendering/heights.d.ts.map +1 -0
- package/dist/rendering/index.d.ts +9 -0
- package/dist/rendering/index.d.ts.map +1 -0
- package/dist/rendering/renderer.d.ts +103 -0
- package/dist/rendering/renderer.d.ts.map +1 -0
- package/dist/rendering/scale.d.ts +116 -0
- package/dist/rendering/scale.d.ts.map +1 -0
- package/dist/rendering/viewport.d.ts +139 -0
- package/dist/rendering/viewport.d.ts.map +1 -0
- package/dist/scale/index.js +1 -0
- package/dist/scroll/index.js +1 -116
- package/dist/scrollbar/index.js +1 -0
- package/dist/sections/index.js +1 -0
- package/dist/selection/index.js +1 -96
- package/dist/snapshots/index.js +1 -21
- package/dist/svelte/index.js +1 -1012
- package/dist/svelte/svelte.js +1 -0
- package/dist/vue/index.js +1 -1018
- package/dist/vue/vue.js +1 -0
- package/dist/window/index.js +1 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,49 +1,38 @@
|
|
|
1
1
|
# vlist
|
|
2
2
|
|
|
3
|
-
Lightweight, high-performance virtual list with zero dependencies.
|
|
3
|
+
Lightweight, high-performance virtual list with zero dependencies and optimal tree-shaking.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@floor/vlist)
|
|
6
6
|
[](https://bundlephobia.com/package/@floor/vlist)
|
|
7
|
-
[](https://github.com/floor/vlist)
|
|
8
8
|
[](https://github.com/floor/vlist/blob/main/LICENSE)
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
12
|
- 🪶 **Zero dependencies** - No external libraries required
|
|
13
|
-
- ⚡ **
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
13
|
+
- ⚡ **Tiny bundle** - 8-12 KB gzipped (vs 20+ KB for traditional virtual lists)
|
|
14
|
+
- 🌲 **Perfect tree-shaking** - Pay only for features you use
|
|
15
|
+
- 🎯 **Builder API** - Explicit, composable plugin system
|
|
16
|
+
- 📐 **Grid layout** - 2D virtualized grid with configurable columns
|
|
17
|
+
- 📏 **Variable heights** - Fixed or per-item height calculation
|
|
18
|
+
- ↔️ **Horizontal scrolling** - Horizontal lists and carousels
|
|
19
|
+
- 📜 **Async loading** - Built-in lazy loading with adapters
|
|
19
20
|
- ✅ **Selection** - Single and multiple selection modes
|
|
20
21
|
- 📌 **Sticky headers** - Grouped lists with sticky section headers
|
|
21
|
-
- 🪟 **
|
|
22
|
-
- 🔄 **Wrap navigation** - Circular scrolling for wizards
|
|
22
|
+
- 🪟 **Page scrolling** - Document-level scrolling mode
|
|
23
|
+
- 🔄 **Wrap navigation** - Circular scrolling for wizards
|
|
24
|
+
- 💬 **Reverse mode** - Chat UI with auto-scroll and history loading
|
|
25
|
+
- ⚖️ **Scale to millions** - Handle 1M+ items with automatic compression
|
|
23
26
|
- 🎨 **Customizable** - Beautiful, customizable styles
|
|
24
|
-
- ♿ **Accessible** - WAI-ARIA
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
- 💬 **Reverse mode** - Chat UI support with auto-scroll, scroll-preserving prepend
|
|
28
|
-
- 🔌 **Framework adapters** - Thin wrappers for React, Vue, and Svelte (<1 KB each)
|
|
29
|
-
- 🌲 **Tree-shakeable** - Sub-module imports for smaller bundles
|
|
27
|
+
- ♿ **Accessible** - Full WAI-ARIA support and keyboard navigation
|
|
28
|
+
- 🔌 **Framework adapters** - React, Vue, and Svelte support
|
|
29
|
+
- 📱 **Mobile optimized** - Touch-friendly with momentum scrolling
|
|
30
30
|
|
|
31
31
|
## Sandbox & Documentation
|
|
32
32
|
|
|
33
|
-
Interactive examples and documentation
|
|
33
|
+
Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
|
|
34
34
|
|
|
35
|
-
**
|
|
36
|
-
|
|
37
|
-
| Category | Examples |
|
|
38
|
-
|----------|----------|
|
|
39
|
-
| **Getting Started** | [Basic](https://vlist.dev/sandbox/basic/) • [Controls](https://vlist.dev/sandbox/controls/) |
|
|
40
|
-
| **Core (Lightweight)** | [Basic Core](https://vlist.dev/sandbox/core/basic/) — 7.8KB, 83% smaller |
|
|
41
|
-
| **Grid Plugin** | [Photo Album](https://vlist.dev/sandbox/grid/photo-album/) • [File Browser](https://vlist.dev/sandbox/grid/file-browser/) |
|
|
42
|
-
| **Data Plugin** | [Large List](https://vlist.dev/sandbox/data/large-list/) (100K–5M items) • [Velocity Loading](https://vlist.dev/sandbox/data/velocity-loading/) |
|
|
43
|
-
| **Horizontal** | [Basic Horizontal](https://vlist.dev/sandbox/horizontal/basic/) — 10K card carousel |
|
|
44
|
-
| **Groups Plugin** | [Sticky Headers](https://vlist.dev/sandbox/groups/sticky-headers/) — A–Z contact list |
|
|
45
|
-
| **Other Plugins** | [Scroll Restore](https://vlist.dev/sandbox/scroll-restore/) • [Window Scroll](https://vlist.dev/sandbox/window-scroll/) |
|
|
46
|
-
| **Advanced** | [Variable Heights](https://vlist.dev/sandbox/variable-heights/) • [Reverse Chat](https://vlist.dev/sandbox/reverse-chat/) • [Wizard Nav](https://vlist.dev/sandbox/wizard-nav/) |
|
|
35
|
+
**30+ examples** with multi-framework implementations (JavaScript, React, Vue, Svelte)
|
|
47
36
|
|
|
48
37
|
## Installation
|
|
49
38
|
|
|
@@ -51,796 +40,1008 @@ Interactive examples and documentation are available at **[vlist.dev](https://vl
|
|
|
51
40
|
npm install @floor/vlist
|
|
52
41
|
```
|
|
53
42
|
|
|
54
|
-
> **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to
|
|
55
|
-
|
|
56
|
-
### Sub-module Imports
|
|
43
|
+
> **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
|
|
57
44
|
|
|
58
|
-
|
|
45
|
+
## Quick Start
|
|
59
46
|
|
|
60
47
|
```typescript
|
|
61
|
-
import {
|
|
62
|
-
import
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
48
|
+
import { vlist } from 'vlist';
|
|
49
|
+
import 'vlist/styles';
|
|
50
|
+
|
|
51
|
+
const list = vlist({
|
|
52
|
+
container: '#my-list',
|
|
53
|
+
items: [
|
|
54
|
+
{ id: 1, name: 'Alice' },
|
|
55
|
+
{ id: 2, name: 'Bob' },
|
|
56
|
+
{ id: 3, name: 'Charlie' },
|
|
57
|
+
],
|
|
58
|
+
item: {
|
|
59
|
+
height: 48,
|
|
60
|
+
template: (item) => `<div>${item.name}</div>`,
|
|
61
|
+
},
|
|
62
|
+
}).build();
|
|
63
|
+
|
|
64
|
+
// API methods
|
|
65
|
+
list.scrollToIndex(10);
|
|
66
|
+
list.setItems(newItems);
|
|
67
|
+
list.on('item:click', ({ item }) => console.log(item));
|
|
71
68
|
```
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|--------|----------|---------|-------------|
|
|
75
|
-
| `@floor/vlist` | 70 KB | 23 KB | All features (plugins + framework adapters) |
|
|
76
|
-
| **`@floor/vlist/core`** | **8 KB** | **3 KB** | **Core virtual list — 88% smaller** |
|
|
77
|
-
| **`@floor/vlist/core-light`** | **5 KB** | **2 KB** | **Ultra-minimal — 93% smaller** |
|
|
78
|
-
| `@floor/vlist/builder` | 17 KB | 6 KB | Declarative API with chaining |
|
|
79
|
-
| `@floor/vlist/data` | 12 KB | 5 KB | Sparse storage, placeholders, data manager |
|
|
80
|
-
| `@floor/vlist/scroll` | 9 KB | 3 KB | Scroll controller + custom scrollbar |
|
|
81
|
-
| `@floor/vlist/grid` | 10 KB | 4 KB | Grid layout + 2D renderer |
|
|
82
|
-
| `@floor/vlist/groups` | 11 KB | 5 KB | Group layout + sticky headers |
|
|
83
|
-
| `@floor/vlist/compression` | 8 KB | 3 KB | Large-list compression utilities |
|
|
84
|
-
| `@floor/vlist/selection` | 6 KB | 2 KB | Selection state management |
|
|
70
|
+
**Bundle:** ~8 KB gzipped
|
|
85
71
|
|
|
86
|
-
|
|
72
|
+
## Builder Pattern
|
|
87
73
|
|
|
88
|
-
|
|
74
|
+
VList uses a composable builder pattern. Start with the base, add only the features you need:
|
|
89
75
|
|
|
90
76
|
```typescript
|
|
91
|
-
import {
|
|
92
|
-
|
|
93
|
-
|
|
77
|
+
import { vlist, withGrid, withSections, withSelection } from 'vlist';
|
|
78
|
+
|
|
79
|
+
const list = vlist({
|
|
80
|
+
container: '#app',
|
|
81
|
+
items: photos,
|
|
82
|
+
item: {
|
|
83
|
+
height: 200,
|
|
84
|
+
template: renderPhoto,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
88
|
+
.use(withSections({
|
|
89
|
+
getGroupForIndex: (i) => photos[i].category,
|
|
90
|
+
headerHeight: 40,
|
|
91
|
+
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
92
|
+
}))
|
|
93
|
+
.use(withSelection({ mode: 'multiple' }))
|
|
94
|
+
.build();
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|--------|----------|---------|-------------|
|
|
98
|
-
| `@floor/vlist/react` | 0.7 KB | 0.4 KB | `useVList` hook + `useVListEvent` |
|
|
99
|
-
| `@floor/vlist/vue` | 0.5 KB | 0.4 KB | `useVList` composable + `useVListEvent` |
|
|
100
|
-
| `@floor/vlist/svelte` | 0.3 KB | 0.2 KB | `vlist` action + `onVListEvent` |
|
|
97
|
+
**Bundle:** ~12 KB gzipped (only includes used plugins)
|
|
101
98
|
|
|
102
|
-
|
|
99
|
+
### Available Plugins
|
|
103
100
|
|
|
104
|
-
|
|
101
|
+
| Plugin | Cost | Description |
|
|
102
|
+
|--------|------|-------------|
|
|
103
|
+
| **Base** | 7.7 KB gzip | Core virtualization, no plugins |
|
|
104
|
+
| `withGrid()` | +4.0 KB | 2D grid layout |
|
|
105
|
+
| `withSections()` | +4.6 KB | Grouped lists with sticky/inline headers |
|
|
106
|
+
| `withAsync()` | +5.3 KB | Async data loading with adapters |
|
|
107
|
+
| `withSelection()` | +2.3 KB | Single/multiple item selection |
|
|
108
|
+
| `withScale()` | +2.2 KB | Handle 1M+ items with compression |
|
|
109
|
+
| `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
|
|
110
|
+
| `withPage()` | +0.9 KB | Document-level scrolling |
|
|
111
|
+
| `withSnapshots()` | Included | Scroll save/restore |
|
|
112
|
+
|
|
113
|
+
**Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
|
|
114
|
+
|
|
115
|
+
## Examples
|
|
116
|
+
|
|
117
|
+
### Simple List (No Plugins)
|
|
105
118
|
|
|
106
119
|
```typescript
|
|
107
|
-
import {
|
|
108
|
-
import '@floor/vlist/styles';
|
|
120
|
+
import { vlist } from 'vlist';
|
|
109
121
|
|
|
110
|
-
const list =
|
|
111
|
-
container: '#
|
|
112
|
-
|
|
122
|
+
const list = vlist({
|
|
123
|
+
container: '#list',
|
|
124
|
+
items: users,
|
|
113
125
|
item: {
|
|
114
|
-
height:
|
|
115
|
-
template: (
|
|
116
|
-
<div class="
|
|
117
|
-
<img src="${
|
|
118
|
-
<span>${
|
|
126
|
+
height: 64,
|
|
127
|
+
template: (user) => `
|
|
128
|
+
<div class="user">
|
|
129
|
+
<img src="${user.avatar}" />
|
|
130
|
+
<span>${user.name}</span>
|
|
119
131
|
</div>
|
|
120
132
|
`,
|
|
121
133
|
},
|
|
122
|
-
|
|
123
|
-
{ id: 1, name: 'Alice', avatar: '/avatars/alice.jpg' },
|
|
124
|
-
{ id: 2, name: 'Bob', avatar: '/avatars/bob.jpg' },
|
|
125
|
-
// ... more items
|
|
126
|
-
],
|
|
127
|
-
});
|
|
134
|
+
}).build();
|
|
128
135
|
```
|
|
129
136
|
|
|
130
|
-
|
|
137
|
+
**Bundle:** 8.2 KB gzipped
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
### Grid Layout
|
|
133
140
|
|
|
134
141
|
```typescript
|
|
135
|
-
import {
|
|
136
|
-
import '@floor/vlist/styles';
|
|
142
|
+
import { vlist, withGrid, withScrollbar } from 'vlist';
|
|
137
143
|
|
|
138
|
-
const
|
|
139
|
-
container: '#
|
|
144
|
+
const gallery = vlist({
|
|
145
|
+
container: '#gallery',
|
|
146
|
+
items: photos,
|
|
140
147
|
item: {
|
|
141
|
-
height:
|
|
142
|
-
template: (
|
|
148
|
+
height: 200,
|
|
149
|
+
template: (photo) => `
|
|
150
|
+
<div class="card">
|
|
151
|
+
<img src="${photo.url}" />
|
|
152
|
+
<span>${photo.title}</span>
|
|
153
|
+
</div>
|
|
154
|
+
`,
|
|
143
155
|
},
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
list.on('item:click', ({ item }) => console.log(item));
|
|
149
|
-
list.scrollToIndex(50, { behavior: 'smooth' });
|
|
156
|
+
})
|
|
157
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
158
|
+
.use(withScrollbar({ autoHide: true }))
|
|
159
|
+
.build();
|
|
150
160
|
```
|
|
151
161
|
|
|
152
|
-
|
|
162
|
+
**Bundle:** 11.7 KB gzipped
|
|
153
163
|
|
|
154
|
-
|
|
164
|
+
Grid mode virtualizes by **rows** - only visible rows are in the DOM. Each item is positioned with `translate(x, y)` for GPU-accelerated rendering.
|
|
165
|
+
|
|
166
|
+
### Sticky Headers (Contact List)
|
|
155
167
|
|
|
156
168
|
```typescript
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
169
|
+
import { vlist, withSections } from 'vlist';
|
|
170
|
+
|
|
171
|
+
const contacts = vlist({
|
|
172
|
+
container: '#contacts',
|
|
173
|
+
items: sortedContacts, // Must be pre-sorted by group
|
|
160
174
|
item: {
|
|
161
|
-
height:
|
|
162
|
-
template:
|
|
163
|
-
}
|
|
175
|
+
height: 56,
|
|
176
|
+
template: (contact) => `<div>${contact.name}</div>`,
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
.use(withSections({
|
|
180
|
+
getGroupForIndex: (i) => contacts[i].lastName[0].toUpperCase(),
|
|
181
|
+
headerHeight: 36,
|
|
182
|
+
headerTemplate: (letter) => `<div class="header">${letter}</div>`,
|
|
183
|
+
sticky: true, // Headers stick to top (Telegram style)
|
|
184
|
+
}))
|
|
185
|
+
.build();
|
|
186
|
+
```
|
|
164
187
|
|
|
165
|
-
|
|
166
|
-
layout?: 'list' | 'grid'; // Layout mode (default: 'list')
|
|
167
|
-
grid?: { // Grid config (required when layout: 'grid')
|
|
168
|
-
columns: number; // Number of columns
|
|
169
|
-
gap?: number; // Gap between items in px (default: 0)
|
|
170
|
-
};
|
|
188
|
+
**Bundle:** 12.3 KB gzipped
|
|
171
189
|
|
|
172
|
-
|
|
173
|
-
items?: T[]; // Static items array
|
|
174
|
-
adapter?: VListAdapter<T>; // Async data adapter
|
|
190
|
+
Set `sticky: false` for inline headers (iMessage/WhatsApp style).
|
|
175
191
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
wrap?: boolean; // Wrap around at boundaries (default: false)
|
|
181
|
-
scrollbar?: 'native' | 'none' // Scrollbar mode (default: custom)
|
|
182
|
-
| ScrollbarOptions; // or { autoHide, autoHideDelay, minThumbSize }
|
|
183
|
-
element?: Window; // Window scrolling mode
|
|
184
|
-
idleTimeout?: number; // Scroll idle detection in ms (default: 150)
|
|
185
|
-
};
|
|
192
|
+
### Chat UI (Reverse + Sections)
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { vlist, withSections } from 'vlist';
|
|
186
196
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
const chat = vlist({
|
|
198
|
+
container: '#messages',
|
|
199
|
+
reverse: true, // Start at bottom, newest messages visible
|
|
200
|
+
items: messages, // Chronological order (oldest first)
|
|
201
|
+
item: {
|
|
202
|
+
height: (i) => messages[i].height || 60,
|
|
203
|
+
template: (msg) => `<div class="message">${msg.text}</div>`,
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
.use(withSections({
|
|
207
|
+
getGroupForIndex: (i) => {
|
|
208
|
+
const date = new Date(messages[i].timestamp);
|
|
209
|
+
return date.toLocaleDateString(); // "Jan 15", "Jan 16", etc.
|
|
210
|
+
},
|
|
211
|
+
headerHeight: 32,
|
|
212
|
+
headerTemplate: (date) => `<div class="date-header">${date}</div>`,
|
|
213
|
+
sticky: false, // Inline date headers (iMessage style)
|
|
214
|
+
}))
|
|
215
|
+
.build();
|
|
191
216
|
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
// New messages - auto-scrolls to bottom
|
|
218
|
+
chat.appendItems([newMessage]);
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
ariaLabel?: string; // Accessible label for the listbox
|
|
198
|
-
}
|
|
220
|
+
// Load history - preserves scroll position
|
|
221
|
+
chat.prependItems(olderMessages);
|
|
199
222
|
```
|
|
200
223
|
|
|
201
|
-
|
|
224
|
+
**Bundle:** 11.9 KB gzipped
|
|
202
225
|
|
|
203
|
-
|
|
226
|
+
Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
|
|
227
|
+
|
|
228
|
+
### Large Datasets (1M+ Items)
|
|
204
229
|
|
|
205
230
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
gap: 8, // 8px gap between columns AND rows
|
|
212
|
-
},
|
|
231
|
+
import { vlist, withScale, withScrollbar } from 'vlist';
|
|
232
|
+
|
|
233
|
+
const bigList = vlist({
|
|
234
|
+
container: '#big-list',
|
|
235
|
+
items: generateItems(5_000_000),
|
|
213
236
|
item: {
|
|
214
|
-
height:
|
|
215
|
-
template: (item) =>
|
|
216
|
-
<div class="card">
|
|
217
|
-
<img src="${item.thumbnail}" />
|
|
218
|
-
<span>${item.title}</span>
|
|
219
|
-
</div>
|
|
220
|
-
`,
|
|
237
|
+
height: 48,
|
|
238
|
+
template: (item) => `<div>#${item.id}: ${item.name}</div>`,
|
|
221
239
|
},
|
|
222
|
-
|
|
223
|
-
|
|
240
|
+
})
|
|
241
|
+
.use(withScale()) // Auto-activates when height > 16.7M pixels
|
|
242
|
+
.use(withScrollbar({ autoHide: true }))
|
|
243
|
+
.build();
|
|
224
244
|
```
|
|
225
245
|
|
|
226
|
-
|
|
246
|
+
**Bundle:** 9.9 KB gzipped
|
|
247
|
+
|
|
248
|
+
The scale plugin automatically compresses scroll space when total height exceeds browser limits (~16.7M pixels), enabling smooth scrolling through millions of items.
|
|
227
249
|
|
|
228
|
-
###
|
|
250
|
+
### Async Loading with Pagination
|
|
229
251
|
|
|
230
252
|
```typescript
|
|
231
|
-
|
|
232
|
-
|
|
253
|
+
import { vlist, withAsync } from 'vlist';
|
|
254
|
+
|
|
255
|
+
const list = vlist({
|
|
256
|
+
container: '#list',
|
|
233
257
|
item: {
|
|
234
|
-
height:
|
|
235
|
-
template: (item) =>
|
|
258
|
+
height: 64,
|
|
259
|
+
template: (item) => {
|
|
260
|
+
if (!item) return `<div class="loading">Loading...</div>`;
|
|
261
|
+
return `<div>${item.name}</div>`;
|
|
262
|
+
},
|
|
236
263
|
},
|
|
237
|
-
|
|
238
|
-
|
|
264
|
+
})
|
|
265
|
+
.use(withAsync({
|
|
266
|
+
adapter: {
|
|
267
|
+
read: async ({ offset, limit }) => {
|
|
268
|
+
const response = await fetch(`/api/users?offset=${offset}&limit=${limit}`);
|
|
269
|
+
const data = await response.json();
|
|
270
|
+
return {
|
|
271
|
+
items: data.items,
|
|
272
|
+
total: data.total,
|
|
273
|
+
hasMore: data.hasMore,
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
loading: {
|
|
278
|
+
cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
|
|
279
|
+
},
|
|
280
|
+
}))
|
|
281
|
+
.build();
|
|
239
282
|
```
|
|
240
283
|
|
|
241
|
-
|
|
284
|
+
**Bundle:** 13.5 KB gzipped
|
|
285
|
+
|
|
286
|
+
The async plugin shows placeholders for unloaded items and fetches data as you scroll. Velocity-aware loading cancels requests when scrolling fast.
|
|
242
287
|
|
|
243
|
-
###
|
|
288
|
+
### Page-Level Scrolling
|
|
244
289
|
|
|
245
290
|
```typescript
|
|
246
|
-
|
|
247
|
-
|
|
291
|
+
import { vlist, withPage } from 'vlist';
|
|
292
|
+
|
|
293
|
+
const list = vlist({
|
|
294
|
+
container: '#list',
|
|
295
|
+
items: articles,
|
|
248
296
|
item: {
|
|
249
|
-
height:
|
|
250
|
-
template: (
|
|
251
|
-
},
|
|
252
|
-
items: contacts, // Must be pre-sorted by group
|
|
253
|
-
groups: {
|
|
254
|
-
getGroupForIndex: (index) => contacts[index].lastName[0],
|
|
255
|
-
headerHeight: 36,
|
|
256
|
-
headerTemplate: (group) => `<div class="section-header">${group}</div>`,
|
|
257
|
-
sticky: true, // Headers stick to the top (default: true)
|
|
297
|
+
height: 200,
|
|
298
|
+
template: (article) => `<article>...</article>`,
|
|
258
299
|
},
|
|
259
|
-
})
|
|
300
|
+
})
|
|
301
|
+
.use(withPage()) // Uses document scroll instead of container
|
|
302
|
+
.build();
|
|
260
303
|
```
|
|
261
304
|
|
|
262
|
-
|
|
305
|
+
**Bundle:** 8.6 KB gzipped
|
|
306
|
+
|
|
307
|
+
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
308
|
+
|
|
309
|
+
### Horizontal Carousel
|
|
263
310
|
|
|
264
311
|
```typescript
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
312
|
+
import { vlist } from 'vlist';
|
|
313
|
+
|
|
314
|
+
const carousel = vlist({
|
|
315
|
+
container: '#carousel',
|
|
316
|
+
direction: 'horizontal',
|
|
317
|
+
items: cards,
|
|
268
318
|
item: {
|
|
269
|
-
|
|
270
|
-
|
|
319
|
+
width: 300, // Required for horizontal
|
|
320
|
+
height: 400, // Optional (can use CSS)
|
|
321
|
+
template: (card) => `<div class="card">...</div>`,
|
|
271
322
|
},
|
|
272
|
-
|
|
273
|
-
|
|
323
|
+
scroll: {
|
|
324
|
+
wrap: true, // Circular scrolling
|
|
325
|
+
},
|
|
326
|
+
}).build();
|
|
274
327
|
```
|
|
275
328
|
|
|
276
|
-
|
|
329
|
+
**Bundle:** 8.6 KB gzipped
|
|
330
|
+
|
|
331
|
+
### Selection & Navigation
|
|
277
332
|
|
|
278
333
|
```typescript
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
334
|
+
import { vlist, withSelection } from 'vlist';
|
|
335
|
+
|
|
336
|
+
const list = vlist({
|
|
337
|
+
container: '#list',
|
|
338
|
+
items: users,
|
|
282
339
|
item: {
|
|
283
|
-
height:
|
|
284
|
-
template: (
|
|
340
|
+
height: 48,
|
|
341
|
+
template: (user, index, { selected }) => {
|
|
342
|
+
const cls = selected ? 'item--selected' : '';
|
|
343
|
+
return `<div class="${cls}">${user.name}</div>`;
|
|
344
|
+
},
|
|
285
345
|
},
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
346
|
+
})
|
|
347
|
+
.use(withSelection({
|
|
348
|
+
mode: 'multiple',
|
|
349
|
+
initial: [1, 5, 10], // Pre-select items
|
|
350
|
+
}))
|
|
351
|
+
.build();
|
|
352
|
+
|
|
353
|
+
// Selection API
|
|
354
|
+
list.selectItem(5);
|
|
355
|
+
list.deselectItem(5);
|
|
356
|
+
list.toggleSelection(5);
|
|
357
|
+
list.getSelectedIds(); // [1, 5, 10]
|
|
358
|
+
list.clearSelection();
|
|
359
|
+
```
|
|
290
360
|
|
|
291
|
-
|
|
292
|
-
btnNext.addEventListener('click', () => {
|
|
293
|
-
current++;
|
|
294
|
-
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
295
|
-
});
|
|
361
|
+
**Bundle:** 10.0 KB gzipped
|
|
296
362
|
|
|
297
|
-
|
|
298
|
-
current--;
|
|
299
|
-
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
300
|
-
});
|
|
301
|
-
```
|
|
363
|
+
Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
|
|
302
364
|
|
|
303
|
-
###
|
|
365
|
+
### Variable Heights (Chat Messages)
|
|
304
366
|
|
|
305
367
|
```typescript
|
|
306
|
-
|
|
368
|
+
import { vlist } from 'vlist';
|
|
369
|
+
|
|
370
|
+
const list = vlist({
|
|
307
371
|
container: '#messages',
|
|
308
|
-
|
|
372
|
+
items: messages,
|
|
309
373
|
item: {
|
|
310
|
-
height: (index) =>
|
|
374
|
+
height: (index) => {
|
|
375
|
+
// Heights computed from actual DOM measurements
|
|
376
|
+
return messages[index].measuredHeight || 60;
|
|
377
|
+
},
|
|
311
378
|
template: (msg) => `
|
|
312
|
-
<div class="
|
|
313
|
-
<
|
|
314
|
-
<
|
|
379
|
+
<div class="message">
|
|
380
|
+
<div class="author">${msg.user}</div>
|
|
381
|
+
<div class="text">${msg.text}</div>
|
|
315
382
|
</div>
|
|
316
383
|
`,
|
|
317
384
|
},
|
|
318
|
-
|
|
319
|
-
|
|
385
|
+
}).build();
|
|
386
|
+
```
|
|
320
387
|
|
|
321
|
-
|
|
322
|
-
chat.appendItems([newMessage]);
|
|
388
|
+
**Bundle:** 10.9 KB gzipped
|
|
323
389
|
|
|
324
|
-
|
|
325
|
-
chat.prependItems(olderMessages);
|
|
326
|
-
```
|
|
390
|
+
Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
|
|
327
391
|
|
|
328
|
-
|
|
392
|
+
## API Reference
|
|
329
393
|
|
|
330
|
-
###
|
|
394
|
+
### Core Methods
|
|
331
395
|
|
|
332
396
|
```typescript
|
|
333
|
-
const list =
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
397
|
+
const list = vlist(config).use(...plugins).build();
|
|
398
|
+
|
|
399
|
+
// Data manipulation
|
|
400
|
+
list.setItems(items: T[]): void
|
|
401
|
+
list.appendItems(items: T[]): void
|
|
402
|
+
list.prependItems(items: T[]): void
|
|
403
|
+
list.updateItem(id: string | number, item: Partial<T>): boolean
|
|
404
|
+
list.removeItem(id: string | number): boolean
|
|
405
|
+
|
|
406
|
+
// Navigation
|
|
407
|
+
list.scrollToIndex(index: number, align?: 'start' | 'center' | 'end'): void
|
|
408
|
+
list.scrollToIndex(index: number, options?: {
|
|
409
|
+
align?: 'start' | 'center' | 'end',
|
|
410
|
+
behavior?: 'auto' | 'smooth',
|
|
411
|
+
duration?: number
|
|
412
|
+
}): void
|
|
413
|
+
list.scrollToItem(id: string | number, align?: string): void
|
|
414
|
+
|
|
415
|
+
// State
|
|
416
|
+
list.getScrollPosition(): number
|
|
417
|
+
list.getVisibleRange(): { start: number, end: number }
|
|
418
|
+
list.getScrollSnapshot(): ScrollSnapshot
|
|
419
|
+
list.restoreScroll(snapshot: ScrollSnapshot): void
|
|
420
|
+
|
|
421
|
+
// Events
|
|
422
|
+
list.on(event: string, handler: Function): Unsubscribe
|
|
423
|
+
list.off(event: string, handler: Function): void
|
|
424
|
+
|
|
425
|
+
// Lifecycle
|
|
426
|
+
list.destroy(): void
|
|
427
|
+
|
|
428
|
+
// Properties
|
|
429
|
+
list.element: HTMLElement
|
|
430
|
+
list.items: readonly T[]
|
|
431
|
+
list.total: number
|
|
432
|
+
```
|
|
350
433
|
|
|
351
|
-
|
|
352
|
-
list.on('selection:change', ({ selected, items }) => {
|
|
353
|
-
console.log('Selected:', selected);
|
|
354
|
-
});
|
|
434
|
+
### Selection Methods (with `withSelection()`)
|
|
355
435
|
|
|
356
|
-
|
|
357
|
-
list.
|
|
358
|
-
list.
|
|
359
|
-
list.
|
|
360
|
-
list.
|
|
436
|
+
```typescript
|
|
437
|
+
list.selectItem(id: string | number): void
|
|
438
|
+
list.deselectItem(id: string | number): void
|
|
439
|
+
list.toggleSelection(id: string | number): void
|
|
440
|
+
list.selectAll(): void
|
|
441
|
+
list.clearSelection(): void
|
|
442
|
+
list.getSelectedIds(): Array<string | number>
|
|
443
|
+
list.getSelectedItems(): T[]
|
|
444
|
+
list.setSelectionMode(mode: 'none' | 'single' | 'multiple'): void
|
|
361
445
|
```
|
|
362
446
|
|
|
363
|
-
###
|
|
447
|
+
### Grid Methods (with `withGrid()`)
|
|
364
448
|
|
|
365
449
|
```typescript
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
item: {
|
|
369
|
-
height: 64,
|
|
370
|
-
template: (item) => `<div>${item.title}</div>`,
|
|
371
|
-
},
|
|
372
|
-
adapter: {
|
|
373
|
-
read: async ({ offset, limit }) => {
|
|
374
|
-
const response = await fetch(
|
|
375
|
-
`/api/items?offset=${offset}&limit=${limit}`
|
|
376
|
-
);
|
|
377
|
-
const data = await response.json();
|
|
378
|
-
|
|
379
|
-
return {
|
|
380
|
-
items: data.items,
|
|
381
|
-
total: data.total,
|
|
382
|
-
hasMore: data.hasMore,
|
|
383
|
-
};
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
});
|
|
450
|
+
list.updateGrid(config: { columns?: number, gap?: number }): void
|
|
451
|
+
```
|
|
387
452
|
|
|
388
|
-
|
|
389
|
-
list.on('load:start', ({ offset, limit }) => {
|
|
390
|
-
console.log('Loading...', offset, limit);
|
|
391
|
-
});
|
|
453
|
+
### Events
|
|
392
454
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
})
|
|
455
|
+
```typescript
|
|
456
|
+
list.on('scroll', ({ scrollTop, direction }) => { })
|
|
457
|
+
list.on('range:change', ({ range }) => { })
|
|
458
|
+
list.on('item:click', ({ item, index, event }) => { })
|
|
459
|
+
list.on('item:dblclick', ({ item, index, event }) => { })
|
|
460
|
+
list.on('selection:change', ({ selectedIds, selectedItems }) => { })
|
|
461
|
+
list.on('load:start', ({ offset, limit }) => { })
|
|
462
|
+
list.on('load:end', ({ items, offset, total }) => { })
|
|
463
|
+
list.on('load:error', ({ error, offset, limit }) => { })
|
|
464
|
+
list.on('velocity:change', ({ velocity, reliable }) => { })
|
|
396
465
|
```
|
|
397
466
|
|
|
398
|
-
|
|
467
|
+
## Configuration
|
|
468
|
+
|
|
469
|
+
### Base Configuration
|
|
399
470
|
|
|
400
471
|
```typescript
|
|
401
|
-
|
|
402
|
-
|
|
472
|
+
interface VListConfig<T> {
|
|
473
|
+
// Required
|
|
474
|
+
container: HTMLElement | string;
|
|
403
475
|
item: {
|
|
404
|
-
height:
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
selection: { mode: 'multiple' },
|
|
409
|
-
});
|
|
476
|
+
height?: number | ((index: number) => number); // Required for vertical
|
|
477
|
+
width?: number | ((index: number) => number); // Required for horizontal
|
|
478
|
+
template: (item: T, index: number, state: ItemState) => string | HTMLElement;
|
|
479
|
+
};
|
|
410
480
|
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
//
|
|
414
|
-
|
|
481
|
+
// Optional
|
|
482
|
+
items?: T[]; // Initial items
|
|
483
|
+
overscan?: number; // Extra items to render (default: 3)
|
|
484
|
+
direction?: 'vertical' | 'horizontal'; // Default: 'vertical'
|
|
485
|
+
reverse?: boolean; // Reverse mode for chat (default: false)
|
|
486
|
+
classPrefix?: string; // CSS class prefix (default: 'vlist')
|
|
487
|
+
ariaLabel?: string; // Accessible label
|
|
415
488
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
//
|
|
489
|
+
scroll?: {
|
|
490
|
+
wheel?: boolean; // Enable mouse wheel (default: true)
|
|
491
|
+
wrap?: boolean; // Circular scrolling (default: false)
|
|
492
|
+
scrollbar?: 'none'; // Hide scrollbar
|
|
493
|
+
idleTimeout?: number; // Scroll idle detection (default: 150ms)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
420
496
|
```
|
|
421
497
|
|
|
422
|
-
###
|
|
498
|
+
### Plugin: `withGrid(config)`
|
|
423
499
|
|
|
424
|
-
|
|
500
|
+
2D grid layout with virtualized rows.
|
|
425
501
|
|
|
426
|
-
|
|
502
|
+
```typescript
|
|
503
|
+
interface GridConfig {
|
|
504
|
+
columns: number; // Number of columns (required)
|
|
505
|
+
gap?: number; // Gap between items in pixels (default: 0)
|
|
506
|
+
}
|
|
507
|
+
```
|
|
427
508
|
|
|
428
|
-
|
|
429
|
-
|
|
509
|
+
**Example:**
|
|
510
|
+
```typescript
|
|
511
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
512
|
+
```
|
|
430
513
|
|
|
431
|
-
|
|
432
|
-
const { containerRef, instanceRef } = useVList({
|
|
433
|
-
item: {
|
|
434
|
-
height: 48,
|
|
435
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
436
|
-
},
|
|
437
|
-
items: users,
|
|
438
|
-
selection: { mode: 'single' },
|
|
439
|
-
});
|
|
514
|
+
### Plugin: `withSections(config)`
|
|
440
515
|
|
|
441
|
-
|
|
442
|
-
useVListEvent(instanceRef, 'selection:change', ({ selected }) => {
|
|
443
|
-
console.log('Selected:', selected);
|
|
444
|
-
});
|
|
516
|
+
Grouped lists with sticky or inline headers.
|
|
445
517
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
);
|
|
518
|
+
```typescript
|
|
519
|
+
interface SectionsConfig {
|
|
520
|
+
getGroupForIndex: (index: number) => string;
|
|
521
|
+
headerHeight: number | ((group: string, groupIndex: number) => number);
|
|
522
|
+
headerTemplate: (group: string, groupIndex: number) => string | HTMLElement;
|
|
523
|
+
sticky?: boolean; // Default: true (Telegram style), false = inline (iMessage style)
|
|
453
524
|
}
|
|
454
525
|
```
|
|
455
526
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
527
|
+
**Example:**
|
|
528
|
+
```typescript
|
|
529
|
+
.use(withSections({
|
|
530
|
+
getGroupForIndex: (i) => items[i].category,
|
|
531
|
+
headerHeight: 40,
|
|
532
|
+
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
533
|
+
sticky: true,
|
|
534
|
+
}))
|
|
535
|
+
```
|
|
460
536
|
|
|
461
|
-
Items
|
|
537
|
+
**Important:** Items must be pre-sorted by group!
|
|
462
538
|
|
|
463
|
-
|
|
539
|
+
### Plugin: `withSelection(config)`
|
|
464
540
|
|
|
465
|
-
|
|
466
|
-
<template>
|
|
467
|
-
<div ref="containerRef" style="height: 400px" />
|
|
468
|
-
</template>
|
|
541
|
+
Single or multiple item selection with keyboard navigation.
|
|
469
542
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
543
|
+
```typescript
|
|
544
|
+
interface SelectionConfig {
|
|
545
|
+
mode: 'single' | 'multiple';
|
|
546
|
+
initial?: Array<string | number>; // Pre-selected item IDs
|
|
547
|
+
}
|
|
548
|
+
```
|
|
473
549
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
550
|
+
**Example:**
|
|
551
|
+
```typescript
|
|
552
|
+
.use(withSelection({
|
|
553
|
+
mode: 'multiple',
|
|
554
|
+
initial: [1, 5, 10],
|
|
555
|
+
}))
|
|
556
|
+
```
|
|
478
557
|
|
|
479
|
-
|
|
480
|
-
item: {
|
|
481
|
-
height: 48,
|
|
482
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
483
|
-
},
|
|
484
|
-
items: users.value,
|
|
485
|
-
});
|
|
558
|
+
### Plugin: `withAsync(config)`
|
|
486
559
|
|
|
487
|
-
|
|
488
|
-
useVListEvent(instance, 'selection:change', ({ selected }) => {
|
|
489
|
-
console.log('Selected:', selected);
|
|
490
|
-
});
|
|
560
|
+
Asynchronous data loading with lazy loading and placeholders.
|
|
491
561
|
|
|
492
|
-
|
|
493
|
-
|
|
562
|
+
```typescript
|
|
563
|
+
interface AsyncConfig {
|
|
564
|
+
adapter: {
|
|
565
|
+
read: (params: { offset: number, limit: number }) => Promise<{
|
|
566
|
+
items: T[];
|
|
567
|
+
total?: number;
|
|
568
|
+
hasMore?: boolean;
|
|
569
|
+
}>;
|
|
570
|
+
};
|
|
571
|
+
loading?: {
|
|
572
|
+
cancelThreshold?: number; // Cancel loads when scrolling fast (px/ms)
|
|
573
|
+
};
|
|
494
574
|
}
|
|
495
|
-
</script>
|
|
496
575
|
```
|
|
497
576
|
|
|
498
|
-
|
|
577
|
+
**Example:**
|
|
578
|
+
```typescript
|
|
579
|
+
.use(withAsync({
|
|
580
|
+
adapter: {
|
|
581
|
+
read: async ({ offset, limit }) => {
|
|
582
|
+
const response = await fetch(`/api?offset=${offset}&limit=${limit}`);
|
|
583
|
+
return response.json();
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
loading: {
|
|
587
|
+
cancelThreshold: 15, // Skip loads when scrolling > 15px/ms
|
|
588
|
+
},
|
|
589
|
+
}))
|
|
590
|
+
```
|
|
499
591
|
|
|
500
|
-
|
|
592
|
+
### Plugin: `withScale()`
|
|
501
593
|
|
|
502
|
-
|
|
503
|
-
<script>
|
|
504
|
-
import { vlist, onVListEvent } from 'vlist/svelte';
|
|
594
|
+
Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
|
|
505
595
|
|
|
506
|
-
|
|
507
|
-
|
|
596
|
+
```typescript
|
|
597
|
+
.use(withScale()) // No config needed - auto-activates
|
|
598
|
+
```
|
|
508
599
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
instance = inst;
|
|
520
|
-
unsubs.push(
|
|
521
|
-
onVListEvent(inst, 'selection:change', ({ selected }) => {
|
|
522
|
-
console.log('Selected:', selected);
|
|
523
|
-
})
|
|
524
|
-
);
|
|
525
|
-
},
|
|
526
|
-
};
|
|
600
|
+
**Example:**
|
|
601
|
+
```typescript
|
|
602
|
+
const bigList = vlist({
|
|
603
|
+
container: '#list',
|
|
604
|
+
items: generateItems(5_000_000),
|
|
605
|
+
item: { height: 48, template: renderItem },
|
|
606
|
+
})
|
|
607
|
+
.use(withScale())
|
|
608
|
+
.build();
|
|
609
|
+
```
|
|
527
610
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
611
|
+
### Plugin: `withScrollbar(config)`
|
|
612
|
+
|
|
613
|
+
Custom scrollbar with auto-hide and smooth dragging.
|
|
531
614
|
|
|
532
|
-
|
|
533
|
-
|
|
615
|
+
```typescript
|
|
616
|
+
interface ScrollbarConfig {
|
|
617
|
+
autoHide?: boolean; // Hide when not scrolling (default: false)
|
|
618
|
+
autoHideDelay?: number; // Hide delay in ms (default: 1000)
|
|
619
|
+
minThumbSize?: number; // Minimum thumb size in px (default: 20)
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Example:**
|
|
624
|
+
```typescript
|
|
625
|
+
.use(withScrollbar({
|
|
626
|
+
autoHide: true,
|
|
627
|
+
autoHideDelay: 1500,
|
|
628
|
+
}))
|
|
534
629
|
```
|
|
535
630
|
|
|
536
|
-
|
|
631
|
+
### Plugin: `withPage()`
|
|
537
632
|
|
|
538
|
-
|
|
633
|
+
Use document-level scrolling instead of container scrolling.
|
|
539
634
|
|
|
540
635
|
```typescript
|
|
541
|
-
|
|
542
|
-
container: '#my-list',
|
|
543
|
-
item: {
|
|
544
|
-
height: 72,
|
|
545
|
-
template: (item, index, { selected, focused }) => {
|
|
546
|
-
// Return an HTMLElement for more control
|
|
547
|
-
const el = document.createElement('div');
|
|
548
|
-
el.className = 'item-content';
|
|
549
|
-
el.innerHTML = `
|
|
550
|
-
<img src="${item.avatar}" class="avatar avatar--large" />
|
|
551
|
-
<div class="item-details">
|
|
552
|
-
<div class="item-name">${item.name}</div>
|
|
553
|
-
<div class="item-email">${item.email}</div>
|
|
554
|
-
</div>
|
|
555
|
-
<div class="item-role">${item.role}</div>
|
|
556
|
-
`;
|
|
557
|
-
return el;
|
|
558
|
-
},
|
|
559
|
-
},
|
|
560
|
-
items: users,
|
|
561
|
-
});
|
|
636
|
+
.use(withPage()) // No config needed
|
|
562
637
|
```
|
|
563
638
|
|
|
564
|
-
|
|
639
|
+
**Example:**
|
|
640
|
+
```typescript
|
|
641
|
+
const list = vlist({
|
|
642
|
+
container: '#list',
|
|
643
|
+
items: articles,
|
|
644
|
+
item: { height: 300, template: renderArticle },
|
|
645
|
+
})
|
|
646
|
+
.use(withPage())
|
|
647
|
+
.build();
|
|
648
|
+
```
|
|
565
649
|
|
|
566
|
-
|
|
650
|
+
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
567
651
|
|
|
568
|
-
|
|
652
|
+
### Plugin: `withSnapshots()`
|
|
653
|
+
|
|
654
|
+
Scroll position save/restore for SPA navigation.
|
|
569
655
|
|
|
570
656
|
```typescript
|
|
571
|
-
|
|
572
|
-
list.
|
|
573
|
-
list.
|
|
574
|
-
list.updateItem(id, updates) // Update item by ID
|
|
575
|
-
list.removeItem(id) // Remove item by ID
|
|
576
|
-
list.reload() // Reload from adapter
|
|
657
|
+
// Included in base - no need to import
|
|
658
|
+
const snapshot = list.getScrollSnapshot();
|
|
659
|
+
list.restoreScroll(snapshot);
|
|
577
660
|
```
|
|
578
661
|
|
|
579
|
-
|
|
580
|
-
|
|
662
|
+
**Example:**
|
|
581
663
|
```typescript
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
list.scrollToItem(id, options?) // Scroll to item with options
|
|
586
|
-
list.cancelScroll() // Cancel in-progress smooth scroll
|
|
587
|
-
list.getScrollPosition() // Get current scroll position
|
|
588
|
-
list.getScrollSnapshot() // Get snapshot for save/restore
|
|
589
|
-
list.restoreScroll(snapshot) // Restore position (and selection) from snapshot
|
|
664
|
+
// Save on navigation away
|
|
665
|
+
const snapshot = list.getScrollSnapshot();
|
|
666
|
+
sessionStorage.setItem('scroll', JSON.stringify(snapshot));
|
|
590
667
|
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
|
|
668
|
+
// Restore on return
|
|
669
|
+
const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
|
|
670
|
+
list.restoreScroll(snapshot);
|
|
594
671
|
```
|
|
595
672
|
|
|
596
|
-
|
|
673
|
+
## Advanced Usage
|
|
674
|
+
|
|
675
|
+
### Variable Heights with DOM Measurement
|
|
597
676
|
|
|
598
677
|
```typescript
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
678
|
+
import { vlist } from 'vlist';
|
|
679
|
+
|
|
680
|
+
// Measure items before creating list
|
|
681
|
+
const measuringDiv = document.createElement('div');
|
|
682
|
+
measuringDiv.style.cssText = 'position:absolute;visibility:hidden;width:400px';
|
|
683
|
+
document.body.appendChild(measuringDiv);
|
|
684
|
+
|
|
685
|
+
items.forEach(item => {
|
|
686
|
+
measuringDiv.innerHTML = renderItem(item);
|
|
687
|
+
item.measuredHeight = measuringDiv.offsetHeight;
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
document.body.removeChild(measuringDiv);
|
|
691
|
+
|
|
692
|
+
// Use measured heights
|
|
693
|
+
const list = vlist({
|
|
694
|
+
container: '#list',
|
|
695
|
+
items,
|
|
696
|
+
item: {
|
|
697
|
+
height: (i) => items[i].measuredHeight,
|
|
698
|
+
template: renderItem,
|
|
699
|
+
},
|
|
700
|
+
}).build();
|
|
606
701
|
```
|
|
607
702
|
|
|
608
|
-
|
|
703
|
+
### Dynamic Grid Columns
|
|
609
704
|
|
|
610
705
|
```typescript
|
|
611
|
-
|
|
612
|
-
|
|
706
|
+
import { vlist, withGrid } from 'vlist';
|
|
707
|
+
|
|
708
|
+
let currentColumns = 4;
|
|
709
|
+
|
|
710
|
+
const list = vlist({
|
|
711
|
+
container: '#gallery',
|
|
712
|
+
items: photos,
|
|
713
|
+
item: {
|
|
714
|
+
height: 200,
|
|
715
|
+
template: renderPhoto,
|
|
716
|
+
},
|
|
717
|
+
})
|
|
718
|
+
.use(withGrid({ columns: currentColumns, gap: 16 }))
|
|
719
|
+
.build();
|
|
720
|
+
|
|
721
|
+
// Update grid on resize
|
|
722
|
+
window.addEventListener('resize', () => {
|
|
723
|
+
const width = container.clientWidth;
|
|
724
|
+
const newColumns = width > 1200 ? 6 : width > 800 ? 4 : 2;
|
|
725
|
+
if (newColumns !== currentColumns) {
|
|
726
|
+
currentColumns = newColumns;
|
|
727
|
+
list.updateGrid({ columns: newColumns });
|
|
728
|
+
}
|
|
729
|
+
});
|
|
613
730
|
```
|
|
614
731
|
|
|
615
|
-
|
|
732
|
+
### Combining Multiple Plugins
|
|
616
733
|
|
|
617
734
|
```typescript
|
|
618
|
-
|
|
735
|
+
import {
|
|
736
|
+
vlist,
|
|
737
|
+
withGrid,
|
|
738
|
+
withSections,
|
|
739
|
+
withSelection,
|
|
740
|
+
withAsync,
|
|
741
|
+
withScrollbar
|
|
742
|
+
} from 'vlist';
|
|
743
|
+
|
|
744
|
+
const list = vlist({
|
|
745
|
+
container: '#gallery',
|
|
746
|
+
item: {
|
|
747
|
+
height: 200,
|
|
748
|
+
template: renderPhoto,
|
|
749
|
+
},
|
|
750
|
+
})
|
|
751
|
+
.use(withAsync({ adapter: photoAdapter }))
|
|
752
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
753
|
+
.use(withSections({
|
|
754
|
+
getGroupForIndex: (i) => items[i]?.category || 'Loading...',
|
|
755
|
+
headerHeight: 48,
|
|
756
|
+
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
757
|
+
}))
|
|
758
|
+
.use(withSelection({ mode: 'multiple' }))
|
|
759
|
+
.use(withScrollbar({ autoHide: true }))
|
|
760
|
+
.build();
|
|
619
761
|
```
|
|
620
762
|
|
|
621
|
-
|
|
763
|
+
**Bundle:** ~15 KB gzipped (includes only used plugins)
|
|
764
|
+
|
|
765
|
+
## Framework Adapters
|
|
766
|
+
|
|
767
|
+
### React
|
|
622
768
|
|
|
623
769
|
```typescript
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
list.total // Total item count
|
|
627
|
-
```
|
|
770
|
+
import { vlist, withSelection } from 'vlist';
|
|
771
|
+
import { useEffect, useRef } from 'react';
|
|
628
772
|
|
|
629
|
-
|
|
773
|
+
function MyList({ items }) {
|
|
774
|
+
const containerRef = useRef(null);
|
|
775
|
+
const listRef = useRef(null);
|
|
630
776
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
| `item:click` | `{ item, index, event }` | Item was clicked |
|
|
634
|
-
| `selection:change` | `{ selected, items }` | Selection changed |
|
|
635
|
-
| `scroll` | `{ scrollTop, direction }` | Scroll position changed |
|
|
636
|
-
| `range:change` | `{ range }` | Visible range changed |
|
|
637
|
-
| `resize` | `{ height, width }` | Container was resized |
|
|
638
|
-
| `load:start` | `{ offset, limit }` | Data loading started |
|
|
639
|
-
| `load:end` | `{ items, total }` | Data loading completed |
|
|
640
|
-
| `error` | `{ error, context }` | Error occurred |
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (!containerRef.current) return;
|
|
641
779
|
|
|
642
|
-
|
|
780
|
+
listRef.current = vlist({
|
|
781
|
+
container: containerRef.current,
|
|
782
|
+
items,
|
|
783
|
+
item: { height: 48, template: renderItem },
|
|
784
|
+
})
|
|
785
|
+
.use(withSelection({ mode: 'single' }))
|
|
786
|
+
.build();
|
|
643
787
|
|
|
644
|
-
|
|
788
|
+
return () => listRef.current?.destroy();
|
|
789
|
+
}, []);
|
|
645
790
|
|
|
646
|
-
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
listRef.current?.setItems(items);
|
|
793
|
+
}, [items]);
|
|
794
|
+
|
|
795
|
+
return <div ref={containerRef} />;
|
|
796
|
+
}
|
|
797
|
+
```
|
|
647
798
|
|
|
648
|
-
|
|
799
|
+
### Vue 3
|
|
649
800
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
801
|
+
```typescript
|
|
802
|
+
import { vlist, withSelection } from 'vlist';
|
|
803
|
+
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
|
804
|
+
|
|
805
|
+
export default {
|
|
806
|
+
setup() {
|
|
807
|
+
const container = ref(null);
|
|
808
|
+
const list = ref(null);
|
|
809
|
+
|
|
810
|
+
onMounted(() => {
|
|
811
|
+
list.value = vlist({
|
|
812
|
+
container: container.value,
|
|
813
|
+
items: items.value,
|
|
814
|
+
item: { height: 48, template: renderItem },
|
|
815
|
+
})
|
|
816
|
+
.use(withSelection({ mode: 'single' }))
|
|
817
|
+
.build();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
watch(items, (newItems) => {
|
|
821
|
+
list.value?.setItems(newItems);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
onUnmounted(() => {
|
|
825
|
+
list.value?.destroy();
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
return { container };
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
```
|
|
657
832
|
|
|
658
|
-
###
|
|
833
|
+
### Svelte
|
|
659
834
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
835
|
+
```typescript
|
|
836
|
+
<script>
|
|
837
|
+
import { vlist, withSelection } from 'vlist';
|
|
838
|
+
import { onMount, onDestroy } from 'svelte';
|
|
839
|
+
|
|
840
|
+
export let items = [];
|
|
841
|
+
|
|
842
|
+
let container;
|
|
843
|
+
let list;
|
|
844
|
+
|
|
845
|
+
onMount(() => {
|
|
846
|
+
list = vlist({
|
|
847
|
+
container,
|
|
848
|
+
items,
|
|
849
|
+
item: { height: 48, template: renderItem },
|
|
850
|
+
})
|
|
851
|
+
.use(withSelection({ mode: 'single' }))
|
|
852
|
+
.build();
|
|
853
|
+
});
|
|
670
854
|
|
|
671
|
-
|
|
855
|
+
$: list?.setItems(items);
|
|
672
856
|
|
|
673
|
-
|
|
857
|
+
onDestroy(() => {
|
|
858
|
+
list?.destroy();
|
|
859
|
+
});
|
|
860
|
+
</script>
|
|
674
861
|
|
|
675
|
-
|
|
862
|
+
<div bind:this={container}></div>
|
|
863
|
+
```
|
|
676
864
|
|
|
677
865
|
## Styling
|
|
678
866
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
Import the default styles:
|
|
867
|
+
Import the base styles:
|
|
682
868
|
|
|
683
869
|
```typescript
|
|
684
870
|
import 'vlist/styles';
|
|
685
871
|
```
|
|
686
872
|
|
|
687
|
-
|
|
873
|
+
Or customize with your own CSS:
|
|
688
874
|
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
|
|
875
|
+
```css
|
|
876
|
+
.vlist {
|
|
877
|
+
/* Container styles */
|
|
878
|
+
}
|
|
692
879
|
|
|
693
|
-
|
|
880
|
+
.vlist-viewport {
|
|
881
|
+
/* Scroll viewport */
|
|
882
|
+
}
|
|
694
883
|
|
|
695
|
-
|
|
884
|
+
.vlist-item {
|
|
885
|
+
/* Item wrapper - positioned absolutely */
|
|
886
|
+
}
|
|
696
887
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
- `.vlist-items` - Items container
|
|
701
|
-
- `.vlist-item` - Individual item
|
|
702
|
-
- `.vlist-item--selected` - Selected item
|
|
703
|
-
- `.vlist-item--focused` - Focused item (keyboard nav)
|
|
704
|
-
- `.vlist--grid` - Grid layout modifier
|
|
705
|
-
- `.vlist-grid-item` - Grid item (positioned with `translate(x, y)`)
|
|
706
|
-
- `.vlist--grouped` - Grouped list modifier
|
|
707
|
-
- `.vlist-sticky-header` - Sticky header overlay
|
|
708
|
-
- `.vlist-live-region` - Visually-hidden live region for screen reader announcements
|
|
709
|
-
- `.vlist--scrolling` - Applied during active scroll (disables transitions)
|
|
888
|
+
.vlist-item--selected {
|
|
889
|
+
/* Selected state */
|
|
890
|
+
}
|
|
710
891
|
|
|
711
|
-
|
|
892
|
+
.vlist-item--focused {
|
|
893
|
+
/* Focused state (keyboard navigation) */
|
|
894
|
+
}
|
|
712
895
|
|
|
713
|
-
|
|
896
|
+
.vlist-scrollbar {
|
|
897
|
+
/* Custom scrollbar track */
|
|
898
|
+
}
|
|
714
899
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
900
|
+
.vlist-scrollbar__thumb {
|
|
901
|
+
/* Scrollbar thumb/handle */
|
|
902
|
+
}
|
|
903
|
+
```
|
|
718
904
|
|
|
719
|
-
|
|
720
|
-
<div class="vlist vlist--comfortable">...</div>
|
|
905
|
+
## Performance
|
|
721
906
|
|
|
722
|
-
|
|
723
|
-
<div class="vlist vlist--borderless">...</div>
|
|
907
|
+
### Bundle Sizes (Gzipped)
|
|
724
908
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
909
|
+
| Configuration | Size | What's Included |
|
|
910
|
+
|---------------|------|-----------------|
|
|
911
|
+
| Base only | 7.7 KB | Core virtualization |
|
|
912
|
+
| + Grid | 11.7 KB | + 2D layout |
|
|
913
|
+
| + Sections | 12.3 KB | + Grouped lists |
|
|
914
|
+
| + Selection | 10.0 KB | + Item selection |
|
|
915
|
+
| + Async | 13.5 KB | + Data loading |
|
|
916
|
+
| + Scale | 9.9 KB | + 1M+ items |
|
|
917
|
+
| + All plugins | ~16 KB | Everything |
|
|
728
918
|
|
|
729
|
-
|
|
919
|
+
**Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
|
|
730
920
|
|
|
731
|
-
|
|
921
|
+
### Memory Efficiency
|
|
732
922
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
923
|
+
With 100,000 items at 48px each:
|
|
924
|
+
- **Total height:** 4,800,000 pixels
|
|
925
|
+
- **Visible items:** ~20 (depending on viewport height)
|
|
926
|
+
- **DOM nodes:** ~26 (visible + overscan)
|
|
927
|
+
- **Memory saved:** ~99.97% (26 DOM nodes vs 100,000)
|
|
928
|
+
|
|
929
|
+
### Benchmarks
|
|
930
|
+
|
|
931
|
+
See [benchmarks](https://vlist.dev/benchmarks/) for detailed performance comparisons.
|
|
932
|
+
|
|
933
|
+
## Browser Support
|
|
934
|
+
|
|
935
|
+
- Chrome/Edge: Latest 2 versions
|
|
936
|
+
- Firefox: Latest 2 versions
|
|
937
|
+
- Safari: Latest 2 versions
|
|
938
|
+
- iOS Safari: 12.4+
|
|
939
|
+
- Chrome Android: Latest
|
|
747
940
|
|
|
748
|
-
|
|
941
|
+
Relies on:
|
|
942
|
+
- `IntersectionObserver` (widely supported)
|
|
943
|
+
- `ResizeObserver` (polyfill available if needed)
|
|
944
|
+
- CSS `transform` (universal support)
|
|
749
945
|
|
|
750
946
|
## TypeScript
|
|
751
947
|
|
|
752
|
-
|
|
948
|
+
Fully typed with comprehensive TypeScript definitions:
|
|
753
949
|
|
|
754
950
|
```typescript
|
|
755
|
-
|
|
951
|
+
import { vlist, withGrid, type VList, type VListConfig } from 'vlist';
|
|
952
|
+
|
|
953
|
+
interface Photo {
|
|
756
954
|
id: number;
|
|
757
|
-
|
|
758
|
-
|
|
955
|
+
url: string;
|
|
956
|
+
title: string;
|
|
759
957
|
}
|
|
760
958
|
|
|
761
|
-
const
|
|
762
|
-
container: '#
|
|
959
|
+
const config: VListConfig<Photo> = {
|
|
960
|
+
container: '#gallery',
|
|
961
|
+
items: photos,
|
|
763
962
|
item: {
|
|
764
|
-
height:
|
|
765
|
-
template: (
|
|
963
|
+
height: 200,
|
|
964
|
+
template: (photo: Photo) => `<img src="${photo.url}" />`,
|
|
766
965
|
},
|
|
767
|
-
|
|
768
|
-
});
|
|
966
|
+
};
|
|
769
967
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
});
|
|
968
|
+
const list: VList<Photo> = vlist(config)
|
|
969
|
+
.use(withGrid({ columns: 4 }))
|
|
970
|
+
.build();
|
|
774
971
|
```
|
|
775
972
|
|
|
776
|
-
##
|
|
777
|
-
|
|
778
|
-
vlist is designed for maximum performance with extensive built-in optimizations:
|
|
973
|
+
## Migration from Monolithic API
|
|
779
974
|
|
|
780
|
-
|
|
781
|
-
- **Element pooling** - DOM elements are recycled via `createElementPool()`, reducing GC pressure
|
|
782
|
-
- **Zero-allocation scroll hot path** - No object allocations per scroll frame; in-place range mutation
|
|
783
|
-
- **RAF-throttled native scroll** - At most one scroll processing per animation frame
|
|
784
|
-
- **CSS containment** - `contain: layout style` on items container, `contain: content` + `will-change: transform` on items
|
|
785
|
-
- **Scroll transition suppression** - `.vlist--scrolling` class disables CSS transitions during active scroll
|
|
786
|
-
- **Circular buffer velocity tracker** - Pre-allocated buffer, zero allocations during scroll
|
|
787
|
-
- **Targeted keyboard focus render** - Arrow keys update only 2 affected items instead of all visible items
|
|
788
|
-
- **Batched LRU timestamps** - Single `Date.now()` per render cycle instead of per-item
|
|
789
|
-
- **DocumentFragment batching** - New elements appended in a single DOM operation
|
|
790
|
-
- **Split CSS** - Core styles (5.6 KB) separated from optional extras (1.8 KB)
|
|
791
|
-
- **Configurable velocity-based loading** - Skip, preload, or defer loading based on scroll speed
|
|
792
|
-
- **Compression for 1M+ items** - Automatic scroll space compression when content exceeds browser height limits
|
|
975
|
+
If you're using the old monolithic API:
|
|
793
976
|
|
|
794
|
-
###
|
|
977
|
+
### Before (Monolithic - Deprecated)
|
|
795
978
|
|
|
796
|
-
|
|
979
|
+
```typescript
|
|
980
|
+
import { createVList } from 'vlist';
|
|
797
981
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
| Memory (scroll delta) | 0 MB | 0 MB |
|
|
982
|
+
const list = createVList({
|
|
983
|
+
container: '#app',
|
|
984
|
+
items: data,
|
|
985
|
+
grid: { columns: 4 },
|
|
986
|
+
groups: { ... },
|
|
987
|
+
selection: { mode: 'single' },
|
|
988
|
+
});
|
|
989
|
+
```
|
|
807
990
|
|
|
808
|
-
|
|
991
|
+
### After (Builder - Recommended)
|
|
809
992
|
|
|
810
|
-
|
|
993
|
+
```typescript
|
|
994
|
+
import { vlist, withGrid, withSections, withSelection } from 'vlist';
|
|
995
|
+
|
|
996
|
+
const list = vlist({
|
|
997
|
+
container: '#app',
|
|
998
|
+
items: data,
|
|
999
|
+
})
|
|
1000
|
+
.use(withGrid({ columns: 4 }))
|
|
1001
|
+
.use(withSections({ ... }))
|
|
1002
|
+
.use(withSelection({ mode: 'single' }))
|
|
1003
|
+
.build();
|
|
1004
|
+
```
|
|
811
1005
|
|
|
812
|
-
|
|
1006
|
+
**Benefits:**
|
|
1007
|
+
- 2-3x smaller bundles (20 KB → 8-12 KB gzipped)
|
|
1008
|
+
- Explicit about what's included
|
|
1009
|
+
- Better tree-shaking
|
|
1010
|
+
- Easier to understand and debug
|
|
813
1011
|
|
|
814
|
-
|
|
815
|
-
- Firefox 55+
|
|
816
|
-
- Safari 12+
|
|
817
|
-
- Edge 79+
|
|
1012
|
+
## Plugin Naming Changes
|
|
818
1013
|
|
|
819
|
-
|
|
1014
|
+
If you're upgrading from an earlier version:
|
|
820
1015
|
|
|
821
|
-
|
|
1016
|
+
| Old Name | New Name | Import |
|
|
1017
|
+
|----------|----------|--------|
|
|
1018
|
+
| `withCompression()` | `withScale()` | `import { withScale } from 'vlist'` |
|
|
1019
|
+
| `withData()` | `withAsync()` | `import { withAsync } from 'vlist'` |
|
|
1020
|
+
| `withWindow()` | `withPage()` | `import { withPage } from 'vlist'` |
|
|
1021
|
+
| `withGroups()` | `withSections()` | `import { withSections } from 'vlist'` |
|
|
1022
|
+
| `withScroll()` | `withScrollbar()` | `import { withScrollbar } from 'vlist'` |
|
|
822
1023
|
|
|
823
|
-
|
|
824
|
-
# Install dependencies
|
|
825
|
-
bun install
|
|
1024
|
+
## Contributing
|
|
826
1025
|
|
|
827
|
-
|
|
828
|
-
bun run dev
|
|
1026
|
+
Contributions are welcome! Please:
|
|
829
1027
|
|
|
830
|
-
|
|
831
|
-
|
|
1028
|
+
1. Fork the repository
|
|
1029
|
+
2. Create a feature branch
|
|
1030
|
+
3. Make your changes
|
|
1031
|
+
4. Add/update tests
|
|
1032
|
+
5. Submit a pull request
|
|
832
1033
|
|
|
833
|
-
|
|
834
|
-
bun run typecheck
|
|
1034
|
+
## License
|
|
835
1035
|
|
|
836
|
-
|
|
837
|
-
bun run build
|
|
838
|
-
```
|
|
1036
|
+
MIT License - see [LICENSE](LICENSE) for details
|
|
839
1037
|
|
|
840
|
-
##
|
|
1038
|
+
## Links
|
|
841
1039
|
|
|
842
|
-
|
|
1040
|
+
- **Documentation & Examples:** [vlist.dev](https://vlist.dev)
|
|
1041
|
+
- **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
|
|
1042
|
+
- **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
|
|
1043
|
+
- **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
|
|
843
1044
|
|
|
844
|
-
|
|
1045
|
+
---
|
|
845
1046
|
|
|
846
|
-
|
|
1047
|
+
**Built by [Floor IO](https://floor.io)** with ❤️ for the web community
|