forked from sveltejs/svelte-virtual-list
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathVirtualList.svelte
169 lines (133 loc) · 3.59 KB
/
VirtualList.svelte
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
<script>
import { onMount, tick } from 'svelte';
// props
export let items;
export let height = '100%';
export let itemHeight = undefined;
// read-only, but visible to consumers via bind:start
export let start = 0;
export let end = 0;
// local state
let height_map = [];
let rows;
let viewport;
let contents;
let viewport_height = 0;
let visible;
let mounted;
let top = 0;
let bottom = 0;
let average_height;
$: visible = items.slice(start, end).map((data, i) => {
return { index: i + start, data };
});
// whenever `items` changes, invalidate the current heightmap
$: if (mounted) refresh(items, viewport_height, itemHeight);
async function refresh(items, viewport_height, itemHeight) {
const isStartOverflow = items.length < start
if (isStartOverflow) {
await scrollToIndex(items.length - 1, {behavior: 'auto'})
}
const { scrollTop } = viewport;
await tick(); // wait until the DOM is up to date
let content_height = top - scrollTop;
let i = start;
while (content_height < viewport_height && i < items.length) {
let row = rows[i - start];
if (!row) {
end = i + 1;
await tick(); // render the newly visible row
row = rows[i - start];
}
const row_height = height_map[i] = itemHeight || row.offsetHeight;
content_height += row_height;
i += 1;
}
end = i;
const remaining = items.length - end;
average_height = (top + content_height) / end;
bottom = remaining * average_height;
height_map.length = items.length;
}
async function handle_scroll() {
const { scrollTop } = viewport;
const old_start = start;
for (let v = 0; v < rows.length; v += 1) {
height_map[start + v] = itemHeight || rows[v].offsetHeight;
}
let i = 0;
let y = 0;
while (i < items.length) {
const row_height = height_map[i] || average_height;
if (y + row_height > scrollTop) {
start = i;
top = y;
break;
}
y += row_height;
i += 1;
}
while (i < items.length) {
y += height_map[i] || average_height;
i += 1;
if (y > scrollTop + viewport_height) break;
}
end = i;
const remaining = items.length - end;
average_height = y / end;
while (i < items.length) height_map[i++] = average_height;
bottom = remaining * average_height;
// TODO if we overestimated the space these
// rows would occupy we may need to add some
// more. maybe we can just call handle_scroll again?
}
export async function scrollToIndex (index, opts) {
const {scrollTop} = viewport;
const itemsDelta = index - start;
const _itemHeight = itemHeight || average_height;
const distance = itemsDelta * _itemHeight;
opts = {
left: 0,
top: scrollTop + distance,
behavior: 'smooth',
...opts
};
viewport.scrollTo(opts);
}
// trigger initial refresh
onMount(() => {
rows = contents.getElementsByTagName('svelte-virtual-list-row');
mounted = true;
});
</script>
<style>
svelte-virtual-list-viewport {
position: relative;
overflow-y: auto;
-webkit-overflow-scrolling:touch;
display: block;
}
svelte-virtual-list-contents, svelte-virtual-list-row {
display: block;
}
svelte-virtual-list-row {
overflow: hidden;
}
</style>
<svelte-virtual-list-viewport
bind:this={viewport}
bind:offsetHeight={viewport_height}
on:scroll={handle_scroll}
style="height: {height};"
>
<svelte-virtual-list-contents
bind:this={contents}
style="padding-top: {top}px; padding-bottom: {bottom}px;"
>
{#each visible as row (row.index)}
<svelte-virtual-list-row>
<slot item={row.data}>Missing template</slot>
</svelte-virtual-list-row>
{/each}
</svelte-virtual-list-contents>
</svelte-virtual-list-viewport>