@floor/vlist 0.7.6 â 0.8.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 +166 -839
- package/dist/builder/core.d.ts +4 -4
- package/dist/builder/data.d.ts +3 -5
- package/dist/builder/dom.d.ts +13 -0
- package/dist/builder/materializectx.d.ts +146 -0
- package/dist/builder/pool.d.ts +10 -0
- package/dist/builder/range.d.ts +10 -0
- package/dist/builder/scroll.d.ts +13 -0
- package/dist/builder/types.d.ts +0 -1
- package/dist/builder/velocity.d.ts +22 -0
- package/dist/index.js +1 -1
- package/package.json +5 -20
- package/dist/async/index.js +0 -1
- package/dist/builder/index.js +0 -1
- package/dist/core/full.d.ts +0 -24
- package/dist/grid/index.js +0 -1
- package/dist/page/index.js +0 -1
- package/dist/scale/index.js +0 -1
- package/dist/scrollbar/index.js +0 -1
- package/dist/sections/index.js +0 -1
- package/dist/selection/index.js +0 -1
- package/dist/snapshots/index.js +0 -1
package/README.md
CHANGED
|
@@ -4,35 +4,19 @@ Lightweight, high-performance virtual list with zero dependencies and optimal tr
|
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- â
**Selection** - Single and multiple selection modes
|
|
21
|
-
- ð **Sticky headers** - Grouped lists with sticky section headers
|
|
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
|
|
26
|
-
- ðĻ **Customizable** - Beautiful, customizable styles
|
|
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
|
-
|
|
31
|
-
## Sandbox & Documentation
|
|
32
|
-
|
|
33
|
-
Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
|
|
34
|
-
|
|
35
|
-
**30+ examples** with multi-framework implementations (JavaScript, React, Vue, Svelte)
|
|
10
|
+
- **Zero dependencies** â no external libraries
|
|
11
|
+
- **Ultra memory efficient** â ~0.1-0.2 MB constant overhead regardless of dataset size
|
|
12
|
+
- **8â12 KB gzipped** â pay only for features you use (vs 20 KB+ monolithic alternatives)
|
|
13
|
+
- **Builder API** â composable plugins with perfect tree-shaking
|
|
14
|
+
- **Grid, sections, async, selection, scale** â all opt-in
|
|
15
|
+
- **Horizontal, reverse, page-scroll, wrap** â every layout mode
|
|
16
|
+
- **Accessible** â WAI-ARIA, keyboard navigation, screen-reader friendly
|
|
17
|
+
- **React, Vue, Svelte** â framework adapters available
|
|
18
|
+
|
|
19
|
+
**30+ interactive examples â [vlist.dev](https://vlist.dev)**
|
|
36
20
|
|
|
37
21
|
## Installation
|
|
38
22
|
|
|
@@ -40,13 +24,11 @@ Interactive examples and documentation at **[vlist.dev](https://vlist.dev)**
|
|
|
40
24
|
npm install @floor/vlist
|
|
41
25
|
```
|
|
42
26
|
|
|
43
|
-
> **Note:** Currently published as `@floor/vlist` (scoped package). When the npm dispute for `vlist` is resolved, the package will migrate to `vlist`.
|
|
44
|
-
|
|
45
27
|
## Quick Start
|
|
46
28
|
|
|
47
29
|
```typescript
|
|
48
|
-
import { vlist } from 'vlist'
|
|
49
|
-
import 'vlist/styles'
|
|
30
|
+
import { vlist } from '@floor/vlist'
|
|
31
|
+
import '@floor/vlist/styles'
|
|
50
32
|
|
|
51
33
|
const list = vlist({
|
|
52
34
|
container: '#my-list',
|
|
@@ -59,87 +41,57 @@ const list = vlist({
|
|
|
59
41
|
height: 48,
|
|
60
42
|
template: (item) => `<div>${item.name}</div>`,
|
|
61
43
|
},
|
|
62
|
-
}).build()
|
|
44
|
+
}).build()
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
list.
|
|
66
|
-
list.
|
|
67
|
-
list.on('item:click', ({ item }) => console.log(item));
|
|
46
|
+
list.scrollToIndex(10)
|
|
47
|
+
list.setItems(newItems)
|
|
48
|
+
list.on('item:click', ({ item }) => console.log(item))
|
|
68
49
|
```
|
|
69
50
|
|
|
70
|
-
**Bundle:** ~8 KB gzipped
|
|
71
|
-
|
|
72
51
|
## Builder Pattern
|
|
73
52
|
|
|
74
|
-
|
|
53
|
+
Start with the base, add only what you need:
|
|
75
54
|
|
|
76
55
|
```typescript
|
|
77
|
-
import { vlist, withGrid, withSections, withSelection } from 'vlist'
|
|
56
|
+
import { vlist, withGrid, withSections, withSelection } from '@floor/vlist'
|
|
78
57
|
|
|
79
58
|
const list = vlist({
|
|
80
59
|
container: '#app',
|
|
81
60
|
items: photos,
|
|
82
|
-
item: {
|
|
83
|
-
height: 200,
|
|
84
|
-
template: renderPhoto,
|
|
85
|
-
},
|
|
61
|
+
item: { height: 200, template: renderPhoto },
|
|
86
62
|
})
|
|
87
63
|
.use(withGrid({ columns: 4, gap: 16 }))
|
|
88
|
-
.use(withSections({
|
|
64
|
+
.use(withSections({
|
|
89
65
|
getGroupForIndex: (i) => photos[i].category,
|
|
90
66
|
headerHeight: 40,
|
|
91
67
|
headerTemplate: (cat) => `<h2>${cat}</h2>`,
|
|
92
68
|
}))
|
|
93
69
|
.use(withSelection({ mode: 'multiple' }))
|
|
94
|
-
.build()
|
|
70
|
+
.build()
|
|
95
71
|
```
|
|
96
72
|
|
|
97
|
-
|
|
73
|
+
### Plugins
|
|
98
74
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Plugin | Cost | Description |
|
|
75
|
+
| Plugin | Size | Description |
|
|
102
76
|
|--------|------|-------------|
|
|
103
|
-
| **Base** | 7.7 KB
|
|
77
|
+
| **Base** | 7.7 KB | Core virtualization |
|
|
104
78
|
| `withGrid()` | +4.0 KB | 2D grid layout |
|
|
105
79
|
| `withSections()` | +4.6 KB | Grouped lists with sticky/inline headers |
|
|
106
|
-
| `withAsync()` | +5.3 KB |
|
|
107
|
-
| `withSelection()` | +2.3 KB | Single/multiple
|
|
108
|
-
| `withScale()` | +2.2 KB |
|
|
80
|
+
| `withAsync()` | +5.3 KB | Lazy loading with adapters |
|
|
81
|
+
| `withSelection()` | +2.3 KB | Single/multiple selection + keyboard nav |
|
|
82
|
+
| `withScale()` | +2.2 KB | 1M+ items via scroll compression |
|
|
109
83
|
| `withScrollbar()` | +1.0 KB | Custom scrollbar UI |
|
|
110
84
|
| `withPage()` | +0.9 KB | Document-level scrolling |
|
|
111
|
-
| `withSnapshots()` |
|
|
112
|
-
|
|
113
|
-
**Compare to monolithic:** Traditional virtual lists bundle everything = 20-23 KB gzipped minimum, regardless of usage.
|
|
85
|
+
| `withSnapshots()` | included | Scroll save/restore |
|
|
114
86
|
|
|
115
87
|
## Examples
|
|
116
88
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
```typescript
|
|
120
|
-
import { vlist } from 'vlist';
|
|
121
|
-
|
|
122
|
-
const list = vlist({
|
|
123
|
-
container: '#list',
|
|
124
|
-
items: users,
|
|
125
|
-
item: {
|
|
126
|
-
height: 64,
|
|
127
|
-
template: (user) => `
|
|
128
|
-
<div class="user">
|
|
129
|
-
<img src="${user.avatar}" />
|
|
130
|
-
<span>${user.name}</span>
|
|
131
|
-
</div>
|
|
132
|
-
`,
|
|
133
|
-
},
|
|
134
|
-
}).build();
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
**Bundle:** 8.2 KB gzipped
|
|
89
|
+
More examples at **[vlist.dev](https://vlist.dev)**.
|
|
138
90
|
|
|
139
91
|
### Grid Layout
|
|
140
92
|
|
|
141
93
|
```typescript
|
|
142
|
-
import { vlist, withGrid, withScrollbar } from 'vlist'
|
|
94
|
+
import { vlist, withGrid, withScrollbar } from '@floor/vlist'
|
|
143
95
|
|
|
144
96
|
const gallery = vlist({
|
|
145
97
|
container: '#gallery',
|
|
@@ -156,887 +108,262 @@ const gallery = vlist({
|
|
|
156
108
|
})
|
|
157
109
|
.use(withGrid({ columns: 4, gap: 16 }))
|
|
158
110
|
.use(withScrollbar({ autoHide: true }))
|
|
159
|
-
.build()
|
|
111
|
+
.build()
|
|
160
112
|
```
|
|
161
113
|
|
|
162
|
-
|
|
163
|
-
|
|
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)
|
|
114
|
+
### Sticky Headers
|
|
167
115
|
|
|
168
116
|
```typescript
|
|
169
|
-
import { vlist, withSections } from 'vlist'
|
|
117
|
+
import { vlist, withSections } from '@floor/vlist'
|
|
170
118
|
|
|
171
119
|
const contacts = vlist({
|
|
172
120
|
container: '#contacts',
|
|
173
|
-
items: sortedContacts,
|
|
121
|
+
items: sortedContacts,
|
|
174
122
|
item: {
|
|
175
123
|
height: 56,
|
|
176
124
|
template: (contact) => `<div>${contact.name}</div>`,
|
|
177
125
|
},
|
|
178
126
|
})
|
|
179
127
|
.use(withSections({
|
|
180
|
-
getGroupForIndex: (i) =>
|
|
128
|
+
getGroupForIndex: (i) => sortedContacts[i].lastName[0].toUpperCase(),
|
|
181
129
|
headerHeight: 36,
|
|
182
130
|
headerTemplate: (letter) => `<div class="header">${letter}</div>`,
|
|
183
|
-
sticky: true,
|
|
131
|
+
sticky: true,
|
|
184
132
|
}))
|
|
185
|
-
.build()
|
|
133
|
+
.build()
|
|
186
134
|
```
|
|
187
135
|
|
|
188
|
-
**Bundle:** 12.3 KB gzipped
|
|
189
|
-
|
|
190
136
|
Set `sticky: false` for inline headers (iMessage/WhatsApp style).
|
|
191
137
|
|
|
192
|
-
###
|
|
193
|
-
|
|
194
|
-
```typescript
|
|
195
|
-
import { vlist, withSections } from 'vlist';
|
|
196
|
-
|
|
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();
|
|
216
|
-
|
|
217
|
-
// New messages - auto-scrolls to bottom
|
|
218
|
-
chat.appendItems([newMessage]);
|
|
219
|
-
|
|
220
|
-
// Load history - preserves scroll position
|
|
221
|
-
chat.prependItems(olderMessages);
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
**Bundle:** 11.9 KB gzipped
|
|
225
|
-
|
|
226
|
-
Perfect for iMessage, WhatsApp, Telegram-style chat interfaces.
|
|
227
|
-
|
|
228
|
-
### Large Datasets (1M+ Items)
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
import { vlist, withScale, withScrollbar } from 'vlist';
|
|
232
|
-
|
|
233
|
-
const bigList = vlist({
|
|
234
|
-
container: '#big-list',
|
|
235
|
-
items: generateItems(5_000_000),
|
|
236
|
-
item: {
|
|
237
|
-
height: 48,
|
|
238
|
-
template: (item) => `<div>#${item.id}: ${item.name}</div>`,
|
|
239
|
-
},
|
|
240
|
-
})
|
|
241
|
-
.use(withScale()) // Auto-activates when height > 16.7M pixels
|
|
242
|
-
.use(withScrollbar({ autoHide: true }))
|
|
243
|
-
.build();
|
|
244
|
-
```
|
|
245
|
-
|
|
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
|
|
138
|
+
### Async Loading
|
|
251
139
|
|
|
252
140
|
```typescript
|
|
253
|
-
import { vlist, withAsync } from 'vlist'
|
|
141
|
+
import { vlist, withAsync } from '@floor/vlist'
|
|
254
142
|
|
|
255
143
|
const list = vlist({
|
|
256
144
|
container: '#list',
|
|
257
145
|
item: {
|
|
258
146
|
height: 64,
|
|
259
|
-
template: (item) =>
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
},
|
|
147
|
+
template: (item) => item
|
|
148
|
+
? `<div>${item.name}</div>`
|
|
149
|
+
: `<div class="placeholder">LoadingâĶ</div>`,
|
|
263
150
|
},
|
|
264
151
|
})
|
|
265
152
|
.use(withAsync({
|
|
266
153
|
adapter: {
|
|
267
154
|
read: async ({ offset, limit }) => {
|
|
268
|
-
const
|
|
269
|
-
const data = await
|
|
270
|
-
return {
|
|
271
|
-
items: data.items,
|
|
272
|
-
total: data.total,
|
|
273
|
-
hasMore: data.hasMore,
|
|
274
|
-
};
|
|
155
|
+
const res = await fetch(`/api/users?offset=${offset}&limit=${limit}`)
|
|
156
|
+
const data = await res.json()
|
|
157
|
+
return { items: data.items, total: data.total, hasMore: data.hasMore }
|
|
275
158
|
},
|
|
276
159
|
},
|
|
277
|
-
loading: {
|
|
278
|
-
cancelThreshold: 15, // Cancel loads when scrolling fast (pixels/ms)
|
|
279
|
-
},
|
|
280
160
|
}))
|
|
281
|
-
.build()
|
|
282
|
-
```
|
|
283
|
-
|
|
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.
|
|
287
|
-
|
|
288
|
-
### Page-Level Scrolling
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
import { vlist, withPage } from 'vlist';
|
|
292
|
-
|
|
293
|
-
const list = vlist({
|
|
294
|
-
container: '#list',
|
|
295
|
-
items: articles,
|
|
296
|
-
item: {
|
|
297
|
-
height: 200,
|
|
298
|
-
template: (article) => `<article>...</article>`,
|
|
299
|
-
},
|
|
300
|
-
})
|
|
301
|
-
.use(withPage()) // Uses document scroll instead of container
|
|
302
|
-
.build();
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
**Bundle:** 8.6 KB gzipped
|
|
306
|
-
|
|
307
|
-
Perfect for blog posts, infinite scroll feeds, and full-page lists.
|
|
308
|
-
|
|
309
|
-
### Horizontal Carousel
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { vlist } from 'vlist';
|
|
313
|
-
|
|
314
|
-
const carousel = vlist({
|
|
315
|
-
container: '#carousel',
|
|
316
|
-
direction: 'horizontal',
|
|
317
|
-
items: cards,
|
|
318
|
-
item: {
|
|
319
|
-
width: 300, // Required for horizontal
|
|
320
|
-
height: 400, // Optional (can use CSS)
|
|
321
|
-
template: (card) => `<div class="card">...</div>`,
|
|
322
|
-
},
|
|
323
|
-
scroll: {
|
|
324
|
-
wrap: true, // Circular scrolling
|
|
325
|
-
},
|
|
326
|
-
}).build();
|
|
161
|
+
.build()
|
|
327
162
|
```
|
|
328
163
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
### Selection & Navigation
|
|
332
|
-
|
|
333
|
-
```typescript
|
|
334
|
-
import { vlist, withSelection } from 'vlist';
|
|
164
|
+
### More Patterns
|
|
335
165
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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]
|
|
358
|
-
list.clearSelection();
|
|
359
|
-
```
|
|
166
|
+
| Pattern | Key options |
|
|
167
|
+
|---------|------------|
|
|
168
|
+
| **Chat UI** | `reverse: true` + `withSections({ sticky: false })` |
|
|
169
|
+
| **Horizontal carousel** | `direction: 'horizontal'`, `item.width` |
|
|
170
|
+
| **Page-level scroll** | `withPage()` |
|
|
171
|
+
| **1M+ items** | `withScale()` â auto-compresses scroll space |
|
|
172
|
+
| **Wrap navigation** | `scroll: { wrap: true }` |
|
|
173
|
+
| **Variable heights** | `item: { height: (index) => heights[index] }` |
|
|
360
174
|
|
|
361
|
-
**
|
|
175
|
+
See **[vlist.dev](https://vlist.dev)** for live demos of each.
|
|
362
176
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
### Variable Heights (Chat Messages)
|
|
177
|
+
## API
|
|
366
178
|
|
|
367
179
|
```typescript
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const list = vlist({
|
|
371
|
-
container: '#messages',
|
|
372
|
-
items: messages,
|
|
373
|
-
item: {
|
|
374
|
-
height: (index) => {
|
|
375
|
-
// Heights computed from actual DOM measurements
|
|
376
|
-
return messages[index].measuredHeight || 60;
|
|
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
|
-
`,
|
|
384
|
-
},
|
|
385
|
-
}).build();
|
|
180
|
+
const list = vlist(config).use(...plugins).build()
|
|
386
181
|
```
|
|
387
182
|
|
|
388
|
-
|
|
183
|
+
### Data
|
|
389
184
|
|
|
390
|
-
|
|
185
|
+
| Method | Description |
|
|
186
|
+
|--------|-------------|
|
|
187
|
+
| `list.setItems(items)` | Replace all items |
|
|
188
|
+
| `list.appendItems(items)` | Add to end (auto-scrolls in reverse mode) |
|
|
189
|
+
| `list.prependItems(items)` | Add to start (preserves scroll position) |
|
|
190
|
+
| `list.updateItem(index, partial)` | Update a single item by index |
|
|
191
|
+
| `list.removeItem(index)` | Remove by index |
|
|
192
|
+
| `list.reload()` | Re-fetch from adapter (async) |
|
|
391
193
|
|
|
392
|
-
|
|
194
|
+
### Navigation
|
|
393
195
|
|
|
394
|
-
|
|
196
|
+
| Method | Description |
|
|
197
|
+
|--------|-------------|
|
|
198
|
+
| `list.scrollToIndex(i, align?)` | Scroll to index (`'start'` \| `'center'` \| `'end'`) |
|
|
199
|
+
| `list.scrollToIndex(i, opts?)` | With `{ align, behavior: 'smooth', duration }` |
|
|
200
|
+
| `list.cancelScroll()` | Cancel smooth scroll animation |
|
|
201
|
+
| `list.getScrollPosition()` | Current scroll offset |
|
|
202
|
+
| `list.getVisibleRange()` | `{ start, end }` of visible indices |
|
|
203
|
+
| `list.getScrollSnapshot()` | Save scroll state (for SPA navigation) |
|
|
204
|
+
| `list.restoreScroll(snapshot)` | Restore saved scroll state |
|
|
395
205
|
|
|
396
|
-
|
|
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
|
-
```
|
|
433
|
-
|
|
434
|
-
### Selection Methods (with `withSelection()`)
|
|
206
|
+
### Selection (with `withSelection()`)
|
|
435
207
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
list.
|
|
439
|
-
list.
|
|
440
|
-
list.
|
|
441
|
-
list.clearSelection()
|
|
442
|
-
list.getSelectedIds()
|
|
443
|
-
list.getSelectedItems()
|
|
444
|
-
list.setSelectionMode(mode: 'none' | 'single' | 'multiple'): void
|
|
445
|
-
```
|
|
208
|
+
| Method | Description |
|
|
209
|
+
|--------|-------------|
|
|
210
|
+
| `list.selectItem(id)` | Select item |
|
|
211
|
+
| `list.deselectItem(id)` | Deselect item |
|
|
212
|
+
| `list.toggleSelection(id)` | Toggle |
|
|
213
|
+
| `list.selectAll()` / `list.clearSelection()` | Bulk operations |
|
|
214
|
+
| `list.getSelectedIds()` | Array of selected IDs |
|
|
215
|
+
| `list.getSelectedItems()` | Array of selected items |
|
|
446
216
|
|
|
447
|
-
### Grid
|
|
217
|
+
### Grid (with `withGrid()`)
|
|
448
218
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
219
|
+
| Method | Description |
|
|
220
|
+
|--------|-------------|
|
|
221
|
+
| `list.updateGrid({ columns, gap })` | Update grid at runtime |
|
|
452
222
|
|
|
453
223
|
### Events
|
|
454
224
|
|
|
455
|
-
|
|
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
|
-
```
|
|
466
|
-
|
|
467
|
-
## Configuration
|
|
468
|
-
|
|
469
|
-
### Base Configuration
|
|
470
|
-
|
|
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
|
-
};
|
|
480
|
-
|
|
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
|
|
488
|
-
|
|
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
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Plugin: `withGrid(config)`
|
|
499
|
-
|
|
500
|
-
2D grid layout with virtualized rows.
|
|
501
|
-
|
|
502
|
-
```typescript
|
|
503
|
-
interface GridConfig {
|
|
504
|
-
columns: number; // Number of columns (required)
|
|
505
|
-
gap?: number; // Gap between items in pixels (default: 0)
|
|
506
|
-
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
**Example:**
|
|
510
|
-
```typescript
|
|
511
|
-
.use(withGrid({ columns: 4, gap: 16 }))
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
### Plugin: `withSections(config)`
|
|
515
|
-
|
|
516
|
-
Grouped lists with sticky or inline headers.
|
|
517
|
-
|
|
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)
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
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
|
-
```
|
|
536
|
-
|
|
537
|
-
**Important:** Items must be pre-sorted by group!
|
|
538
|
-
|
|
539
|
-
### Plugin: `withSelection(config)`
|
|
540
|
-
|
|
541
|
-
Single or multiple item selection with keyboard navigation.
|
|
542
|
-
|
|
543
|
-
```typescript
|
|
544
|
-
interface SelectionConfig {
|
|
545
|
-
mode: 'single' | 'multiple';
|
|
546
|
-
initial?: Array<string | number>; // Pre-selected item IDs
|
|
547
|
-
}
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
**Example:**
|
|
551
|
-
```typescript
|
|
552
|
-
.use(withSelection({
|
|
553
|
-
mode: 'multiple',
|
|
554
|
-
initial: [1, 5, 10],
|
|
555
|
-
}))
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### Plugin: `withAsync(config)`
|
|
559
|
-
|
|
560
|
-
Asynchronous data loading with lazy loading and placeholders.
|
|
561
|
-
|
|
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
|
-
};
|
|
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();
|
|
584
|
-
},
|
|
585
|
-
},
|
|
586
|
-
loading: {
|
|
587
|
-
cancelThreshold: 15, // Skip loads when scrolling > 15px/ms
|
|
588
|
-
},
|
|
589
|
-
}))
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
### Plugin: `withScale()`
|
|
593
|
-
|
|
594
|
-
Automatically handles lists with 1M+ items by compressing scroll space when total height exceeds browser limits (~16.7M pixels).
|
|
595
|
-
|
|
596
|
-
```typescript
|
|
597
|
-
.use(withScale()) // No config needed - auto-activates
|
|
598
|
-
```
|
|
599
|
-
|
|
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
|
-
```
|
|
610
|
-
|
|
611
|
-
### Plugin: `withScrollbar(config)`
|
|
612
|
-
|
|
613
|
-
Custom scrollbar with auto-hide and smooth dragging.
|
|
614
|
-
|
|
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
|
-
}))
|
|
629
|
-
```
|
|
630
|
-
|
|
631
|
-
### Plugin: `withPage()`
|
|
632
|
-
|
|
633
|
-
Use document-level scrolling instead of container scrolling.
|
|
634
|
-
|
|
635
|
-
```typescript
|
|
636
|
-
.use(withPage()) // No config needed
|
|
637
|
-
```
|
|
638
|
-
|
|
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
|
-
```
|
|
649
|
-
|
|
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.
|
|
225
|
+
`list.on()` returns an unsubscribe function. You can also use `list.off(event, handler)`.
|
|
655
226
|
|
|
656
227
|
```typescript
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
list.
|
|
228
|
+
list.on('scroll', ({ scrollTop, direction }) => {})
|
|
229
|
+
list.on('range:change', ({ range }) => {})
|
|
230
|
+
list.on('item:click', ({ item, index, event }) => {})
|
|
231
|
+
list.on('item:dblclick', ({ item, index, event }) => {})
|
|
232
|
+
list.on('selection:change', ({ selectedIds, selectedItems }) => {})
|
|
233
|
+
list.on('load:start', ({ offset, limit }) => {})
|
|
234
|
+
list.on('load:end', ({ items, offset, total }) => {})
|
|
235
|
+
list.on('load:error', ({ error, offset, limit }) => {})
|
|
236
|
+
list.on('velocity:change', ({ velocity, reliable }) => {})
|
|
660
237
|
```
|
|
661
238
|
|
|
662
|
-
|
|
663
|
-
```typescript
|
|
664
|
-
// Save on navigation away
|
|
665
|
-
const snapshot = list.getScrollSnapshot();
|
|
666
|
-
sessionStorage.setItem('scroll', JSON.stringify(snapshot));
|
|
239
|
+
### Properties
|
|
667
240
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
list.
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
## Advanced Usage
|
|
241
|
+
| Property | Description |
|
|
242
|
+
|----------|-------------|
|
|
243
|
+
| `list.element` | Root DOM element |
|
|
244
|
+
| `list.items` | Current items (readonly) |
|
|
245
|
+
| `list.total` | Total item count |
|
|
674
246
|
|
|
675
|
-
###
|
|
247
|
+
### Lifecycle
|
|
676
248
|
|
|
677
249
|
```typescript
|
|
678
|
-
|
|
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();
|
|
250
|
+
list.destroy()
|
|
701
251
|
```
|
|
702
252
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
```typescript
|
|
706
|
-
import { vlist, withGrid } from 'vlist';
|
|
253
|
+
## Plugin Configuration
|
|
707
254
|
|
|
708
|
-
|
|
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
|
-
});
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### Combining Multiple Plugins
|
|
255
|
+
Each plugin's config is fully typed â hover in your IDE for details.
|
|
733
256
|
|
|
734
257
|
```typescript
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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();
|
|
258
|
+
withGrid({ columns: 4, gap: 16 })
|
|
259
|
+
withSections({ getGroupForIndex, headerHeight, headerTemplate, sticky?: true })
|
|
260
|
+
withSelection({ mode: 'single' | 'multiple', initial?: [...ids] })
|
|
261
|
+
withAsync({ adapter: { read }, loading?: { cancelThreshold? } })
|
|
262
|
+
withScale() // no config â auto-activates at 16.7M px
|
|
263
|
+
withScrollbar({ autoHide?, autoHideDelay?, minThumbSize? })
|
|
264
|
+
withPage() // no config â uses document scroll
|
|
265
|
+
withSnapshots() // included by default
|
|
761
266
|
```
|
|
762
267
|
|
|
763
|
-
|
|
268
|
+
Full configuration reference â **[vlist.dev](https://vlist.dev)**
|
|
764
269
|
|
|
765
270
|
## Framework Adapters
|
|
766
271
|
|
|
767
|
-
Framework
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
```bash
|
|
774
|
-
npm install @floor/vlist vlist-react
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
```tsx
|
|
778
|
-
import { useVList } from 'vlist-react';
|
|
779
|
-
import '@floor/vlist/styles';
|
|
780
|
-
|
|
781
|
-
function UserList({ users }) {
|
|
782
|
-
const { containerRef, instanceRef } = useVList({
|
|
783
|
-
item: {
|
|
784
|
-
height: 48,
|
|
785
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
786
|
-
},
|
|
787
|
-
items: users,
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
return <div ref={containerRef} style={{ height: 400 }} />;
|
|
791
|
-
}
|
|
792
|
-
```
|
|
793
|
-
|
|
794
|
-
**Documentation:** [github.com/floor/vlist-react](https://github.com/floor/vlist-react)
|
|
795
|
-
|
|
796
|
-
### Vue
|
|
797
|
-
|
|
798
|
-
**Package:** [`vlist-vue`](https://github.com/floor/vlist-vue) - 1.1 KB (0.6 KB gzipped)
|
|
272
|
+
| Framework | Package | Size |
|
|
273
|
+
|-----------|---------|------|
|
|
274
|
+
| React | [`vlist-react`](https://github.com/floor/vlist-react) | 0.6 KB gzip |
|
|
275
|
+
| Vue | [`vlist-vue`](https://github.com/floor/vlist-vue) | 0.6 KB gzip |
|
|
276
|
+
| Svelte | [`vlist-svelte`](https://github.com/floor/vlist-svelte) | 0.5 KB gzip |
|
|
799
277
|
|
|
800
278
|
```bash
|
|
801
|
-
npm install @floor/vlist vlist-vue
|
|
802
|
-
```
|
|
803
|
-
|
|
804
|
-
```vue
|
|
805
|
-
<script setup>
|
|
806
|
-
import { useVList } from 'vlist-vue';
|
|
807
|
-
import '@floor/vlist/styles';
|
|
808
|
-
|
|
809
|
-
const users = ref([...]);
|
|
810
|
-
|
|
811
|
-
const { containerRef, instance } = useVList({
|
|
812
|
-
item: {
|
|
813
|
-
height: 48,
|
|
814
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
815
|
-
},
|
|
816
|
-
items: users,
|
|
817
|
-
});
|
|
818
|
-
</script>
|
|
819
|
-
|
|
820
|
-
<template>
|
|
821
|
-
<div ref="containerRef" style="height: 400px" />
|
|
822
|
-
</template>
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
**Documentation:** [github.com/floor/vlist-vue](https://github.com/floor/vlist-vue)
|
|
826
|
-
|
|
827
|
-
### Svelte
|
|
828
|
-
|
|
829
|
-
**Package:** [`vlist-svelte`](https://github.com/floor/vlist-svelte) - 0.9 KB (0.5 KB gzipped)
|
|
830
|
-
|
|
831
|
-
```bash
|
|
832
|
-
npm install @floor/vlist vlist-svelte
|
|
833
|
-
```
|
|
834
|
-
|
|
835
|
-
```svelte
|
|
836
|
-
<script>
|
|
837
|
-
import { vlist } from 'vlist-svelte';
|
|
838
|
-
import '@floor/vlist/styles';
|
|
839
|
-
|
|
840
|
-
let users = [...];
|
|
841
|
-
let instance;
|
|
842
|
-
|
|
843
|
-
const config = {
|
|
844
|
-
item: {
|
|
845
|
-
height: 48,
|
|
846
|
-
template: (user) => `<div class="user">${user.name}</div>`,
|
|
847
|
-
},
|
|
848
|
-
items: users,
|
|
849
|
-
};
|
|
850
|
-
</script>
|
|
851
|
-
|
|
852
|
-
<div
|
|
853
|
-
use:vlist={{ config, onInstance: (i) => (instance = i) }}
|
|
854
|
-
style="height: 400px"
|
|
855
|
-
/>
|
|
279
|
+
npm install @floor/vlist vlist-react # or vlist-vue / vlist-svelte
|
|
856
280
|
```
|
|
857
281
|
|
|
858
|
-
|
|
282
|
+
Each adapter README has setup examples and API docs.
|
|
859
283
|
|
|
860
284
|
## Styling
|
|
861
285
|
|
|
862
|
-
Import the base styles:
|
|
863
|
-
|
|
864
286
|
```typescript
|
|
865
|
-
import 'vlist/styles'
|
|
287
|
+
import '@floor/vlist/styles' // base styles (required)
|
|
288
|
+
import '@floor/vlist/styles/extras' // optional enhancements
|
|
866
289
|
```
|
|
867
290
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
```css
|
|
871
|
-
.vlist {
|
|
872
|
-
/* Container styles */
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
.vlist-viewport {
|
|
876
|
-
/* Scroll viewport */
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
.vlist-item {
|
|
880
|
-
/* Item wrapper - positioned absolutely */
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
.vlist-item--selected {
|
|
884
|
-
/* Selected state */
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
.vlist-item--focused {
|
|
888
|
-
/* Focused state (keyboard navigation) */
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
.vlist-scrollbar {
|
|
892
|
-
/* Custom scrollbar track */
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
.vlist-scrollbar__thumb {
|
|
896
|
-
/* Scrollbar thumb/handle */
|
|
897
|
-
}
|
|
898
|
-
```
|
|
291
|
+
Override with your own CSS using the `.vlist`, `.vlist-item`, `.vlist-item--selected`, `.vlist-scrollbar` selectors. See [vlist.dev](https://vlist.dev) for theming examples.
|
|
899
292
|
|
|
900
293
|
## Performance
|
|
901
294
|
|
|
902
|
-
### Bundle
|
|
903
|
-
|
|
904
|
-
| Configuration | Size | What's Included |
|
|
905
|
-
|---------------|------|-----------------|
|
|
906
|
-
| Base only | 7.7 KB | Core virtualization |
|
|
907
|
-
| + Grid | 11.7 KB | + 2D layout |
|
|
908
|
-
| + Sections | 12.3 KB | + Grouped lists |
|
|
909
|
-
| + Selection | 10.0 KB | + Item selection |
|
|
910
|
-
| + Async | 13.5 KB | + Data loading |
|
|
911
|
-
| + Scale | 9.9 KB | + 1M+ items |
|
|
912
|
-
| + All plugins | ~16 KB | Everything |
|
|
295
|
+
### Bundle Size
|
|
913
296
|
|
|
914
|
-
|
|
297
|
+
| Configuration | Gzipped |
|
|
298
|
+
|---------------|---------|
|
|
299
|
+
| Base only | 7.7 KB |
|
|
300
|
+
| + Grid | 11.7 KB |
|
|
301
|
+
| + Sections | 12.3 KB |
|
|
302
|
+
| + Async | 13.5 KB |
|
|
303
|
+
| All plugins | ~16 KB |
|
|
915
304
|
|
|
916
305
|
### Memory Efficiency
|
|
917
306
|
|
|
918
|
-
|
|
919
|
-
- **Total height:** 4,800,000 pixels
|
|
920
|
-
- **Visible items:** ~20 (depending on viewport height)
|
|
921
|
-
- **DOM nodes:** ~26 (visible + overscan)
|
|
922
|
-
- **Memory saved:** ~99.97% (26 DOM nodes vs 100,000)
|
|
307
|
+
vlist uses **constant memory** regardless of dataset size through optimized internal architecture:
|
|
923
308
|
|
|
924
|
-
|
|
309
|
+
| Dataset Size | Memory Usage | Notes |
|
|
310
|
+
|--------------|--------------|-------|
|
|
311
|
+
| 10K items | ~0.2 MB | Constant baseline |
|
|
312
|
+
| 100K items | ~0.2 MB | 10Ã items, same memory |
|
|
313
|
+
| 1M items | ~0.4 MB | 100Ã items, 2Ã memory |
|
|
925
314
|
|
|
926
|
-
|
|
315
|
+
**Key advantages:**
|
|
316
|
+
- No array copying â uses references for zero-copy performance
|
|
317
|
+
- No ID indexing overhead â O(1) memory complexity
|
|
318
|
+
- Industry-leading memory efficiency for virtual list libraries
|
|
927
319
|
|
|
928
|
-
|
|
320
|
+
### DOM Efficiency
|
|
929
321
|
|
|
930
|
-
|
|
931
|
-
- Firefox: Latest 2 versions
|
|
932
|
-
- Safari: Latest 2 versions
|
|
933
|
-
- iOS Safari: 12.4+
|
|
934
|
-
- Chrome Android: Latest
|
|
322
|
+
With 100K items: **~26 DOM nodes** in the document (visible + overscan) instead of 100,000.
|
|
935
323
|
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
-
|
|
939
|
-
-
|
|
324
|
+
### Render Performance
|
|
325
|
+
|
|
326
|
+
- **Initial render:** ~8ms (constant, regardless of item count)
|
|
327
|
+
- **Scroll performance:** 120 FPS (perfect smoothness)
|
|
328
|
+
- **1M items:** Same performance as 10K items
|
|
940
329
|
|
|
941
330
|
## TypeScript
|
|
942
331
|
|
|
943
|
-
Fully typed
|
|
332
|
+
Fully typed. Generic over your item type:
|
|
944
333
|
|
|
945
334
|
```typescript
|
|
946
|
-
import { vlist, withGrid, type VList
|
|
335
|
+
import { vlist, withGrid, type VList } from '@floor/vlist'
|
|
947
336
|
|
|
948
|
-
interface Photo {
|
|
949
|
-
id: number;
|
|
950
|
-
url: string;
|
|
951
|
-
title: string;
|
|
952
|
-
}
|
|
337
|
+
interface Photo { id: number; url: string; title: string }
|
|
953
338
|
|
|
954
|
-
const
|
|
339
|
+
const list: VList<Photo> = vlist<Photo>({
|
|
955
340
|
container: '#gallery',
|
|
956
341
|
items: photos,
|
|
957
342
|
item: {
|
|
958
343
|
height: 200,
|
|
959
|
-
template: (photo
|
|
344
|
+
template: (photo) => `<img src="${photo.url}" />`,
|
|
960
345
|
},
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
const list: VList<Photo> = vlist(config)
|
|
964
|
-
.use(withGrid({ columns: 4 }))
|
|
965
|
-
.build();
|
|
966
|
-
```
|
|
967
|
-
|
|
968
|
-
## Migration from Monolithic API
|
|
969
|
-
|
|
970
|
-
If you're using the old monolithic API:
|
|
971
|
-
|
|
972
|
-
### Before (Monolithic - Deprecated)
|
|
973
|
-
|
|
974
|
-
```typescript
|
|
975
|
-
import { createVList } from 'vlist';
|
|
976
|
-
|
|
977
|
-
const list = createVList({
|
|
978
|
-
container: '#app',
|
|
979
|
-
items: data,
|
|
980
|
-
grid: { columns: 4 },
|
|
981
|
-
groups: { ... },
|
|
982
|
-
selection: { mode: 'single' },
|
|
983
|
-
});
|
|
984
|
-
```
|
|
985
|
-
|
|
986
|
-
### After (Builder - Recommended)
|
|
987
|
-
|
|
988
|
-
```typescript
|
|
989
|
-
import { vlist, withGrid, withSections, withSelection } from 'vlist';
|
|
990
|
-
|
|
991
|
-
const list = vlist({
|
|
992
|
-
container: '#app',
|
|
993
|
-
items: data,
|
|
994
346
|
})
|
|
995
347
|
.use(withGrid({ columns: 4 }))
|
|
996
|
-
.
|
|
997
|
-
.use(withSelection({ mode: 'single' }))
|
|
998
|
-
.build();
|
|
348
|
+
.build()
|
|
999
349
|
```
|
|
1000
350
|
|
|
1001
|
-
**Benefits:**
|
|
1002
|
-
- 2-3x smaller bundles (20 KB â 8-12 KB gzipped)
|
|
1003
|
-
- Explicit about what's included
|
|
1004
|
-
- Better tree-shaking
|
|
1005
|
-
- Easier to understand and debug
|
|
1006
|
-
|
|
1007
|
-
## Plugin Naming Changes
|
|
1008
|
-
|
|
1009
|
-
If you're upgrading from an earlier version:
|
|
1010
|
-
|
|
1011
|
-
| Old Name | New Name | Import |
|
|
1012
|
-
|----------|----------|--------|
|
|
1013
|
-
| `withCompression()` | `withScale()` | `import { withScale } from 'vlist'` |
|
|
1014
|
-
| `withData()` | `withAsync()` | `import { withAsync } from 'vlist'` |
|
|
1015
|
-
| `withWindow()` | `withPage()` | `import { withPage } from 'vlist'` |
|
|
1016
|
-
| `withGroups()` | `withSections()` | `import { withSections } from 'vlist'` |
|
|
1017
|
-
| `withScroll()` | `withScrollbar()` | `import { withScrollbar } from 'vlist'` |
|
|
1018
|
-
|
|
1019
351
|
## Contributing
|
|
1020
352
|
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
1. Fork the repository
|
|
1024
|
-
2. Create a feature branch
|
|
1025
|
-
3. Make your changes
|
|
1026
|
-
4. Add/update tests
|
|
1027
|
-
5. Submit a pull request
|
|
353
|
+
1. Fork â branch â make changes â add tests â pull request
|
|
354
|
+
2. Run `bun test` (1739 tests) and `bun run build` before submitting
|
|
1028
355
|
|
|
1029
356
|
## License
|
|
1030
357
|
|
|
1031
|
-
MIT
|
|
358
|
+
[MIT](LICENSE)
|
|
1032
359
|
|
|
1033
360
|
## Links
|
|
1034
361
|
|
|
1035
|
-
- **
|
|
362
|
+
- **Docs & Examples:** [vlist.dev](https://vlist.dev)
|
|
1036
363
|
- **GitHub:** [github.com/floor/vlist](https://github.com/floor/vlist)
|
|
1037
364
|
- **NPM:** [@floor/vlist](https://www.npmjs.com/package/@floor/vlist)
|
|
1038
365
|
- **Issues:** [GitHub Issues](https://github.com/floor/vlist/issues)
|
|
1039
366
|
|
|
1040
367
|
---
|
|
1041
368
|
|
|
1042
|
-
|
|
369
|
+
Built by [Floor IO](https://floor.io)
|