Skip to content

Commit e220331

Browse files
WIP virtual list
1 parent de7d03c commit e220331

File tree

4 files changed

+135
-120
lines changed

4 files changed

+135
-120
lines changed

src/components/BetterList.svelte

Lines changed: 124 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,132 +1,147 @@
11
<script lang="ts">
2-
import { onMount, tick } from "svelte"
3-
import View from "../ol/View.svelte"
2+
import { onMount, tick } from 'svelte'
43
5-
export let items: any[]
6-
export let height = '100%'
4+
export let items: any[]
5+
export let height = '100%'
76
8-
export let scrollPos: number = 0
7+
export let scrollPos: number = 0
98
10-
let heightMap = new WeakMap<any, {
11-
offset: number,
12-
height: number
13-
}>();
9+
// Number of additional elements out of view. Helps when scrolling fast.
10+
export let pad = 1
1411
15-
let viewport: HTMLElement
16-
let viewportHeight = 0
12+
let heightMap = new WeakMap<
13+
any,
14+
{
15+
offset: number
16+
height: number
17+
}
18+
>()
19+
let heights = []
20+
window['heights'] = heights
1721
18-
let contents: HTMLElement;
22+
let viewport: HTMLElement
23+
let contents: HTMLElement
1924
20-
let top = 0
21-
let bottom = 0
22-
let averageHeight = 0
25+
let top = 0
26+
let bottom = 0
27+
let averageHeight = 0
2328
24-
let startHeight = 0
25-
let start = 0
26-
let endHeight = 0
27-
let end = 0
29+
let start = 0
30+
let end = 0
2831
29-
let mounted = false
32+
let mounted = false
3033
31-
let rows: any
32-
$: visible = items.slice(start, end).map((data, i) => ({
33-
index: i + start,
34-
data
34+
let visible = []
35+
function updateVisible() {
36+
const s = Math.max(0, start - pad)
37+
const e = Math.min(end + pad, items.length - 1)
38+
visible = items.slice(s, e).map((data, i) => ({
39+
index: i + s,
40+
data,
3541
}))
42+
}
43+
updateVisible()
44+
45+
$: if (mounted) updateStartAndEnd(top, bottom, items)
46+
47+
async function updateStartAndEnd(...args: any) {
48+
if (top === bottom) return
49+
50+
start = 0
51+
let y = 0
52+
for (let i = 0; i < items.length; ++i) {
53+
if (y >= bottom) {
54+
break
55+
}
3656
37-
$: if(mounted) updateStartAndEnd(top, bottom, items)
38-
39-
async function updateStartAndEnd(top, bottom, items) {
40-
if (top === bottom) return
41-
42-
let y = 0
43-
for (let i = 0; i < items.length; ++i) {
44-
if (y >= bottom) break
45-
46-
if (!heightMap.has(items[i])) {
47-
// update start/end and render row
48-
if (y <= top) {
49-
start = i
50-
startHeight = y
51-
}
52-
end = i + 1
53-
54-
await tick()
55-
56-
console.log(`Rendered ${start} to ${end}`)
57-
58-
const row = contents.querySelector(`l-row[data-row="${i}"]`)
59-
if (!row) {
60-
console.log(contents.childNodes)
61-
console.log("Something went wrong!")
62-
break
63-
}
64-
heightMap.set(items[i], {
65-
offset: row['offsetHeight'],
66-
height: row.clientHeight
67-
})
68-
}
69-
70-
const info = heightMap.get(items[i])
71-
if (y <= top && y + info.height >= top) {
72-
start = i
73-
startHeight = y
74-
}
75-
76-
endHeight = y
77-
end = i + 1
78-
79-
if (y > bottom) {
80-
break
81-
}
57+
if (!heightMap.has(items[i])) {
58+
// update start/end and render row
59+
if (y <= top) {
60+
start = i
8261
}
62+
end = Math.max(start, i) + 1
63+
updateVisible()
8364
84-
console.log(`Start: ${start}, End: ${end}, Top: ${top}, Bottom: ${bottom}`)
85-
}
65+
await tick()
8666
87-
function onScroll() {
88-
top = viewport.scrollTop
89-
bottom = top + viewport.clientHeight
90-
scrollPos = top
91-
}
67+
const row = contents.querySelector(`l-row[data-row="${i}"]`)
68+
if (!row) {
69+
console.error("Didn't manage to render row", i)
70+
break
71+
}
9272
93-
onMount(() => {
94-
mounted = true
95-
rows = contents.getElementsByTagName('l-row')
96-
onScroll()
97-
})
98-
</script>
73+
heightMap.set(items[i], {
74+
offset: y,
75+
height: row['offsetHeight'],
76+
})
77+
heights[i] = heightMap.get(items[i])
78+
}
9979
100-
<style>
101-
l-viewport {
102-
position: relative;
103-
overflow-y: auto;
104-
-webkit-overflow-scrolling: touch;
105-
display: block;
106-
}
80+
const info = heightMap.get(items[i])
81+
if (y <= top) {
82+
start = i
83+
}
10784
108-
l-contents, l-row {
109-
display: block;
85+
y += info.height
86+
end = i + 1
11087
}
11188
112-
l-row {
113-
overflow: hidden;
114-
}
115-
</style>
89+
averageHeight =
90+
heights.reduce((prev, next) => prev + next.height, 0) / heights.length
91+
updateVisible()
92+
}
93+
94+
function onScroll() {
95+
top = viewport.scrollTop
96+
bottom = top + viewport.clientHeight
97+
scrollPos = top
98+
}
99+
100+
onMount(() => {
101+
mounted = true
102+
onScroll()
103+
})
104+
</script>
116105

117106
<l-viewport
118-
bind:this={viewport}
119-
bind:offsetHeight={viewportHeight}
120-
on:scroll={onScroll}
121-
style:height="{height}">
122-
<l-contents bind:this={contents}
123-
style:padding-top="{startHeight}px"
124-
style:padding-bottom="{bottom}px">
125-
{#each visible as row (row.index)}
126-
<l-row data-row={row.index}>
127-
<slot item={row.data}>No template</slot>
128-
</l-row>
129-
{/each}
130-
131-
</l-contents>
107+
bind:this={viewport}
108+
on:resize={onScroll}
109+
on:scroll={onScroll}
110+
style:height>
111+
<l-contents
112+
bind:this={contents}
113+
style:height="{averageHeight * items.length}px">
114+
{#each visible as row}
115+
<l-row
116+
data-row={row.index}
117+
style:top="{heights[row.index]?.offset ?? averageHeight * row.index}px">
118+
<slot item={row.data}>No template</slot>
119+
</l-row>
120+
{/each}
121+
</l-contents>
132122
</l-viewport>
123+
124+
<style>
125+
l-viewport {
126+
position: relative;
127+
overflow-y: auto;
128+
-webkit-overflow-scrolling: touch;
129+
display: block;
130+
}
131+
132+
l-contents,
133+
l-row {
134+
display: block;
135+
}
136+
137+
l-contents {
138+
position: relative;
139+
}
140+
141+
l-row {
142+
overflow: hidden;
143+
position: absolute;
144+
left: 0;
145+
right: 0;
146+
}
147+
</style>

src/components/VirtualList.svelte

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
export let itemHeight = undefined;
99
1010
export let scrollPos: number = 0;
11-
12-
// Handle changes to the scroll binding.
13-
$: {
14-
if (viewport && scrollPos != viewport?.scrollTop) {
15-
setTimeout(() => scrollTo(scrollPos))
16-
}
17-
}
11+
let initialScrollPos: number = scrollPos;
1812
1913
let lastItems = [];
2014
@@ -148,6 +142,7 @@
148142
}
149143
150144
export async function scrollTo(y: number) {
145+
console.log("Scroll to", y)
151146
let last = -1
152147
const dir = viewport.scrollTop < y ? 1 : -1
153148
while (((viewport.scrollTop < y && dir < 0) || (viewport.scrollTop > y && dir > 0))
@@ -156,7 +151,7 @@
156151
157152
const scrollBy = height_map[start] || average_height || height
158153
159-
console.log(dir, scrollBy, viewport.scrollTop, y)
154+
// console.log(dir, scrollBy, viewport.scrollTop, y)
160155
161156
last = viewport.scrollTop
162157
viewport.scrollBy({ top: dir * scrollBy })
@@ -176,6 +171,10 @@
176171
onMount(() => {
177172
rows = contents.getElementsByTagName('svelte-virtual-list-row');
178173
mounted = true;
174+
175+
tick().then(() => {
176+
scrollTo(initialScrollPos)
177+
})
179178
});
180179
</script>
181180

src/components/mountains/MountainInfo.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
99
$: mountain = $mountains[id] || ({} as Mountain)
1010
$: places = mountain.places ?? []
11-
$: console.log(id, route)
1211
</script>
1312

1413
<PlaceInfo {mountain} scrollToRoute={route} />

src/components/mountains/MountainsFilter.svelte

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import { extent } from '../../stores/map'
2020
import { containsCoordinate } from 'ol/extent'
2121
import { onMount } from 'svelte'
22+
import BettererList from '../BettererList.svelte'
23+
import BetterList from '../BetterList.svelte'
2224
2325
let hasGrade: number
2426
@@ -94,7 +96,7 @@
9496
(showing {filteredMountains.length} of {totalMountains} mountains)
9597
</div>
9698
<div class="flex flex-col gap-2 -mx-4 -mb-4 min-h-0 flex-1">
97-
<VirtualList items={sorted} let:item bind:scrollPos={$scrollPos}>
99+
<BetterList items={sorted} let:item>
98100
<div
99101
class="cursor-pointer px-4 py-1"
100102
on:keyup={(e) => {
@@ -104,7 +106,7 @@
104106
on:click={(e) => viewMountain(item)}>
105107
<MountainCard mountain={item} />
106108
</div>
107-
</VirtualList>
109+
</BetterList>
108110
</div>
109111
</div>
110112

0 commit comments

Comments
 (0)