@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 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)
@@ -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
+ }