@floor/vlist 0.5.8 → 0.6.1
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 +787 -611
- 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/types.d.ts +3 -3
- package/dist/builder/types.d.ts.map +1 -1
- package/dist/core/full.d.ts +22 -0
- package/dist/core/full.d.ts.map +1 -0
- 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/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 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/page/index.js +1 -0
- package/dist/react/index.js +1 -1
- 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/scrollbar/index.js +1 -0
- package/dist/sections/index.js +1 -0
- package/dist/selection/index.js +1 -1
- package/dist/svelte/index.js +1 -1
- package/dist/vue/index.js +1 -1
- package/package.json +1 -51
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 + Groups](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,821 +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
|
|
57
|
-
|
|
58
|
-
For smaller bundles, import only what you need:
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
import { createVList } from '@floor/vlist' // full library (70 KB / 23 KB gzip)
|
|
62
|
-
import { createVList } from '@floor/vlist/core' // lightweight core (8 KB / 3 KB gzip)
|
|
63
|
-
import { createVList } from '@floor/vlist/core-light' // ultra-minimal (5 KB / 2 KB gzip)
|
|
64
|
-
import { createVList } from '@floor/vlist/builder' // declarative API (17 KB / 6 KB gzip)
|
|
65
|
-
import { createGridLayout } from '@floor/vlist/grid' // grid layout utilities only
|
|
66
|
-
import { createSparseStorage } from '@floor/vlist/data' // data utilities only
|
|
67
|
-
import { getCompressionInfo } from '@floor/vlist/compression' // compression utilities only
|
|
68
|
-
import { createSelectionState } from '@floor/vlist/selection' // selection utilities only
|
|
69
|
-
import { createScrollController } from '@floor/vlist/scroll' // scroll utilities only
|
|
70
|
-
import { createGroupLayout } from '@floor/vlist/groups' // group/sticky header utilities only
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
| Import | Minified | Gzipped | Description |
|
|
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 |
|
|
85
|
-
|
|
86
|
-
### Framework Adapters
|
|
87
|
-
|
|
88
|
-
Thin wrappers for React, Vue, and Svelte — each under 1 KB:
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { useVList } from '@floor/vlist/react' // React hook (62 KB / 21 KB gzip)
|
|
92
|
-
import { useVList } from '@floor/vlist/vue' // Vue 3 composable (62 KB / 21 KB gzip)
|
|
93
|
-
import { vlist } from '@floor/vlist/svelte' // Svelte action (62 KB / 20 KB gzip)
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
| Import | Minified | Gzipped | Description |
|
|
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` |
|
|
101
|
-
|
|
102
|
-
Adapters manage the vlist lifecycle (create on mount, destroy on unmount) and sync items reactively. See [Framework Adapters](#framework-adapters) for full examples.
|
|
43
|
+
> **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
|
|
103
44
|
|
|
104
45
|
## Quick Start
|
|
105
46
|
|
|
106
47
|
```typescript
|
|
107
|
-
import {
|
|
108
|
-
import '
|
|
48
|
+
import { vlist } from 'vlist';
|
|
49
|
+
import 'vlist/styles';
|
|
109
50
|
|
|
110
|
-
const list =
|
|
51
|
+
const list = vlist({
|
|
111
52
|
container: '#my-list',
|
|
112
|
-
ariaLabel: 'Contact list',
|
|
113
|
-
item: {
|
|
114
|
-
height: 48,
|
|
115
|
-
template: (item) => `
|
|
116
|
-
<div class="item-content">
|
|
117
|
-
<img src="${item.avatar}" class="avatar" />
|
|
118
|
-
<span>${item.name}</span>
|
|
119
|
-
</div>
|
|
120
|
-
`,
|
|
121
|
-
},
|
|
122
53
|
items: [
|
|
123
|
-
{ id: 1, name: 'Alice'
|
|
124
|
-
{ id: 2, name: 'Bob'
|
|
125
|
-
|
|
54
|
+
{ id: 1, name: 'Alice' },
|
|
55
|
+
{ id: 2, name: 'Bob' },
|
|
56
|
+
{ id: 3, name: 'Charlie' },
|
|
126
57
|
],
|
|
127
|
-
});
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Lightweight Core (7.8 KB)
|
|
131
|
-
|
|
132
|
-
If you don't need selection, groups, grid, compression, custom scrollbar, or async data adapters, use the lightweight core for an **83% smaller bundle**:
|
|
133
|
-
|
|
134
|
-
```typescript
|
|
135
|
-
import { createVList } from '@floor/vlist/core';
|
|
136
|
-
import '@floor/vlist/styles';
|
|
137
|
-
|
|
138
|
-
const list = createVList({
|
|
139
|
-
container: '#my-list',
|
|
140
58
|
item: {
|
|
141
59
|
height: 48,
|
|
142
60
|
template: (item) => `<div>${item.name}</div>`,
|
|
143
61
|
},
|
|
144
|
-
|
|
145
|
-
});
|
|
62
|
+
}).build();
|
|
146
63
|
|
|
147
|
-
//
|
|
64
|
+
// API methods
|
|
65
|
+
list.scrollToIndex(10);
|
|
66
|
+
list.setItems(newItems);
|
|
148
67
|
list.on('item:click', ({ item }) => console.log(item));
|
|
149
|
-
list.scrollToIndex(50, { behavior: 'smooth' });
|
|
150
68
|
```
|
|
151
69
|
|
|
152
|
-
|
|
70
|
+
**Bundle:** ~8 KB gzipped
|
|
153
71
|
|
|
154
|
-
##
|
|
72
|
+
## Builder Pattern
|
|
155
73
|
|
|
156
|
-
|
|
157
|
-
interface VListConfig<T> {
|
|
158
|
-
// Required
|
|
159
|
-
container: HTMLElement | string; // Container element or selector
|
|
160
|
-
item: {
|
|
161
|
-
height: number | ((index: number) => number); // Fixed or variable height
|
|
162
|
-
template: ItemTemplate<T>; // Render function for each item
|
|
163
|
-
};
|
|
74
|
+
VList uses a composable builder pattern. Start with the base, add only the features you need:
|
|
164
75
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
};
|
|
76
|
+
```typescript
|
|
77
|
+
import { vlist, withGrid, withSections, withSelection } from 'vlist';
|
|
171
78
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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();
|
|
95
|
+
```
|
|
175
96
|
|
|
176
|
-
|
|
177
|
-
overscan?: number; // Extra items to render (default: 3)
|
|
178
|
-
scroll?: {
|
|
179
|
-
wheel?: boolean; // Enable mouse wheel (default: true)
|
|
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
|
-
};
|
|
97
|
+
**Bundle:** ~12 KB gzipped (only includes used plugins)
|
|
186
98
|
|
|
187
|
-
|
|
188
|
-
selection?: SelectionConfig; // Selection configuration
|
|
189
|
-
groups?: GroupsConfig; // Sticky headers / grouped lists
|
|
190
|
-
loading?: LoadingConfig; // Velocity-based loading thresholds
|
|
99
|
+
### Available Plugins
|
|
191
100
|
|
|
192
|
-
|
|
193
|
-
|
|
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 |
|
|
194
112
|
|
|
195
|
-
|
|
196
|
-
classPrefix?: string; // CSS class prefix (default: 'vlist')
|
|
197
|
-
ariaLabel?: string; // Accessible label for the listbox
|
|
198
|
-
}
|
|
199
|
-
```
|
|
113
|
+
**Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
|
|
200
114
|
|
|
201
115
|
## Examples
|
|
202
116
|
|
|
203
|
-
###
|
|
117
|
+
### Simple List (No Plugins)
|
|
204
118
|
|
|
205
119
|
```typescript
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
gap: 8, // 8px gap between columns AND rows
|
|
212
|
-
},
|
|
120
|
+
import { vlist } from 'vlist';
|
|
121
|
+
|
|
122
|
+
const list = vlist({
|
|
123
|
+
container: '#list',
|
|
124
|
+
items: users,
|
|
213
125
|
item: {
|
|
214
|
-
height:
|
|
215
|
-
template: (
|
|
216
|
-
<div class="
|
|
217
|
-
<img src="${
|
|
218
|
-
<span>${
|
|
126
|
+
height: 64,
|
|
127
|
+
template: (user) => `
|
|
128
|
+
<div class="user">
|
|
129
|
+
<img src="${user.avatar}" />
|
|
130
|
+
<span>${user.name}</span>
|
|
219
131
|
</div>
|
|
220
132
|
`,
|
|
221
133
|
},
|
|
222
|
-
|
|
223
|
-
});
|
|
134
|
+
}).build();
|
|
224
135
|
```
|
|
225
136
|
|
|
226
|
-
|
|
137
|
+
**Bundle:** 8.2 KB gzipped
|
|
227
138
|
|
|
228
|
-
###
|
|
139
|
+
### Grid Layout
|
|
229
140
|
|
|
230
141
|
```typescript
|
|
231
|
-
|
|
232
|
-
|
|
142
|
+
import { vlist, withGrid, withScrollbar } from 'vlist';
|
|
143
|
+
|
|
144
|
+
const gallery = vlist({
|
|
145
|
+
container: '#gallery',
|
|
146
|
+
items: photos,
|
|
233
147
|
item: {
|
|
234
|
-
height:
|
|
235
|
-
template: (
|
|
148
|
+
height: 200,
|
|
149
|
+
template: (photo) => `
|
|
150
|
+
<div class="card">
|
|
151
|
+
<img src="${photo.url}" />
|
|
152
|
+
<span>${photo.title}</span>
|
|
153
|
+
</div>
|
|
154
|
+
`,
|
|
236
155
|
},
|
|
237
|
-
|
|
238
|
-
})
|
|
156
|
+
})
|
|
157
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
158
|
+
.use(withScrollbar({ autoHide: true }))
|
|
159
|
+
.build();
|
|
239
160
|
```
|
|
240
161
|
|
|
241
|
-
|
|
162
|
+
**Bundle:** 11.7 KB gzipped
|
|
242
163
|
|
|
243
|
-
|
|
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)
|
|
244
167
|
|
|
245
168
|
```typescript
|
|
246
|
-
|
|
169
|
+
import { vlist, withSections } from 'vlist';
|
|
170
|
+
|
|
171
|
+
const contacts = vlist({
|
|
247
172
|
container: '#contacts',
|
|
173
|
+
items: sortedContacts, // Must be pre-sorted by group
|
|
248
174
|
item: {
|
|
249
175
|
height: 56,
|
|
250
|
-
template: (
|
|
176
|
+
template: (contact) => `<div>${contact.name}</div>`,
|
|
251
177
|
},
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
getGroupForIndex: (
|
|
178
|
+
})
|
|
179
|
+
.use(withSections({
|
|
180
|
+
getGroupForIndex: (i) => contacts[i].lastName[0].toUpperCase(),
|
|
255
181
|
headerHeight: 36,
|
|
256
|
-
headerTemplate: (
|
|
257
|
-
sticky: true,
|
|
258
|
-
}
|
|
259
|
-
|
|
182
|
+
headerTemplate: (letter) => `<div class="header">${letter}</div>`,
|
|
183
|
+
sticky: true, // Headers stick to top (Telegram style)
|
|
184
|
+
}))
|
|
185
|
+
.build();
|
|
260
186
|
```
|
|
261
187
|
|
|
262
|
-
|
|
188
|
+
**Bundle:** 12.3 KB gzipped
|
|
189
|
+
|
|
190
|
+
Set `sticky: false` for inline headers (iMessage/WhatsApp style).
|
|
191
|
+
|
|
192
|
+
### Chat UI (Reverse + Sections)
|
|
263
193
|
|
|
264
194
|
```typescript
|
|
265
|
-
|
|
195
|
+
import { vlist, withSections } from 'vlist';
|
|
196
|
+
|
|
197
|
+
const chat = vlist({
|
|
266
198
|
container: '#messages',
|
|
267
|
-
reverse: true,
|
|
199
|
+
reverse: true, // Start at bottom, newest messages visible
|
|
200
|
+
items: messages, // Chronological order (oldest first)
|
|
268
201
|
item: {
|
|
269
|
-
height: (
|
|
202
|
+
height: (i) => messages[i].height || 60,
|
|
270
203
|
template: (msg) => `<div class="message">${msg.text}</div>`,
|
|
271
204
|
},
|
|
272
|
-
|
|
273
|
-
|
|
205
|
+
})
|
|
206
|
+
.use(withSections({
|
|
274
207
|
getGroupForIndex: (i) => {
|
|
275
208
|
const date = new Date(messages[i].timestamp);
|
|
276
|
-
return date.toLocaleDateString();
|
|
209
|
+
return date.toLocaleDateString(); // "Jan 15", "Jan 16", etc.
|
|
277
210
|
},
|
|
278
|
-
headerHeight:
|
|
211
|
+
headerHeight: 32,
|
|
279
212
|
headerTemplate: (date) => `<div class="date-header">${date}</div>`,
|
|
280
|
-
sticky: false,
|
|
281
|
-
}
|
|
282
|
-
|
|
213
|
+
sticky: false, // Inline date headers (iMessage style)
|
|
214
|
+
}))
|
|
215
|
+
.build();
|
|
216
|
+
|
|
217
|
+
// New messages - auto-scrolls to bottom
|
|
218
|
+
chat.appendItems([newMessage]);
|
|
219
|
+
|
|
220
|
+
// Load history - preserves scroll position
|
|
221
|
+
chat.prependItems(olderMessages);
|
|
283
222
|
```
|
|
284
223
|
|
|
285
|
-
|
|
224
|
+
**Bundle:** 11.9 KB gzipped
|
|
286
225
|
|
|
287
|
-
|
|
226
|
+
Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
|
|
227
|
+
|
|
228
|
+
### Large Datasets (1M+ Items)
|
|
288
229
|
|
|
289
230
|
```typescript
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
231
|
+
import { vlist, withScale, withScrollbar } from 'vlist';
|
|
232
|
+
|
|
233
|
+
const bigList = vlist({
|
|
234
|
+
container: '#big-list',
|
|
235
|
+
items: generateItems(5_000_000),
|
|
293
236
|
item: {
|
|
294
237
|
height: 48,
|
|
295
|
-
template: (item) => `<div
|
|
238
|
+
template: (item) => `<div>#${item.id}: ${item.name}</div>`,
|
|
296
239
|
},
|
|
297
|
-
|
|
298
|
-
|
|
240
|
+
})
|
|
241
|
+
.use(withScale()) // Auto-activates when height > 16.7M pixels
|
|
242
|
+
.use(withScrollbar({ autoHide: true }))
|
|
243
|
+
.build();
|
|
299
244
|
```
|
|
300
245
|
|
|
301
|
-
|
|
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.
|
|
249
|
+
|
|
250
|
+
### Async Loading with Pagination
|
|
302
251
|
|
|
303
252
|
```typescript
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
253
|
+
import { vlist, withAsync } from 'vlist';
|
|
254
|
+
|
|
255
|
+
const list = vlist({
|
|
256
|
+
container: '#list',
|
|
307
257
|
item: {
|
|
308
|
-
height:
|
|
309
|
-
template: (
|
|
258
|
+
height: 64,
|
|
259
|
+
template: (item) => {
|
|
260
|
+
if (!item) return `<div class="loading">Loading...</div>`;
|
|
261
|
+
return `<div>${item.name}</div>`;
|
|
262
|
+
},
|
|
310
263
|
},
|
|
311
|
-
|
|
312
|
-
|
|
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();
|
|
282
|
+
```
|
|
313
283
|
|
|
314
|
-
|
|
284
|
+
**Bundle:** 13.5 KB gzipped
|
|
315
285
|
|
|
316
|
-
|
|
317
|
-
btnNext.addEventListener('click', () => {
|
|
318
|
-
current++;
|
|
319
|
-
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
320
|
-
});
|
|
286
|
+
The async plugin shows placeholders for unloaded items and fetches data as you scroll. Velocity-aware loading cancels requests when scrolling fast.
|
|
321
287
|
|
|
322
|
-
|
|
323
|
-
current--;
|
|
324
|
-
wizard.scrollToIndex(current, { align: 'start', behavior: 'smooth' });
|
|
325
|
-
});
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
### Reverse Mode (Chat UI)
|
|
288
|
+
### Page-Level Scrolling
|
|
329
289
|
|
|
330
290
|
```typescript
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
291
|
+
import { vlist, withPage } from 'vlist';
|
|
292
|
+
|
|
293
|
+
const list = vlist({
|
|
294
|
+
container: '#list',
|
|
295
|
+
items: articles,
|
|
334
296
|
item: {
|
|
335
|
-
height:
|
|
336
|
-
template: (
|
|
337
|
-
<div class="bubble bubble--${msg.sender}">
|
|
338
|
-
<span class="sender">${msg.sender}</span>
|
|
339
|
-
<p>${msg.text}</p>
|
|
340
|
-
</div>
|
|
341
|
-
`,
|
|
297
|
+
height: 200,
|
|
298
|
+
template: (article) => `<article>...</article>`,
|
|
342
299
|
},
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// New message arrives — auto-scrolls to bottom if user was at bottom
|
|
347
|
-
chat.appendItems([newMessage]);
|
|
348
|
-
|
|
349
|
-
// Load older messages — scroll position preserved (no jump)
|
|
350
|
-
chat.prependItems(olderMessages);
|
|
300
|
+
})
|
|
301
|
+
.use(withPage()) // Uses document scroll instead of container
|
|
302
|
+
.build();
|
|
351
303
|
```
|
|
352
304
|
|
|
353
|
-
|
|
305
|
+
**Bundle:** 8.6 KB gzipped
|
|
354
306
|
|
|
355
|
-
|
|
307
|
+
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
308
|
+
|
|
309
|
+
### Horizontal Carousel
|
|
356
310
|
|
|
357
311
|
```typescript
|
|
358
|
-
|
|
359
|
-
|
|
312
|
+
import { vlist } from 'vlist';
|
|
313
|
+
|
|
314
|
+
const carousel = vlist({
|
|
315
|
+
container: '#carousel',
|
|
316
|
+
direction: 'horizontal',
|
|
317
|
+
items: cards,
|
|
360
318
|
item: {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
<span>${item.name}</span>
|
|
365
|
-
${selected ? '✓' : ''}
|
|
366
|
-
</div>
|
|
367
|
-
`,
|
|
319
|
+
width: 300, // Required for horizontal
|
|
320
|
+
height: 400, // Optional (can use CSS)
|
|
321
|
+
template: (card) => `<div class="card">...</div>`,
|
|
368
322
|
},
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
mode: 'multiple', // 'none' | 'single' | 'multiple'
|
|
372
|
-
initial: [1, 2], // Initially selected IDs
|
|
323
|
+
scroll: {
|
|
324
|
+
wrap: true, // Circular scrolling
|
|
373
325
|
},
|
|
374
|
-
});
|
|
326
|
+
}).build();
|
|
327
|
+
```
|
|
375
328
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
329
|
+
**Bundle:** 8.6 KB gzipped
|
|
330
|
+
|
|
331
|
+
### Selection & Navigation
|
|
380
332
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
list
|
|
333
|
+
```typescript
|
|
334
|
+
import { vlist, withSelection } from 'vlist';
|
|
335
|
+
|
|
336
|
+
const list = vlist({
|
|
337
|
+
container: '#list',
|
|
338
|
+
items: users,
|
|
339
|
+
item: {
|
|
340
|
+
height: 48,
|
|
341
|
+
template: (user, index, { selected }) => {
|
|
342
|
+
const cls = selected ? 'item--selected' : '';
|
|
343
|
+
return `<div class="${cls}">${user.name}</div>`;
|
|
344
|
+
},
|
|
345
|
+
},
|
|
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]
|
|
385
358
|
list.clearSelection();
|
|
386
359
|
```
|
|
387
360
|
|
|
388
|
-
|
|
361
|
+
**Bundle:** 10.0 KB gzipped
|
|
362
|
+
|
|
363
|
+
Supports `mode: 'single'` or `'multiple'` with keyboard navigation (Arrow keys, Home, End, Space, Enter).
|
|
364
|
+
|
|
365
|
+
### Variable Heights (Chat Messages)
|
|
389
366
|
|
|
390
367
|
```typescript
|
|
391
|
-
|
|
392
|
-
|
|
368
|
+
import { vlist } from 'vlist';
|
|
369
|
+
|
|
370
|
+
const list = vlist({
|
|
371
|
+
container: '#messages',
|
|
372
|
+
items: messages,
|
|
393
373
|
item: {
|
|
394
|
-
height:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
adapter: {
|
|
398
|
-
read: async ({ offset, limit }) => {
|
|
399
|
-
const response = await fetch(
|
|
400
|
-
`/api/items?offset=${offset}&limit=${limit}`
|
|
401
|
-
);
|
|
402
|
-
const data = await response.json();
|
|
403
|
-
|
|
404
|
-
return {
|
|
405
|
-
items: data.items,
|
|
406
|
-
total: data.total,
|
|
407
|
-
hasMore: data.hasMore,
|
|
408
|
-
};
|
|
374
|
+
height: (index) => {
|
|
375
|
+
// Heights computed from actual DOM measurements
|
|
376
|
+
return messages[index].measuredHeight || 60;
|
|
409
377
|
},
|
|
378
|
+
template: (msg) => `
|
|
379
|
+
<div class="message">
|
|
380
|
+
<div class="author">${msg.user}</div>
|
|
381
|
+
<div class="text">${msg.text}</div>
|
|
382
|
+
</div>
|
|
383
|
+
`,
|
|
410
384
|
},
|
|
411
|
-
});
|
|
385
|
+
}).build();
|
|
386
|
+
```
|
|
412
387
|
|
|
413
|
-
|
|
414
|
-
list.on('load:start', ({ offset, limit }) => {
|
|
415
|
-
console.log('Loading...', offset, limit);
|
|
416
|
-
});
|
|
388
|
+
**Bundle:** 10.9 KB gzipped
|
|
417
389
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
390
|
+
Variable heights use a prefix-sum array for O(1) offset lookups and O(log n) binary search.
|
|
391
|
+
|
|
392
|
+
## API Reference
|
|
393
|
+
|
|
394
|
+
### Core Methods
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
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
|
|
421
432
|
```
|
|
422
433
|
|
|
423
|
-
###
|
|
434
|
+
### Selection Methods (with `withSelection()`)
|
|
424
435
|
|
|
425
436
|
```typescript
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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
|
|
445
|
+
```
|
|
435
446
|
|
|
436
|
-
|
|
437
|
-
const snapshot = list.getScrollSnapshot();
|
|
438
|
-
// { index: 523, offsetInItem: 12, selectedIds: [3, 7, 42] }
|
|
439
|
-
sessionStorage.setItem('list-scroll', JSON.stringify(snapshot));
|
|
447
|
+
### Grid Methods (with `withGrid()`)
|
|
440
448
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
list.restoreScroll(saved);
|
|
444
|
-
// Scroll position AND selection are perfectly restored
|
|
449
|
+
```typescript
|
|
450
|
+
list.updateGrid(config: { columns?: number, gap?: number }): void
|
|
445
451
|
```
|
|
446
452
|
|
|
447
|
-
###
|
|
453
|
+
### Events
|
|
448
454
|
|
|
449
|
-
|
|
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 }) => { })
|
|
465
|
+
```
|
|
450
466
|
|
|
451
|
-
|
|
467
|
+
## Configuration
|
|
452
468
|
|
|
453
|
-
|
|
454
|
-
import { useVList, useVListEvent } from 'vlist/react';
|
|
469
|
+
### Base Configuration
|
|
455
470
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
471
|
+
```typescript
|
|
472
|
+
interface VListConfig<T> {
|
|
473
|
+
// Required
|
|
474
|
+
container: HTMLElement | string;
|
|
475
|
+
item: {
|
|
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
|
+
};
|
|
465
480
|
|
|
466
|
-
// Optional
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
470
488
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
);
|
|
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
|
+
};
|
|
478
495
|
}
|
|
479
496
|
```
|
|
480
497
|
|
|
481
|
-
`
|
|
482
|
-
- `containerRef` — attach to your container `<div>`
|
|
483
|
-
- `instanceRef` — ref to the `VList` instance (populated after mount)
|
|
484
|
-
- `getInstance()` — stable helper to access the instance
|
|
485
|
-
|
|
486
|
-
Items auto-sync when `config.items` changes by reference.
|
|
498
|
+
### Plugin: `withGrid(config)`
|
|
487
499
|
|
|
488
|
-
|
|
500
|
+
2D grid layout with virtualized rows.
|
|
489
501
|
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
import { useVList, useVListEvent } from 'vlist/vue';
|
|
497
|
-
import { ref } from 'vue';
|
|
502
|
+
```typescript
|
|
503
|
+
interface GridConfig {
|
|
504
|
+
columns: number; // Number of columns (required)
|
|
505
|
+
gap?: number; // Gap between items in pixels (default: 0)
|
|
506
|
+
}
|
|
507
|
+
```
|
|
498
508
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
509
|
+
**Example:**
|
|
510
|
+
```typescript
|
|
511
|
+
.use(withGrid({ columns: 4, gap: 16 }))
|
|
512
|
+
```
|
|
503
513
|
|
|
504
|
-
|
|
505
|
-
item: {
|
|
506
|
-
height: 48,
|
|
507
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
508
|
-
},
|
|
509
|
-
items: users.value,
|
|
510
|
-
});
|
|
514
|
+
### Plugin: `withSections(config)`
|
|
511
515
|
|
|
512
|
-
|
|
513
|
-
useVListEvent(instance, 'selection:change', ({ selected }) => {
|
|
514
|
-
console.log('Selected:', selected);
|
|
515
|
-
});
|
|
516
|
+
Grouped lists with sticky or inline headers.
|
|
516
517
|
|
|
517
|
-
|
|
518
|
-
|
|
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)
|
|
519
524
|
}
|
|
520
|
-
</script>
|
|
521
525
|
```
|
|
522
526
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
+
```
|
|
526
536
|
|
|
527
|
-
|
|
528
|
-
<script>
|
|
529
|
-
import { vlist, onVListEvent } from 'vlist/svelte';
|
|
537
|
+
**Important:** Items must be pre-sorted by group!
|
|
530
538
|
|
|
531
|
-
|
|
532
|
-
let unsubs = [];
|
|
539
|
+
### Plugin: `withSelection(config)`
|
|
533
540
|
|
|
534
|
-
|
|
535
|
-
config: {
|
|
536
|
-
item: {
|
|
537
|
-
height: 48,
|
|
538
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
539
|
-
},
|
|
540
|
-
items: users,
|
|
541
|
-
selection: { mode: 'single' },
|
|
542
|
-
},
|
|
543
|
-
onInstance: (inst) => {
|
|
544
|
-
instance = inst;
|
|
545
|
-
unsubs.push(
|
|
546
|
-
onVListEvent(inst, 'selection:change', ({ selected }) => {
|
|
547
|
-
console.log('Selected:', selected);
|
|
548
|
-
})
|
|
549
|
-
);
|
|
550
|
-
},
|
|
551
|
-
};
|
|
541
|
+
Single or multiple item selection with keyboard navigation.
|
|
552
542
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
543
|
+
```typescript
|
|
544
|
+
interface SelectionConfig {
|
|
545
|
+
mode: 'single' | 'multiple';
|
|
546
|
+
initial?: Array<string | number>; // Pre-selected item IDs
|
|
547
|
+
}
|
|
548
|
+
```
|
|
556
549
|
|
|
557
|
-
|
|
558
|
-
|
|
550
|
+
**Example:**
|
|
551
|
+
```typescript
|
|
552
|
+
.use(withSelection({
|
|
553
|
+
mode: 'multiple',
|
|
554
|
+
initial: [1, 5, 10],
|
|
555
|
+
}))
|
|
559
556
|
```
|
|
560
557
|
|
|
561
|
-
|
|
558
|
+
### Plugin: `withAsync(config)`
|
|
562
559
|
|
|
563
|
-
|
|
560
|
+
Asynchronous data loading with lazy loading and placeholders.
|
|
564
561
|
|
|
565
562
|
```typescript
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
};
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
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();
|
|
583
584
|
},
|
|
584
585
|
},
|
|
585
|
-
|
|
586
|
-
|
|
586
|
+
loading: {
|
|
587
|
+
cancelThreshold: 15, // Skip loads when scrolling > 15px/ms
|
|
588
|
+
},
|
|
589
|
+
}))
|
|
587
590
|
```
|
|
588
591
|
|
|
589
|
-
|
|
592
|
+
### Plugin: `withScale()`
|
|
590
593
|
|
|
591
|
-
|
|
594
|
+
Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
|
|
592
595
|
|
|
593
|
-
|
|
596
|
+
```typescript
|
|
597
|
+
.use(withScale()) // No config needed - auto-activates
|
|
598
|
+
```
|
|
594
599
|
|
|
600
|
+
**Example:**
|
|
595
601
|
```typescript
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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();
|
|
602
609
|
```
|
|
603
610
|
|
|
604
|
-
|
|
611
|
+
### Plugin: `withScrollbar(config)`
|
|
612
|
+
|
|
613
|
+
Custom scrollbar with auto-hide and smooth dragging.
|
|
605
614
|
|
|
606
615
|
```typescript
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
list.getScrollSnapshot() // Get snapshot for save/restore
|
|
614
|
-
list.restoreScroll(snapshot) // Restore position (and selection) from snapshot
|
|
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
|
+
```
|
|
615
622
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
623
|
+
**Example:**
|
|
624
|
+
```typescript
|
|
625
|
+
.use(withScrollbar({
|
|
626
|
+
autoHide: true,
|
|
627
|
+
autoHideDelay: 1500,
|
|
628
|
+
}))
|
|
619
629
|
```
|
|
620
630
|
|
|
621
|
-
|
|
631
|
+
### Plugin: `withPage()`
|
|
632
|
+
|
|
633
|
+
Use document-level scrolling instead of container scrolling.
|
|
622
634
|
|
|
623
635
|
```typescript
|
|
624
|
-
|
|
625
|
-
list.deselect(...ids) // Deselect items
|
|
626
|
-
list.toggleSelect(id) // Toggle selection
|
|
627
|
-
list.selectAll() // Select all
|
|
628
|
-
list.clearSelection() // Clear selection
|
|
629
|
-
list.getSelected() // Get selected IDs
|
|
630
|
-
list.getSelectedItems() // Get selected items
|
|
636
|
+
.use(withPage()) // No config needed
|
|
631
637
|
```
|
|
632
638
|
|
|
633
|
-
|
|
634
|
-
|
|
639
|
+
**Example:**
|
|
635
640
|
```typescript
|
|
636
|
-
list
|
|
637
|
-
list
|
|
641
|
+
const list = vlist({
|
|
642
|
+
container: '#list',
|
|
643
|
+
items: articles,
|
|
644
|
+
item: { height: 300, template: renderArticle },
|
|
645
|
+
})
|
|
646
|
+
.use(withPage())
|
|
647
|
+
.build();
|
|
638
648
|
```
|
|
639
649
|
|
|
640
|
-
|
|
650
|
+
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
651
|
+
|
|
652
|
+
### Plugin: `withSnapshots()`
|
|
653
|
+
|
|
654
|
+
Scroll position save/restore for SPA navigation.
|
|
641
655
|
|
|
642
656
|
```typescript
|
|
643
|
-
|
|
657
|
+
// Included in base - no need to import
|
|
658
|
+
const snapshot = list.getScrollSnapshot();
|
|
659
|
+
list.restoreScroll(snapshot);
|
|
644
660
|
```
|
|
645
661
|
|
|
646
|
-
|
|
647
|
-
|
|
662
|
+
**Example:**
|
|
648
663
|
```typescript
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
664
|
+
// Save on navigation away
|
|
665
|
+
const snapshot = list.getScrollSnapshot();
|
|
666
|
+
sessionStorage.setItem('scroll', JSON.stringify(snapshot));
|
|
667
|
+
|
|
668
|
+
// Restore on return
|
|
669
|
+
const snapshot = JSON.parse(sessionStorage.getItem('scroll'));
|
|
670
|
+
list.restoreScroll(snapshot);
|
|
652
671
|
```
|
|
653
672
|
|
|
654
|
-
|
|
673
|
+
## Advanced Usage
|
|
674
|
+
|
|
675
|
+
### Variable Heights with DOM Measurement
|
|
655
676
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
| `item:click` | `{ item, index, event }` | Item was clicked |
|
|
659
|
-
| `selection:change` | `{ selected, items }` | Selection changed |
|
|
660
|
-
| `scroll` | `{ scrollTop, direction }` | Scroll position changed |
|
|
661
|
-
| `range:change` | `{ range }` | Visible range changed |
|
|
662
|
-
| `resize` | `{ height, width }` | Container was resized |
|
|
663
|
-
| `load:start` | `{ offset, limit }` | Data loading started |
|
|
664
|
-
| `load:end` | `{ items, total }` | Data loading completed |
|
|
665
|
-
| `error` | `{ error, context }` | Error occurred |
|
|
677
|
+
```typescript
|
|
678
|
+
import { vlist } from 'vlist';
|
|
666
679
|
|
|
667
|
-
|
|
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);
|
|
668
684
|
|
|
669
|
-
|
|
685
|
+
items.forEach(item => {
|
|
686
|
+
measuringDiv.innerHTML = renderItem(item);
|
|
687
|
+
item.measuredHeight = measuringDiv.offsetHeight;
|
|
688
|
+
});
|
|
670
689
|
|
|
671
|
-
|
|
690
|
+
document.body.removeChild(measuringDiv);
|
|
672
691
|
|
|
673
|
-
|
|
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();
|
|
701
|
+
```
|
|
674
702
|
|
|
675
|
-
|
|
676
|
-
|-----|--------|
|
|
677
|
-
| `↑` / `↓` | Move focus up/down |
|
|
678
|
-
| `Home` | Move focus to first item |
|
|
679
|
-
| `End` | Move focus to last item |
|
|
680
|
-
| `Space` / `Enter` | Toggle selection on focused item |
|
|
681
|
-
| `Tab` | Move focus into / out of the list |
|
|
703
|
+
### Dynamic Grid Columns
|
|
682
704
|
|
|
683
|
-
|
|
705
|
+
```typescript
|
|
706
|
+
import { vlist, withGrid } from 'vlist';
|
|
684
707
|
|
|
685
|
-
|
|
686
|
-
|-----------|---------|---------|
|
|
687
|
-
| `role="listbox"` | Root | Identifies the widget as a list of selectable items |
|
|
688
|
-
| `role="option"` | Each item | Identifies each item as a selectable option |
|
|
689
|
-
| `aria-setsize` | Each item | Total item count — screen readers announce "item 5 of 10,000" |
|
|
690
|
-
| `aria-posinset` | Each item | 1-based position within the list |
|
|
691
|
-
| `aria-activedescendant` | Root | Points to the focused item's ID for screen reader tracking |
|
|
692
|
-
| `aria-selected` | Each item | Reflects selection state |
|
|
693
|
-
| `aria-busy` | Root | Present during async data loading |
|
|
694
|
-
| `aria-label` | Root | Set via `ariaLabel` config option |
|
|
708
|
+
let currentColumns = 4;
|
|
695
709
|
|
|
696
|
-
|
|
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
|
+
});
|
|
730
|
+
```
|
|
697
731
|
|
|
698
|
-
|
|
732
|
+
### Combining Multiple Plugins
|
|
699
733
|
|
|
700
|
-
|
|
734
|
+
```typescript
|
|
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();
|
|
761
|
+
```
|
|
701
762
|
|
|
702
|
-
|
|
763
|
+
**Bundle:** ~15 KB gzipped (includes only used plugins)
|
|
703
764
|
|
|
704
|
-
|
|
765
|
+
## Framework Adapters
|
|
705
766
|
|
|
706
|
-
|
|
767
|
+
### React
|
|
707
768
|
|
|
708
769
|
```typescript
|
|
709
|
-
import 'vlist
|
|
770
|
+
import { vlist, withSelection } from 'vlist';
|
|
771
|
+
import { useEffect, useRef } from 'react';
|
|
772
|
+
|
|
773
|
+
function MyList({ items }) {
|
|
774
|
+
const containerRef = useRef(null);
|
|
775
|
+
const listRef = useRef(null);
|
|
776
|
+
|
|
777
|
+
useEffect(() => {
|
|
778
|
+
if (!containerRef.current) return;
|
|
779
|
+
|
|
780
|
+
listRef.current = vlist({
|
|
781
|
+
container: containerRef.current,
|
|
782
|
+
items,
|
|
783
|
+
item: { height: 48, template: renderItem },
|
|
784
|
+
})
|
|
785
|
+
.use(withSelection({ mode: 'single' }))
|
|
786
|
+
.build();
|
|
787
|
+
|
|
788
|
+
return () => listRef.current?.destroy();
|
|
789
|
+
}, []);
|
|
790
|
+
|
|
791
|
+
useEffect(() => {
|
|
792
|
+
listRef.current?.setItems(items);
|
|
793
|
+
}, [items]);
|
|
794
|
+
|
|
795
|
+
return <div ref={containerRef} />;
|
|
796
|
+
}
|
|
710
797
|
```
|
|
711
798
|
|
|
712
|
-
|
|
799
|
+
### Vue 3
|
|
713
800
|
|
|
714
801
|
```typescript
|
|
715
|
-
import 'vlist
|
|
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
|
+
};
|
|
716
831
|
```
|
|
717
832
|
|
|
718
|
-
###
|
|
833
|
+
### Svelte
|
|
719
834
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
+
});
|
|
735
854
|
|
|
736
|
-
|
|
855
|
+
$: list?.setItems(items);
|
|
737
856
|
|
|
738
|
-
|
|
857
|
+
onDestroy(() => {
|
|
858
|
+
list?.destroy();
|
|
859
|
+
});
|
|
860
|
+
</script>
|
|
739
861
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
<div class="vlist vlist--compact">...</div>
|
|
862
|
+
<div bind:this={container}></div>
|
|
863
|
+
```
|
|
743
864
|
|
|
744
|
-
|
|
745
|
-
<div class="vlist vlist--comfortable">...</div>
|
|
865
|
+
## Styling
|
|
746
866
|
|
|
747
|
-
|
|
748
|
-
<div class="vlist vlist--borderless">...</div>
|
|
867
|
+
Import the base styles:
|
|
749
868
|
|
|
750
|
-
|
|
751
|
-
|
|
869
|
+
```typescript
|
|
870
|
+
import 'vlist/styles';
|
|
752
871
|
```
|
|
753
872
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
All visual aspects can be customized via CSS custom properties:
|
|
873
|
+
Or customize with your own CSS:
|
|
757
874
|
|
|
758
875
|
```css
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
876
|
+
.vlist {
|
|
877
|
+
/* Container styles */
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
.vlist-viewport {
|
|
881
|
+
/* Scroll viewport */
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.vlist-item {
|
|
885
|
+
/* Item wrapper - positioned absolutely */
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.vlist-item--selected {
|
|
889
|
+
/* Selected state */
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.vlist-item--focused {
|
|
893
|
+
/* Focused state (keyboard navigation) */
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.vlist-scrollbar {
|
|
897
|
+
/* Custom scrollbar track */
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.vlist-scrollbar__thumb {
|
|
901
|
+
/* Scrollbar thumb/handle */
|
|
770
902
|
}
|
|
771
903
|
```
|
|
772
904
|
|
|
773
|
-
|
|
905
|
+
## Performance
|
|
906
|
+
|
|
907
|
+
### Bundle Sizes (Gzipped)
|
|
908
|
+
|
|
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 |
|
|
918
|
+
|
|
919
|
+
**Traditional virtual lists:** 20-23 KB minimum (all features bundled regardless of usage)
|
|
920
|
+
|
|
921
|
+
### Memory Efficiency
|
|
922
|
+
|
|
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
|
|
940
|
+
|
|
941
|
+
Relies on:
|
|
942
|
+
- `IntersectionObserver` (widely supported)
|
|
943
|
+
- `ResizeObserver` (polyfill available if needed)
|
|
944
|
+
- CSS `transform` (universal support)
|
|
774
945
|
|
|
775
946
|
## TypeScript
|
|
776
947
|
|
|
777
|
-
|
|
948
|
+
Fully typed with comprehensive TypeScript definitions:
|
|
778
949
|
|
|
779
950
|
```typescript
|
|
780
|
-
|
|
951
|
+
import { vlist, withGrid, type VList, type VListConfig } from 'vlist';
|
|
952
|
+
|
|
953
|
+
interface Photo {
|
|
781
954
|
id: number;
|
|
782
|
-
|
|
783
|
-
|
|
955
|
+
url: string;
|
|
956
|
+
title: string;
|
|
784
957
|
}
|
|
785
958
|
|
|
786
|
-
const
|
|
787
|
-
container: '#
|
|
959
|
+
const config: VListConfig<Photo> = {
|
|
960
|
+
container: '#gallery',
|
|
961
|
+
items: photos,
|
|
788
962
|
item: {
|
|
789
|
-
height:
|
|
790
|
-
template: (
|
|
963
|
+
height: 200,
|
|
964
|
+
template: (photo: Photo) => `<img src="${photo.url}" />`,
|
|
791
965
|
},
|
|
792
|
-
|
|
793
|
-
});
|
|
966
|
+
};
|
|
794
967
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
});
|
|
968
|
+
const list: VList<Photo> = vlist(config)
|
|
969
|
+
.use(withGrid({ columns: 4 }))
|
|
970
|
+
.build();
|
|
799
971
|
```
|
|
800
972
|
|
|
801
|
-
##
|
|
802
|
-
|
|
803
|
-
vlist is designed for maximum performance with extensive built-in optimizations:
|
|
973
|
+
## Migration from Monolithic API
|
|
804
974
|
|
|
805
|
-
|
|
806
|
-
- **Element pooling** - DOM elements are recycled via `createElementPool()`, reducing GC pressure
|
|
807
|
-
- **Zero-allocation scroll hot path** - No object allocations per scroll frame; in-place range mutation
|
|
808
|
-
- **RAF-throttled native scroll** - At most one scroll processing per animation frame
|
|
809
|
-
- **CSS containment** - `contain: layout style` on items container, `contain: content` + `will-change: transform` on items
|
|
810
|
-
- **Scroll transition suppression** - `.vlist--scrolling` class disables CSS transitions during active scroll
|
|
811
|
-
- **Circular buffer velocity tracker** - Pre-allocated buffer, zero allocations during scroll
|
|
812
|
-
- **Targeted keyboard focus render** - Arrow keys update only 2 affected items instead of all visible items
|
|
813
|
-
- **Batched LRU timestamps** - Single `Date.now()` per render cycle instead of per-item
|
|
814
|
-
- **DocumentFragment batching** - New elements appended in a single DOM operation
|
|
815
|
-
- **Split CSS** - Core styles (5.6 KB) separated from optional extras (1.8 KB)
|
|
816
|
-
- **Configurable velocity-based loading** - Skip, preload, or defer loading based on scroll speed
|
|
817
|
-
- **Compression for 1M+ items** - Automatic scroll space compression when content exceeds browser height limits
|
|
975
|
+
If you're using the old monolithic API:
|
|
818
976
|
|
|
819
|
-
###
|
|
977
|
+
### Before (Monolithic - Deprecated)
|
|
820
978
|
|
|
821
|
-
|
|
979
|
+
```typescript
|
|
980
|
+
import { createVList } from 'vlist';
|
|
822
981
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
| 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
|
+
```
|
|
832
990
|
|
|
833
|
-
|
|
991
|
+
### After (Builder - Recommended)
|
|
834
992
|
|
|
835
|
-
|
|
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
|
+
```
|
|
836
1005
|
|
|
837
|
-
|
|
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
|
|
838
1011
|
|
|
839
|
-
|
|
840
|
-
- Firefox 55+
|
|
841
|
-
- Safari 12+
|
|
842
|
-
- Edge 79+
|
|
1012
|
+
## Plugin Naming Changes
|
|
843
1013
|
|
|
844
|
-
|
|
1014
|
+
If you're upgrading from an earlier version:
|
|
845
1015
|
|
|
846
|
-
|
|
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'` |
|
|
847
1023
|
|
|
848
|
-
|
|
849
|
-
# Install dependencies
|
|
850
|
-
bun install
|
|
1024
|
+
## Contributing
|
|
851
1025
|
|
|
852
|
-
|
|
853
|
-
bun run dev
|
|
1026
|
+
Contributions are welcome! Please:
|
|
854
1027
|
|
|
855
|
-
|
|
856
|
-
|
|
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
|
|
857
1033
|
|
|
858
|
-
|
|
859
|
-
bun run typecheck
|
|
1034
|
+
## License
|
|
860
1035
|
|
|
861
|
-
|
|
862
|
-
bun run build
|
|
863
|
-
```
|
|
1036
|
+
MIT License - see [LICENSE](LICENSE) for details
|
|
864
1037
|
|
|
865
|
-
##
|
|
1038
|
+
## Links
|
|
866
1039
|
|
|
867
|
-
|
|
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)
|
|
868
1044
|
|
|
869
|
-
|
|
1045
|
+
---
|
|
870
1046
|
|
|
871
|
-
|
|
1047
|
+
**Built by [Floor IO](https://floor.io)** with ❤️ for the web community
|