|
1 | 1 | <script lang="ts">
|
2 |
| - import { onMount, tick } from "svelte" |
3 |
| - import View from "../ol/View.svelte" |
| 2 | + import { onMount, tick } from 'svelte' |
4 | 3 |
|
5 |
| - export let items: any[] |
6 |
| - export let height = '100%' |
| 4 | + export let items: any[] |
| 5 | + export let height = '100%' |
7 | 6 |
|
8 |
| - export let scrollPos: number = 0 |
| 7 | + export let scrollPos: number = 0 |
9 | 8 |
|
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 |
14 | 11 |
|
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 |
17 | 21 |
|
18 |
| - let contents: HTMLElement; |
| 22 | + let viewport: HTMLElement |
| 23 | + let contents: HTMLElement |
19 | 24 |
|
20 |
| - let top = 0 |
21 |
| - let bottom = 0 |
22 |
| - let averageHeight = 0 |
| 25 | + let top = 0 |
| 26 | + let bottom = 0 |
| 27 | + let averageHeight = 0 |
23 | 28 |
|
24 |
| - let startHeight = 0 |
25 |
| - let start = 0 |
26 |
| - let endHeight = 0 |
27 |
| - let end = 0 |
| 29 | + let start = 0 |
| 30 | + let end = 0 |
28 | 31 |
|
29 |
| - let mounted = false |
| 32 | + let mounted = false |
30 | 33 |
|
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, |
35 | 41 | }))
|
| 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 | + } |
36 | 56 |
|
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 |
82 | 61 | }
|
| 62 | + end = Math.max(start, i) + 1 |
| 63 | + updateVisible() |
83 | 64 |
|
84 |
| - console.log(`Start: ${start}, End: ${end}, Top: ${top}, Bottom: ${bottom}`) |
85 |
| - } |
| 65 | + await tick() |
86 | 66 |
|
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 | + } |
92 | 72 |
|
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 | + } |
99 | 79 |
|
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 | + } |
107 | 84 |
|
108 |
| - l-contents, l-row { |
109 |
| - display: block; |
| 85 | + y += info.height |
| 86 | + end = i + 1 |
110 | 87 | }
|
111 | 88 |
|
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> |
116 | 105 |
|
117 | 106 | <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> |
132 | 122 | </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> |
0 commit comments