forked from sveltejs/svelte-virtual-list
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathVirtualList.svelte
185 lines (145 loc) · 4.06 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
<script>
import { onMount, tick, createEventDispatcher } 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
export const getViewport = () => viewport
export const toTop = () => (viewport.scrollTop = 0)
export const toBottom = () => (viewport.scrollTop = viewport.scrollHeight)
// local state
let height_map = []
let rows
let viewport
let contents
let viewport_height = 0
let visible
let mounted
const dispatch = createEventDispatcher()
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)
export async function doRefresh() {
return refresh(items, viewport_height, itemHeight)
}
async function refresh(items, viewport_height, itemHeight) {
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(event) {
const { scrollTop, clientHeight, scrollHeight } = viewport
if (scrollTop === 0) {
dispatch('onTopReached')
} else if (scrollTop + clientHeight >= scrollHeight) {
dispatch('onEndReached')
}
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
// prevent jumping if we scrolled up into unknown territory
if (start < old_start) {
await tick()
let expected_height = 0
let actual_height = 0
for (let i = start; i < old_start; i += 1) {
if (rows[i - start]) {
expected_height += height_map[i]
actual_height += itemHeight || rows[i - start].offsetHeight
}
}
const d = actual_height - expected_height
viewport.scrollTo(0, scrollTop + d)
}
dispatch('scroll', event)
// 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?
}
// 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};"
on:touchstart
on:touchmove
on:mousewheel>
<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>