scroll-snap-kit 1.0.0
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/README.md +181 -0
- package/package.json +49 -0
- package/src/hooks.js +136 -0
- package/src/index.js +22 -0
- package/src/utils.js +154 -0
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# scroll-snap-kit 🎯
|
|
2
|
+
|
|
3
|
+
> Smooth scroll utilities + React hooks for modern web apps.
|
|
4
|
+
|
|
5
|
+
Zero dependencies. Tree-shakeable. Works with or without React.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install scroll-snap-kit
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Vanilla JS Utilities
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
import { scrollTo, scrollToTop, scrollToBottom, getScrollPosition, onScroll, isInViewport, lockScroll, unlockScroll } from 'scroll-snap-kit/utils';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### `scrollTo(target, options?)`
|
|
24
|
+
|
|
25
|
+
Smoothly scroll to a DOM element or a Y pixel value.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
scrollTo(document.querySelector('#section'));
|
|
29
|
+
scrollTo(500); // scroll to y=500px
|
|
30
|
+
scrollTo(document.querySelector('#hero'), { offset: -80 }); // with offset (e.g. sticky header)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| Option | Type | Default | Description |
|
|
34
|
+
|--------|------|---------|-------------|
|
|
35
|
+
| `behavior` | `'smooth' \| 'instant'` | `'smooth'` | Scroll behavior |
|
|
36
|
+
| `block` | `ScrollLogicalPosition` | `'start'` | Vertical alignment |
|
|
37
|
+
| `offset` | `number` | `0` | Pixel offset adjustment |
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### `scrollToTop(options?)` / `scrollToBottom(options?)`
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
scrollToTop();
|
|
45
|
+
scrollToBottom({ behavior: 'instant' });
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### `getScrollPosition(container?)`
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const { x, y, percentX, percentY } = getScrollPosition();
|
|
54
|
+
// percentY = how far down the page (0–100)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### `onScroll(callback, options?)`
|
|
60
|
+
|
|
61
|
+
Throttled scroll listener. Returns a cleanup function.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const stop = onScroll(({ y, percentY }) => {
|
|
65
|
+
console.log(`Scrolled ${percentY}% down`);
|
|
66
|
+
}, { throttle: 150 });
|
|
67
|
+
|
|
68
|
+
// Later:
|
|
69
|
+
stop(); // removes the listener
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### `isInViewport(element, options?)`
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
if (isInViewport(document.querySelector('.card'), { threshold: 0.5 })) {
|
|
78
|
+
// At least 50% of the card is visible
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `lockScroll()` / `unlockScroll()`
|
|
85
|
+
|
|
86
|
+
Lock page scroll (e.g. when a modal is open) and restore position on unlock.
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
lockScroll(); // body stops scrolling
|
|
90
|
+
unlockScroll(); // restored to previous position
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## React Hooks
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
import { useScrollPosition, useInViewport, useScrollTo, useScrolledPast, useScrollDirection } from 'scroll-snap-kit/hooks';
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `useScrollPosition(options?)`
|
|
102
|
+
|
|
103
|
+
```jsx
|
|
104
|
+
function ProgressBar() {
|
|
105
|
+
const { percentY } = useScrollPosition({ throttle: 50 });
|
|
106
|
+
return <div style={{ width: `${percentY}%` }} className="progress" />;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
### `useInViewport(options?)`
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
function FadeIn() {
|
|
116
|
+
const [ref, inView] = useInViewport({ threshold: 0.2, once: true });
|
|
117
|
+
return (
|
|
118
|
+
<div ref={ref} style={{ opacity: inView ? 1 : 0, transition: 'opacity 0.5s' }}>
|
|
119
|
+
I fade in when visible!
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Option | Type | Default | Description |
|
|
126
|
+
|--------|------|---------|-------------|
|
|
127
|
+
| `threshold` | `number` | `0` | 0–1 portion of element visible to trigger |
|
|
128
|
+
| `once` | `boolean` | `false` | Only trigger the first time |
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `useScrollTo()`
|
|
133
|
+
|
|
134
|
+
```jsx
|
|
135
|
+
function Page() {
|
|
136
|
+
const [containerRef, scrollToTarget] = useScrollTo();
|
|
137
|
+
const sectionRef = useRef(null);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div ref={containerRef} style={{ overflowY: 'scroll', height: '400px' }}>
|
|
141
|
+
<button onClick={() => scrollToTarget(sectionRef.current)}>Jump to section</button>
|
|
142
|
+
<div style={{ height: 300 }} />
|
|
143
|
+
<div ref={sectionRef}>Target section</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
### `useScrolledPast(threshold?, options?)`
|
|
152
|
+
|
|
153
|
+
```jsx
|
|
154
|
+
function BackToTopButton() {
|
|
155
|
+
const scrolledPast = useScrolledPast(300);
|
|
156
|
+
return scrolledPast ? <button onClick={scrollToTop}>↑ Top</button> : null;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### `useScrollDirection()`
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
function Navbar() {
|
|
166
|
+
const direction = useScrollDirection();
|
|
167
|
+
return (
|
|
168
|
+
<nav style={{ transform: direction === 'down' ? 'translateY(-100%)' : 'translateY(0)' }}>
|
|
169
|
+
My Navbar
|
|
170
|
+
</nav>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Returns `'up'`, `'down'`, or `null` (on initial render).
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scroll-snap-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Smooth scroll utilities and React hooks for modern web apps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./utils": {
|
|
13
|
+
"import": "./src/utils.js"
|
|
14
|
+
},
|
|
15
|
+
"./hooks": {
|
|
16
|
+
"import": "./src/hooks.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "echo \"No tests yet\""
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"scroll",
|
|
27
|
+
"smooth-scroll",
|
|
28
|
+
"scroll-hooks",
|
|
29
|
+
"react-hooks",
|
|
30
|
+
"viewport",
|
|
31
|
+
"scroll-position",
|
|
32
|
+
"frontend",
|
|
33
|
+
"ui"
|
|
34
|
+
],
|
|
35
|
+
"author": "Fabian Faraz Farid",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=16.8.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"react": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/farazfarid/scroll-snap-kit"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/hooks.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll-snap-kit — React Hooks
|
|
3
|
+
* Requires React 16.8+
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
6
|
+
import { getScrollPosition, onScroll, isInViewport } from './utils.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the current scroll position, updated on scroll.
|
|
10
|
+
* @param {{ throttle?: number, container?: Element }} options
|
|
11
|
+
* @returns {{ x: number, y: number, percentX: number, percentY: number }}
|
|
12
|
+
*/
|
|
13
|
+
export function useScrollPosition(options = {}) {
|
|
14
|
+
const { throttle = 100, container } = options;
|
|
15
|
+
const [position, setPosition] = useState(() =>
|
|
16
|
+
typeof window !== 'undefined' ? getScrollPosition(container) : { x: 0, y: 0, percentX: 0, percentY: 0 }
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const cleanup = onScroll((pos) => setPosition(pos), { throttle, container });
|
|
21
|
+
return cleanup;
|
|
22
|
+
}, [throttle, container]);
|
|
23
|
+
|
|
24
|
+
return position;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns whether a referenced element is currently in the viewport.
|
|
29
|
+
* @param {{ threshold?: number, once?: boolean }} options
|
|
30
|
+
* @returns {[React.RefObject, boolean]}
|
|
31
|
+
*/
|
|
32
|
+
export function useInViewport(options = {}) {
|
|
33
|
+
const { threshold = 0, once = false } = options;
|
|
34
|
+
const ref = useRef(null);
|
|
35
|
+
const [inView, setInView] = useState(false);
|
|
36
|
+
const hasTriggered = useRef(false);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!ref.current) return;
|
|
40
|
+
|
|
41
|
+
const observer = new IntersectionObserver(
|
|
42
|
+
([entry]) => {
|
|
43
|
+
const visible = entry.isIntersecting;
|
|
44
|
+
if (once) {
|
|
45
|
+
if (visible && !hasTriggered.current) {
|
|
46
|
+
hasTriggered.current = true;
|
|
47
|
+
setInView(true);
|
|
48
|
+
observer.disconnect();
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
setInView(visible);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{ threshold }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
observer.observe(ref.current);
|
|
58
|
+
return () => observer.disconnect();
|
|
59
|
+
}, [threshold, once]);
|
|
60
|
+
|
|
61
|
+
return [ref, inView];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a scrollTo function scoped to an element ref or the window.
|
|
66
|
+
* @returns {[React.RefObject, (target: Element|number, options?: object) => void]}
|
|
67
|
+
*/
|
|
68
|
+
export function useScrollTo() {
|
|
69
|
+
const containerRef = useRef(null);
|
|
70
|
+
|
|
71
|
+
const scrollToTarget = useCallback((target, options = {}) => {
|
|
72
|
+
const { behavior = 'smooth', offset = 0 } = options;
|
|
73
|
+
const container = containerRef.current;
|
|
74
|
+
|
|
75
|
+
if (!container) {
|
|
76
|
+
// fallback to window scroll
|
|
77
|
+
if (typeof target === 'number') {
|
|
78
|
+
window.scrollTo({ top: target + offset, behavior });
|
|
79
|
+
} else if (target instanceof Element) {
|
|
80
|
+
target.scrollIntoView({ behavior });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof target === 'number') {
|
|
86
|
+
container.scrollTo({ top: target + offset, behavior });
|
|
87
|
+
} else if (target instanceof Element) {
|
|
88
|
+
const containerTop = container.getBoundingClientRect().top;
|
|
89
|
+
const targetTop = target.getBoundingClientRect().top;
|
|
90
|
+
container.scrollBy({ top: targetTop - containerTop + offset, behavior });
|
|
91
|
+
}
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
return [containerRef, scrollToTarget];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tracks whether the user has scrolled past a given pixel threshold.
|
|
99
|
+
* @param {number} threshold - Y pixel value to check against (default: 100)
|
|
100
|
+
* @param {{ container?: Element }} options
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function useScrolledPast(threshold = 100, options = {}) {
|
|
104
|
+
const { container } = options;
|
|
105
|
+
const [scrolledPast, setScrolledPast] = useState(false);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const cleanup = onScroll(({ y }) => {
|
|
109
|
+
setScrolledPast(y > threshold);
|
|
110
|
+
}, { container });
|
|
111
|
+
return cleanup;
|
|
112
|
+
}, [threshold, container]);
|
|
113
|
+
|
|
114
|
+
return scrolledPast;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns scroll direction: 'up' | 'down' | null
|
|
119
|
+
* @param {{ throttle?: number }} options
|
|
120
|
+
* @returns {'up'|'down'|null}
|
|
121
|
+
*/
|
|
122
|
+
export function useScrollDirection(options = {}) {
|
|
123
|
+
const { throttle = 100 } = options;
|
|
124
|
+
const [direction, setDirection] = useState(null);
|
|
125
|
+
const lastY = useRef(typeof window !== 'undefined' ? window.scrollY : 0);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
const cleanup = onScroll(({ y }) => {
|
|
129
|
+
setDirection(y > lastY.current ? 'down' : 'up');
|
|
130
|
+
lastY.current = y;
|
|
131
|
+
}, { throttle });
|
|
132
|
+
return cleanup;
|
|
133
|
+
}, [throttle]);
|
|
134
|
+
|
|
135
|
+
return direction;
|
|
136
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// scroll-snap-kit
|
|
2
|
+
// Smooth scroll utilities + React hooks
|
|
3
|
+
// https://github.com/farazfarid/scroll-snap-kit
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
scrollTo,
|
|
7
|
+
scrollToTop,
|
|
8
|
+
scrollToBottom,
|
|
9
|
+
getScrollPosition,
|
|
10
|
+
onScroll,
|
|
11
|
+
isInViewport,
|
|
12
|
+
lockScroll,
|
|
13
|
+
unlockScroll,
|
|
14
|
+
} from './utils.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
useScrollPosition,
|
|
18
|
+
useInViewport,
|
|
19
|
+
useScrollTo,
|
|
20
|
+
useScrolledPast,
|
|
21
|
+
useScrollDirection,
|
|
22
|
+
} from './hooks.js';
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scroll-snap-kit — Core Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Smoothly scrolls to a target element or position.
|
|
7
|
+
* @param {Element|number} target - A DOM element or Y pixel value
|
|
8
|
+
* @param {{ behavior?: ScrollBehavior, block?: ScrollLogicalPosition, offset?: number }} options
|
|
9
|
+
*/
|
|
10
|
+
export function scrollTo(target, options = {}) {
|
|
11
|
+
const { behavior = 'smooth', block = 'start', offset = 0 } = options;
|
|
12
|
+
|
|
13
|
+
if (typeof target === 'number') {
|
|
14
|
+
window.scrollTo({ top: target + offset, behavior });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!(target instanceof Element)) {
|
|
19
|
+
console.warn('[scroll-snap-kit] scrollTo: target must be an Element or a number');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (offset !== 0) {
|
|
24
|
+
const y = target.getBoundingClientRect().top + window.scrollY + offset;
|
|
25
|
+
window.scrollTo({ top: y, behavior });
|
|
26
|
+
} else {
|
|
27
|
+
target.scrollIntoView({ behavior, block });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Smoothly scrolls to the top of the page.
|
|
33
|
+
* @param {{ behavior?: ScrollBehavior }} options
|
|
34
|
+
*/
|
|
35
|
+
export function scrollToTop(options = {}) {
|
|
36
|
+
const { behavior = 'smooth' } = options;
|
|
37
|
+
window.scrollTo({ top: 0, behavior });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Smoothly scrolls to the bottom of the page.
|
|
42
|
+
* @param {{ behavior?: ScrollBehavior }} options
|
|
43
|
+
*/
|
|
44
|
+
export function scrollToBottom(options = {}) {
|
|
45
|
+
const { behavior = 'smooth' } = options;
|
|
46
|
+
window.scrollTo({ top: document.body.scrollHeight, behavior });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns the current scroll position and scroll percentage.
|
|
51
|
+
* @param {Element} [container=window] - Optional scrollable container
|
|
52
|
+
* @returns {{ x: number, y: number, percentX: number, percentY: number }}
|
|
53
|
+
*/
|
|
54
|
+
export function getScrollPosition(container) {
|
|
55
|
+
if (container && container instanceof Element) {
|
|
56
|
+
const { scrollLeft, scrollTop, scrollWidth, scrollHeight, clientWidth, clientHeight } = container;
|
|
57
|
+
return {
|
|
58
|
+
x: scrollLeft,
|
|
59
|
+
y: scrollTop,
|
|
60
|
+
percentX: scrollWidth > clientWidth ? Math.round((scrollLeft / (scrollWidth - clientWidth)) * 100) : 0,
|
|
61
|
+
percentY: scrollHeight > clientHeight ? Math.round((scrollTop / (scrollHeight - clientHeight)) * 100) : 0,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const x = window.scrollX;
|
|
66
|
+
const y = window.scrollY;
|
|
67
|
+
const maxX = document.body.scrollWidth - window.innerWidth;
|
|
68
|
+
const maxY = document.body.scrollHeight - window.innerHeight;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
x,
|
|
72
|
+
y,
|
|
73
|
+
percentX: maxX > 0 ? Math.round((x / maxX) * 100) : 0,
|
|
74
|
+
percentY: maxY > 0 ? Math.round((y / maxY) * 100) : 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Attaches a throttled scroll event listener.
|
|
80
|
+
* @param {(position: ReturnType<typeof getScrollPosition>) => void} callback
|
|
81
|
+
* @param {{ throttle?: number, container?: Element }} options
|
|
82
|
+
* @returns {() => void} Cleanup function to remove the listener
|
|
83
|
+
*/
|
|
84
|
+
export function onScroll(callback, options = {}) {
|
|
85
|
+
const { throttle: throttleMs = 100, container } = options;
|
|
86
|
+
const target = container || window;
|
|
87
|
+
|
|
88
|
+
let ticking = false;
|
|
89
|
+
let lastTime = 0;
|
|
90
|
+
|
|
91
|
+
const handler = () => {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (now - lastTime < throttleMs) {
|
|
94
|
+
if (!ticking) {
|
|
95
|
+
ticking = true;
|
|
96
|
+
requestAnimationFrame(() => {
|
|
97
|
+
callback(getScrollPosition(container));
|
|
98
|
+
ticking = false;
|
|
99
|
+
lastTime = Date.now();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
lastTime = now;
|
|
105
|
+
callback(getScrollPosition(container));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
target.addEventListener('scroll', handler, { passive: true });
|
|
109
|
+
return () => target.removeEventListener('scroll', handler);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Checks whether an element is currently visible in the viewport.
|
|
114
|
+
* @param {Element} element
|
|
115
|
+
* @param {{ threshold?: number }} options - threshold: 0–1, portion of element that must be visible
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
export function isInViewport(element, options = {}) {
|
|
119
|
+
if (!(element instanceof Element)) {
|
|
120
|
+
console.warn('[scroll-snap-kit] isInViewport: argument must be an Element');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const { threshold = 0 } = options;
|
|
125
|
+
const rect = element.getBoundingClientRect();
|
|
126
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
127
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
128
|
+
|
|
129
|
+
const verticalVisible = rect.top + rect.height * threshold < windowHeight && rect.bottom - rect.height * threshold > 0;
|
|
130
|
+
const horizontalVisible = rect.left + rect.width * threshold < windowWidth && rect.right - rect.width * threshold > 0;
|
|
131
|
+
|
|
132
|
+
return verticalVisible && horizontalVisible;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Locks the page scroll (e.g. when a modal is open).
|
|
137
|
+
*/
|
|
138
|
+
export function lockScroll() {
|
|
139
|
+
const scrollY = window.scrollY;
|
|
140
|
+
document.body.style.position = 'fixed';
|
|
141
|
+
document.body.style.top = `-${scrollY}px`;
|
|
142
|
+
document.body.style.width = '100%';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Unlocks the page scroll and restores position.
|
|
147
|
+
*/
|
|
148
|
+
export function unlockScroll() {
|
|
149
|
+
const scrollY = document.body.style.top;
|
|
150
|
+
document.body.style.position = '';
|
|
151
|
+
document.body.style.top = '';
|
|
152
|
+
document.body.style.width = '';
|
|
153
|
+
window.scrollTo(0, parseInt(scrollY || '0') * -1);
|
|
154
|
+
}
|