Skip to content

Commit c2e0344

Browse files
author
Georg Perhofer
committed
Merge branch 'no-key'
2 parents 7e85df2 + 48ae91a commit c2e0344

File tree

12 files changed

+824
-13
lines changed

12 files changed

+824
-13
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/node_modules/
22
/example/public/build/
3-
dist/
43
types/
4+
#/dist # for github package reference.
55
.idea/
66
.DS_Store
77
/build
8-
/dist
98
/.svelte-kit
109
/package
1110
.env

dist/Item.svelte

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<script>
2+
import {afterUpdate, createEventDispatcher, onDestroy, onMount} from "svelte"
3+
4+
export let horizontal = false
5+
export let uniqueKey
6+
export let type = "item"
7+
8+
let resizeObserver
9+
let itemDiv
10+
let previousSize
11+
12+
const dispatch = createEventDispatcher()
13+
const shapeKey = horizontal ? "offsetWidth" : "offsetHeight"
14+
15+
onMount(() => {
16+
if (typeof ResizeObserver !== "undefined") {
17+
resizeObserver = new ResizeObserver(dispatchSizeChange)
18+
resizeObserver.observe(itemDiv)
19+
}
20+
})
21+
afterUpdate(dispatchSizeChange)
22+
onDestroy(() => {
23+
if (resizeObserver) {
24+
resizeObserver.disconnect()
25+
resizeObserver = null
26+
}
27+
})
28+
29+
function dispatchSizeChange() {
30+
const size = itemDiv ? itemDiv[shapeKey] : 0
31+
if (size === previousSize) return
32+
previousSize = size
33+
dispatch("resize", {id: uniqueKey, size, type})
34+
}
35+
</script>
36+
37+
<div bind:this={itemDiv} class="virtual-scroll-item">
38+
<slot/>
39+
</div>

dist/Item.svelte.d.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/** @typedef {typeof __propDef.props} ItemProps */
2+
/** @typedef {typeof __propDef.events} ItemEvents */
3+
/** @typedef {typeof __propDef.slots} ItemSlots */
4+
export default class Item extends SvelteComponentTyped<{
5+
uniqueKey: any;
6+
horizontal?: boolean | undefined;
7+
type?: string | undefined;
8+
}, {
9+
resize: CustomEvent<any>;
10+
} & {
11+
[evt: string]: CustomEvent<any>;
12+
}, {
13+
default: {};
14+
}> {
15+
}
16+
export type ItemProps = typeof __propDef.props;
17+
export type ItemEvents = typeof __propDef.events;
18+
export type ItemSlots = typeof __propDef.slots;
19+
import { SvelteComponentTyped } from "svelte";
20+
declare const __propDef: {
21+
props: {
22+
uniqueKey: any;
23+
horizontal?: boolean | undefined;
24+
type?: string | undefined;
25+
};
26+
events: {
27+
resize: CustomEvent<any>;
28+
} & {
29+
[evt: string]: CustomEvent<any>;
30+
};
31+
slots: {
32+
default: {};
33+
};
34+
};
35+
export {};

dist/VirtualScroll.svelte

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
<script>
2+
import Virtual, {isBrowser} from "./virtual"
3+
import Item from "./Item.svelte"
4+
import {createEventDispatcher, onDestroy, onMount} from "svelte"
5+
6+
/**
7+
* Source for list
8+
* @type {Array<any>}
9+
*/
10+
export let data
11+
/**
12+
* Count of rendered items
13+
* @type {number}
14+
*/
15+
export let keeps = 30
16+
/**
17+
* Estimate size of each item, needs for smooth scrollbar
18+
* @type {number}
19+
*/
20+
export let estimateSize = 50
21+
/**
22+
* Scroll direction
23+
* @type {boolean}
24+
*/
25+
export let isHorizontal = false
26+
/**
27+
* scroll position start index
28+
*/
29+
export let start = 0
30+
/**
31+
* scroll position offset
32+
*/
33+
export let offset = 0
34+
/**
35+
* Let virtual list using global document to scroll through the list
36+
* @type {boolean}
37+
*/
38+
export let pageMode = false
39+
/**
40+
* The threshold to emit `top` event, attention to multiple calls.
41+
* @type {number}
42+
*/
43+
export let topThreshold = 0
44+
/**
45+
* The threshold to emit `bottom` event, attention to multiple calls.
46+
* @type {number}
47+
*/
48+
export let bottomThreshold = 0
49+
50+
let displayItems = []
51+
let paddingStyle
52+
let directionKey = isHorizontal ? "scrollLeft" : "scrollTop"
53+
let range = null
54+
let virtual = new Virtual({
55+
slotHeaderSize: 0,
56+
slotFooterSize: 0,
57+
keeps: keeps,
58+
estimateSize: estimateSize,
59+
buffer: Math.round(keeps / 3), // recommend for a third of keeps
60+
uniqueIds: getUniqueIdFromDataSources(),
61+
}, onRangeChanged)
62+
let root
63+
let shepherd
64+
const dispatch = createEventDispatcher()
65+
66+
/**
67+
* @type {(id: number) => number}
68+
*/
69+
export function getSize(id) {
70+
return virtual.sizes.get(id)
71+
}
72+
73+
/**
74+
* Count of items
75+
* @type {() => number}
76+
*/
77+
export function getSizes() {
78+
return virtual.sizes.size
79+
}
80+
81+
/**
82+
* @type {() => number}
83+
*/
84+
export function getOffset() {
85+
if (pageMode && isBrowser()) {
86+
return document.documentElement[directionKey] || document.body[directionKey]
87+
} else {
88+
return root ? Math.ceil(root[directionKey]) : 0
89+
}
90+
}
91+
92+
/**
93+
* @type {() => number}
94+
*/
95+
export function getClientSize() {
96+
const key = isHorizontal ? "clientWidth" : "clientHeight"
97+
if (pageMode && isBrowser()) {
98+
return document.documentElement[key] || document.body[key]
99+
} else {
100+
return root ? Math.ceil(root[key]) : 0
101+
}
102+
}
103+
104+
/**
105+
* @type {() => number}
106+
*/
107+
export function getScrollSize() {
108+
const key = isHorizontal ? "scrollWidth" : "scrollHeight"
109+
if (pageMode && isBrowser()) {
110+
return document.documentElement[key] || document.body[key]
111+
} else {
112+
return root ? Math.ceil(root[key]) : 0
113+
}
114+
}
115+
116+
/**
117+
* @type {() => void}
118+
*/
119+
export function updatePageModeFront() {
120+
if (root && isBrowser()) {
121+
const rect = root.getBoundingClientRect()
122+
const {defaultView} = root.ownerDocument
123+
const offsetFront = isHorizontal ? (rect.left + defaultView.pageXOffset) : (rect.top + defaultView.pageYOffset)
124+
virtual.updateParam("slotHeaderSize", offsetFront)
125+
}
126+
}
127+
128+
/**
129+
* @type {(offset: number) => void}
130+
*/
131+
export function scrollToOffset(offset) {
132+
if (!isBrowser()) return
133+
if (pageMode) {
134+
document.body[directionKey] = offset
135+
document.documentElement[directionKey] = offset
136+
} else if (root) {
137+
root[directionKey] = offset
138+
}
139+
}
140+
141+
/**
142+
* @type {(index: number) => void}
143+
*/
144+
export function scrollToIndex(index) {
145+
if (index >= data.length - 1) {
146+
scrollToBottom()
147+
} else {
148+
const offset = virtual.getOffset(index)
149+
scrollToOffset(offset)
150+
}
151+
}
152+
153+
/**
154+
* @type {() => void}
155+
*/
156+
export function scrollToBottom() {
157+
if (shepherd) {
158+
const offset = shepherd[isHorizontal ? "offsetLeft" : "offsetTop"]
159+
scrollToOffset(offset)
160+
161+
// check if it's really scrolled to the bottom
162+
// maybe list doesn't render and calculate to last range,
163+
// so we need retry in next event loop until it really at bottom
164+
setTimeout(() => {
165+
if (getOffset() + getClientSize() + 1 < getScrollSize()) {
166+
scrollToBottom()
167+
}
168+
}, 3)
169+
}
170+
}
171+
172+
onMount(() => {
173+
if (start) {
174+
scrollToIndex(start)
175+
} else if (offset) {
176+
scrollToOffset(offset)
177+
}
178+
179+
if (pageMode) {
180+
updatePageModeFront()
181+
182+
document.addEventListener("scroll", onScroll, {
183+
passive: false,
184+
})
185+
}
186+
})
187+
188+
onDestroy(() => {
189+
virtual.destroy()
190+
if (pageMode && isBrowser()) {
191+
document.removeEventListener("scroll", onScroll)
192+
}
193+
})
194+
195+
function getUniqueIdFromDataSources() {
196+
return data.map((dataSource, i) => i)
197+
}
198+
199+
function onItemResized(event) {
200+
const {id, size, type} = event.detail
201+
if (type === "item")
202+
virtual.saveSize(id, size)
203+
else if (type === "slot") {
204+
if (id === "header")
205+
virtual.updateParam("slotHeaderSize", size)
206+
else if (id === "footer")
207+
virtual.updateParam("slotFooterSize", size)
208+
209+
// virtual.handleSlotSizeChange()
210+
}
211+
}
212+
213+
function onRangeChanged(range_) {
214+
range = range_
215+
paddingStyle = paddingStyle = isHorizontal ? `0px ${range.padBehind}px 0px ${range.padFront}px` : `${range.padFront}px 0px ${range.padBehind}px`
216+
displayItems = data.slice(range.start, range.end + 1)
217+
}
218+
219+
function onScroll(event) {
220+
const offset = getOffset()
221+
const clientSize = getClientSize()
222+
const scrollSize = getScrollSize()
223+
224+
// iOS scroll-spring-back behavior will make direction mistake
225+
if (offset < 0 || (offset + clientSize > scrollSize) || !scrollSize) {
226+
return
227+
}
228+
229+
virtual.handleScroll(offset)
230+
emitEvent(offset, clientSize, scrollSize, event)
231+
}
232+
233+
function emitEvent(offset, clientSize, scrollSize, event) {
234+
dispatch("scroll", {event, range: virtual.getRange()})
235+
236+
if (virtual.isFront() && !!data.length && (offset - topThreshold <= 0)) {
237+
dispatch("top")
238+
} else if (virtual.isBehind() && (offset + clientSize + bottomThreshold >= scrollSize)) {
239+
dispatch("bottom")
240+
}
241+
}
242+
243+
$: scrollToOffset(offset)
244+
$: scrollToIndex(start)
245+
$: handleKeepsChange(keeps)
246+
247+
function handleKeepsChange(keeps) {
248+
virtual.updateParam("keeps", keeps)
249+
virtual.handleSlotSizeChange()
250+
}
251+
252+
$: handleDataSourcesChange(data)
253+
254+
async function handleDataSourcesChange(data) {
255+
virtual.updateParam("uniqueIds", getUniqueIdFromDataSources())
256+
virtual.handleDataSourcesChange()
257+
}
258+
</script>
259+
260+
<div bind:this={root} on:scroll={onScroll} style="overflow-y: auto; height: inherit" class="virtual-scroll-root">
261+
{#if $$slots.header}
262+
<Item on:resize={onItemResized} type="slot" uniqueKey="header">
263+
<slot name="header"/>
264+
</Item>
265+
{/if}
266+
<div style="padding: {paddingStyle}" class="virtual-scroll-wrapper">
267+
{#each displayItems as dataItem, dataIndex}
268+
<Item
269+
on:resize={onItemResized}
270+
uniqueKey={dataIndex}
271+
horizontal={isHorizontal}
272+
type="item">
273+
<slot data={dataItem} index={dataIndex} />
274+
</Item>
275+
{/each}
276+
</div>
277+
{#if $$slots.footer}
278+
<Item on:resize={onItemResized} type="slot" uniqueKey="footer">
279+
<slot name="footer"/>
280+
</Item>
281+
{/if}
282+
<div bind:this={shepherd} class="shepherd"
283+
style="width: {isHorizontal ? '0px' : '100%'};height: {isHorizontal ? '100%' : '0px'}"></div>
284+
</div>

0 commit comments

Comments
 (0)