@gka/svelte-scroller 3.0.0-pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -0
- package/README.md +77 -0
- package/Scroller.svelte +229 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright (c) 2018 Rich Harris
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
This license, or a link to its text, must be included with all copies of the software and any derivative works.
|
|
6
|
+
|
|
7
|
+
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
|
|
8
|
+
|
|
9
|
+
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use.
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# svelte-scroller ([demo](https://svelte.dev/repl/76846b7ae27b3a21becb64ffd6e9d4a6?version=3))
|
|
2
|
+
|
|
3
|
+
The Svelte scroller we all love, but in Svelte 5!
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @gka/svelte-scroller
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<script>
|
|
15
|
+
import Scroller from "@gka/svelte-scroller";
|
|
16
|
+
|
|
17
|
+
let index, offset, progress;
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<style>
|
|
21
|
+
section {
|
|
22
|
+
height: 80vh;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
|
|
26
|
+
<Scroller top="{0.2}" bottom="{0.8}" bind:index bind:offset bind:progress>
|
|
27
|
+
{#snippet background()}
|
|
28
|
+
<p>
|
|
29
|
+
This is the background content. It will stay fixed in place while the
|
|
30
|
+
foreground scrolls over the top.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<p>Section {index + 1} is currently active.</p>
|
|
34
|
+
{/snippet}
|
|
35
|
+
|
|
36
|
+
{#snippet foreground()}
|
|
37
|
+
<section>This is the first section.</section>
|
|
38
|
+
<section>This is the second section.</section>
|
|
39
|
+
<section>This is the third section.</section>
|
|
40
|
+
{/snippet}
|
|
41
|
+
</Scroller>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You must have one `background` snippet and one `foreground` snippet — see [Snippets and render tags](https://svelte.dev/tutorial/svelte/snippets-and-render-tags) for more info.
|
|
45
|
+
|
|
46
|
+
## Parameters
|
|
47
|
+
|
|
48
|
+
The following parameters are available:
|
|
49
|
+
|
|
50
|
+
| parameter | default | description |
|
|
51
|
+
| --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| top | 0 | The vertical position that the top of the foreground must scroll past before the background becomes fixed, as a proportion of window height |
|
|
53
|
+
| bottom | 1 | The inverse of `top` — once the bottom of the foreground passes this point, the background becomes unfixed |
|
|
54
|
+
| threshold | 0.5 | Once a section crosses this point, it becomes 'active' |
|
|
55
|
+
| query | 'section' | A CSS selector that describes the individual sections of your foreground |
|
|
56
|
+
| parallax | false | If `true`, the background will scroll such that the bottom edge reaches the `bottom` at the same time as the foreground. This effect can be unpleasant for people with high motion sensitivity, so use it advisedly |
|
|
57
|
+
|
|
58
|
+
## `index`, `offset`, `progress` and `count`
|
|
59
|
+
|
|
60
|
+
By binding to these properties, you can track the user's behaviour:
|
|
61
|
+
|
|
62
|
+
- `index` — the currently active section
|
|
63
|
+
- `offset` — how far the section has scrolled past the `threshold`, as a value between 0 and 1
|
|
64
|
+
- `progress` — how far the foreground has travelled, where 0 is the top of the foreground crossing `top`, and 1 is the bottom crossing `bottom`
|
|
65
|
+
- `count` — the number of sections
|
|
66
|
+
|
|
67
|
+
You can rename them with e.g. `bind:index={i}`.
|
|
68
|
+
|
|
69
|
+
## Configuring webpack
|
|
70
|
+
|
|
71
|
+
If you're using webpack with [svelte-loader](https://github.com/sveltejs/svelte-loader), make sure that you add `"svelte"` to [`resolve.mainFields`](https://webpack.js.org/configuration/resolve/#resolve-mainfields) in your webpack config. This ensures that webpack imports the uncompiled component (`src/index.html`) rather than the compiled version (`index.mjs`) — this is more efficient.
|
|
72
|
+
|
|
73
|
+
If you're using Rollup with [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte), this will happen automatically.
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
[LIL](LICENSE)
|
package/Scroller.svelte
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script module>
|
|
2
|
+
const handlers = [];
|
|
3
|
+
let manager;
|
|
4
|
+
|
|
5
|
+
if (typeof window !== 'undefined') {
|
|
6
|
+
const run_all = () => handlers.forEach(fn => fn());
|
|
7
|
+
|
|
8
|
+
window.addEventListener('scroll', run_all);
|
|
9
|
+
window.addEventListener('resize', run_all);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
13
|
+
const map = new Map();
|
|
14
|
+
|
|
15
|
+
const observer = new IntersectionObserver((entries, observer) => {
|
|
16
|
+
entries.forEach(entry => {
|
|
17
|
+
const update = map.get(entry.target);
|
|
18
|
+
const index = handlers.indexOf(update);
|
|
19
|
+
|
|
20
|
+
if (entry.isIntersecting) {
|
|
21
|
+
if (index === -1) handlers.push(update);
|
|
22
|
+
} else {
|
|
23
|
+
update();
|
|
24
|
+
if (index !== -1) handlers.splice(index, 1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}, {
|
|
28
|
+
rootMargin: '400px 0px' // TODO why 400?
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
manager = {
|
|
32
|
+
add: ({ outer, update }) => {
|
|
33
|
+
const { top, bottom } = outer.getBoundingClientRect();
|
|
34
|
+
|
|
35
|
+
if (top < window.innerHeight && bottom > 0) handlers.push(update);
|
|
36
|
+
|
|
37
|
+
map.set(outer, update);
|
|
38
|
+
observer.observe(outer);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
remove: ({ outer, update }) => {
|
|
42
|
+
const index = handlers.indexOf(update);
|
|
43
|
+
if (index !== -1) handlers.splice(index, 1);
|
|
44
|
+
|
|
45
|
+
map.delete(outer);
|
|
46
|
+
observer.unobserve(outer);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
manager = {
|
|
51
|
+
add: ({ update }) => {
|
|
52
|
+
handlers.push(update);
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
remove: ({ update }) => {
|
|
56
|
+
const index = handlers.indexOf(update);
|
|
57
|
+
if (index !== -1) handlers.splice(index, 1);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<script>
|
|
64
|
+
import { onMount } from 'svelte';
|
|
65
|
+
|
|
66
|
+
let {
|
|
67
|
+
// config
|
|
68
|
+
top = 0,
|
|
69
|
+
bottom = 1,
|
|
70
|
+
threshold = 0.5,
|
|
71
|
+
query = 'section',
|
|
72
|
+
parallax = false,
|
|
73
|
+
// snippets,
|
|
74
|
+
background: snippetBackground,
|
|
75
|
+
foreground: snippetForeground,
|
|
76
|
+
// bindings
|
|
77
|
+
index = $bindable(0),
|
|
78
|
+
count = $bindable(0),
|
|
79
|
+
offset = $bindable(0),
|
|
80
|
+
progress = $bindable(0),
|
|
81
|
+
visible = $bindable(false)
|
|
82
|
+
} = $props();
|
|
83
|
+
|
|
84
|
+
let outer = $state();
|
|
85
|
+
let foreground = $state();
|
|
86
|
+
let background = $state();
|
|
87
|
+
let left = $state();
|
|
88
|
+
let sections = $state([]);
|
|
89
|
+
let wh = $state(0);
|
|
90
|
+
let fixed = $state();
|
|
91
|
+
let offset_top = $state(0);
|
|
92
|
+
let width = $state(1);
|
|
93
|
+
let height = $state();
|
|
94
|
+
let inverted = $state();
|
|
95
|
+
|
|
96
|
+
const top_px = $derived(Math.round(top * wh));
|
|
97
|
+
const bottom_px = $derived(Math.round(bottom * wh));
|
|
98
|
+
const threshold_px = $derived(Math.round(threshold * wh));
|
|
99
|
+
|
|
100
|
+
$effect(() => (top, bottom, threshold, parallax, update()));
|
|
101
|
+
|
|
102
|
+
const style = $derived(`
|
|
103
|
+
position: ${fixed ? 'fixed' : 'absolute'};
|
|
104
|
+
top: 0;
|
|
105
|
+
transform: translate(0, ${offset_top}px);
|
|
106
|
+
z-index: ${inverted ? 3 : 1};
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
const widthStyle = $derived(fixed ? `width:${width}px;` : '');
|
|
110
|
+
|
|
111
|
+
onMount(() => {
|
|
112
|
+
sections = foreground.querySelectorAll(query);
|
|
113
|
+
count = sections.length;
|
|
114
|
+
|
|
115
|
+
update();
|
|
116
|
+
|
|
117
|
+
const scroller = { outer, update };
|
|
118
|
+
|
|
119
|
+
manager.add(scroller);
|
|
120
|
+
return () => manager.remove(scroller);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
function update() {
|
|
124
|
+
if (!foreground) return;
|
|
125
|
+
|
|
126
|
+
// re-measure outer container
|
|
127
|
+
const bcr = outer.getBoundingClientRect();
|
|
128
|
+
left = bcr.left;
|
|
129
|
+
width = bcr.right - left;
|
|
130
|
+
|
|
131
|
+
// determine fix state
|
|
132
|
+
const fg = foreground.getBoundingClientRect();
|
|
133
|
+
const bg = background.getBoundingClientRect();
|
|
134
|
+
|
|
135
|
+
visible = fg.top < wh && fg.bottom > 0;
|
|
136
|
+
|
|
137
|
+
const foreground_height = fg.bottom - fg.top;
|
|
138
|
+
const background_height = bg.bottom - bg.top;
|
|
139
|
+
|
|
140
|
+
const available_space = bottom_px - top_px;
|
|
141
|
+
progress = (top_px - fg.top) / (foreground_height - available_space);
|
|
142
|
+
|
|
143
|
+
if (progress <= 0) {
|
|
144
|
+
offset_top = 0;
|
|
145
|
+
fixed = false;
|
|
146
|
+
} else if (progress >= 1) {
|
|
147
|
+
offset_top = parallax
|
|
148
|
+
? (foreground_height - background_height)
|
|
149
|
+
: (foreground_height - available_space);
|
|
150
|
+
fixed = false;
|
|
151
|
+
} else {
|
|
152
|
+
offset_top = parallax ?
|
|
153
|
+
Math.round(top_px - progress * (background_height - available_space)) :
|
|
154
|
+
top_px;
|
|
155
|
+
fixed = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < sections.length; i++) {
|
|
159
|
+
const section = sections[i];
|
|
160
|
+
const { top } = section.getBoundingClientRect();
|
|
161
|
+
|
|
162
|
+
const next = sections[i + 1];
|
|
163
|
+
const bottom = next ? next.getBoundingClientRect().top : fg.bottom;
|
|
164
|
+
|
|
165
|
+
offset = (threshold_px - top) / (bottom - top);
|
|
166
|
+
if (bottom >= threshold_px) {
|
|
167
|
+
index = i;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<svelte:window bind:innerHeight={wh}/>
|
|
175
|
+
|
|
176
|
+
<svelte-scroller-outer bind:this={outer}>
|
|
177
|
+
<svelte-scroller-background-container class='background-container' style="{style}{widthStyle}">
|
|
178
|
+
<svelte-scroller-background bind:this={background}>
|
|
179
|
+
{@render snippetBackground()}
|
|
180
|
+
</svelte-scroller-background>
|
|
181
|
+
</svelte-scroller-background-container>
|
|
182
|
+
|
|
183
|
+
<svelte-scroller-foreground bind:this={foreground}>
|
|
184
|
+
{@render snippetForeground()}
|
|
185
|
+
</svelte-scroller-foreground>
|
|
186
|
+
</svelte-scroller-outer>
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
svelte-scroller-outer {
|
|
190
|
+
display: block;
|
|
191
|
+
position: relative;
|
|
192
|
+
pointer-events: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
svelte-scroller-background {
|
|
196
|
+
display: block;
|
|
197
|
+
position: relative;
|
|
198
|
+
width: 100%;
|
|
199
|
+
pointer-events: all;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
svelte-scroller-foreground {
|
|
203
|
+
display: block;
|
|
204
|
+
position: relative;
|
|
205
|
+
pointer-events: none;
|
|
206
|
+
z-index: 2;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
svelte-scroller-foreground::after {
|
|
210
|
+
content: ' ';
|
|
211
|
+
display: block;
|
|
212
|
+
clear: both;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
svelte-scroller-background-container {
|
|
216
|
+
display: block;
|
|
217
|
+
position: absolute;
|
|
218
|
+
width: 100%;
|
|
219
|
+
max-width: 100%;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
/* height: 100%; */
|
|
222
|
+
|
|
223
|
+
/* in theory this helps prevent jumping */
|
|
224
|
+
will-change: transform;
|
|
225
|
+
/* -webkit-transform: translate3d(0, 0, 0);
|
|
226
|
+
-moz-transform: translate3d(0, 0, 0);
|
|
227
|
+
transform: translate3d(0, 0, 0); */
|
|
228
|
+
}
|
|
229
|
+
</style>
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gka/svelte-scroller",
|
|
3
|
+
"version": "3.0.0-pre1",
|
|
4
|
+
"description": "A <Scroller> component for Svelte apps",
|
|
5
|
+
"svelte": "Scroller.svelte",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"test": "vitest run",
|
|
9
|
+
"test:watch": "vitest",
|
|
10
|
+
"prepublishOnly": "npm test"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"svelte": "^5.0.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
18
|
+
"jsdom": "^26.0.0",
|
|
19
|
+
"svelte": "^5.0.0",
|
|
20
|
+
"vite": "^6.0.0",
|
|
21
|
+
"vitest": "^3.0.0"
|
|
22
|
+
},
|
|
23
|
+
"repository": "https://github.com/gka/svelte-scroller",
|
|
24
|
+
"author": "Rich Harris",
|
|
25
|
+
"license": "LIL",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"svelte"
|
|
28
|
+
],
|
|
29
|
+
"files": [
|
|
30
|
+
"Scroller.svelte"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"tag": "next"
|
|
34
|
+
}
|
|
35
|
+
}
|